Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1086-per-country-core-pins.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Each country now carries its own policyengine-core pin (microcosm#1086, option 2). Every extra that installs policyengine-us (`microcosm-build[us]`, `microcosm-data[us]` and `microcosm-frame[policyengine]`) pins `policyengine-core==3.32.5`, the core the certified US default was built with, and the workspace declares each of them in conflict with each extra that installs policyengine-uk, so uv locks the two sides in separate forks and refuses any install that asks for both. Nothing imports both engines and CI installs one country extra per job. This change moves no engine version: every package in `uv.lock` keeps its version, and `APPROVED_UV_LOCK_SHA256` is re-pinned for the new lock. The UK side can now follow policyengine-uk's own core floor, and the US side moves with its next certified build. A lock-level test reads `uv.lock` through `uv export --frozen`: every US install path resolves policyengine-core 3.32.5, policyengine-us 2.2.1 and spm-calculator 1.0.0, and every US and UK combination is refused.
5 changes: 5 additions & 0 deletions packages/microcosm-build/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@ dependencies = [
# The US extra adds the rules engine for formula-owned export checks. Source
# content is declared in packaged manifests and interpreted by shared Microcosm
# runtimes; country source loaders must not depend on incumbent data packages.
# microcosm#1086 (option 2): each country carries its own policyengine-core
# pin. The US side stays on the core the certified US default was built with
# until its next certified build; [tool.uv] conflicts keeps it apart from the
# UK side, which follows policyengine-uk's own core floor.
us = [
"policyengine-us>=2.2.1,<3",
"policyengine-core==3.32.5",
"microcosm-data>=0.1,<0.2",
"h5py>=3",
"microunit>=0.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
PRIMARY_QRF_WORKER_MODULE = "microcosm.build.us_runtime.puf_qrf_worker"
PRIMARY_QRF_INTERPRETER_PLACEHOLDER = "{python_interpreter}"
APPROVED_UV_LOCK_SHA256 = (
"696ea49c875ae8e811a9d5e624e14b199252017df2b6eeb4161fcedf5578a554"
"a31eb026f0091cefa3e6afcd1613e04ac6a232aee752b573350e49d389476cfb"
)
LEGACY_CAMPAIGN_UV_LOCK_SHA256 = (
"27f47e385cfa35e2644a37410d1804b361ad9aee123577551c8421547bda65ee"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""Each country keeps its own policyengine-core pin (microcosm#1086, option 2).

Nothing imports both engines and CI installs one country extra per job, so
every extra that installs policyengine-us is declared in conflict with every
extra that installs policyengine-uk, and uv locks the two sides in separate
forks. The US side stays on the core the certified US default was built with
until its next certified build. The resolution checks read the committed
``uv.lock`` through ``uv export --frozen``, uv's own reading of what an install
path gets, without the network or the environment.
"""

from __future__ import annotations

import itertools
import shutil
import subprocess
import tomllib

import pytest
from packaging.requirements import Requirement

from test_support.paths import REPOSITORY_ROOT

#: What every US install path resolves until the next certified US build.
US_ENGINE = {
"policyengine-core": "3.32.5",
"policyengine-us": "2.2.1",
"spm-calculator": "1.0.0",
}
ENGINE_PACKAGES = ("microcosm-build", "microcosm-data", "microcosm-frame")


def _optional_dependencies(package: str) -> dict[str, list[str]]:
pyproject = REPOSITORY_ROOT / "packages" / package / "pyproject.toml"
project = tomllib.loads(pyproject.read_text())["project"]
return project.get("optional-dependencies", {})


def _extras_installing(engine: str) -> list[tuple[str, str]]:
return [
(package, extra)
for package in ENGINE_PACKAGES
for extra, requirements in _optional_dependencies(package).items()
if any(Requirement(requirement).name == engine for requirement in requirements)
]


US_EXTRAS = _extras_installing("policyengine-us")
UK_EXTRAS = _extras_installing("policyengine-uk")


def _uv_export(*args: str) -> subprocess.CompletedProcess[str]:
uv = shutil.which("uv")
assert uv is not None, "uv reads the lock as an install would; CI sets it up."
return subprocess.run(
[uv, "export", "--frozen", "--no-hashes", "--no-header"]
+ ["--no-emit-workspace", *args],
cwd=REPOSITORY_ROOT,
capture_output=True,
text=True,
check=False,
)


def _resolved(requirements: str) -> dict[str, str]:
versions = {}
for line in requirements.splitlines():
pin = line.split(";")[0].strip()
if "==" in pin and not pin.startswith(("#", "-")):
name, version = pin.split("==", 1)
versions[name.strip()] = version.strip()
return versions


def test_the_rule_covers_every_engine_extra() -> None:
# A new extra that installs either engine must join the conflicts below.
assert US_EXTRAS == [
("microcosm-build", "us"),
("microcosm-data", "us"),
("microcosm-frame", "policyengine"),
]
assert UK_EXTRAS == [
("microcosm-build", "uk"),
("microcosm-data", "uk"),
("microcosm-frame", "uk"),
]


@pytest.mark.parametrize(("package", "extra"), US_EXTRAS)
def test_every_us_engine_extra_pins_the_us_core(package: str, extra: str) -> None:
requirements = _optional_dependencies(package)[extra]
assert f"policyengine-core=={US_ENGINE['policyengine-core']}" in requirements


def _us_install_paths() -> list[tuple[str, ...]]:
return [
("--all-packages", "--extra", "us"),
*(("--package", package, "--extra", extra) for package, extra in US_EXTRAS),
]


@pytest.mark.parametrize("args", _us_install_paths(), ids=" ".join)
def test_every_us_install_path_resolves_the_certified_us_engine(
args: tuple[str, ...],
) -> None:
result = _uv_export(*args)

assert result.returncode == 0, result.stderr
resolved = _resolved(result.stdout)
assert {name: resolved.get(name) for name in US_ENGINE} == US_ENGINE
assert "policyengine-uk" not in resolved


def test_every_us_and_uk_pair_is_declared_and_locked_as_a_conflict() -> None:
expected = {
frozenset({us, uk}) for us, uk in itertools.product(US_EXTRAS, UK_EXTRAS)
}

def pairs(groups: list[list[dict[str, str]]]) -> set[frozenset[tuple[str, str]]]:
return {
frozenset((item["package"], item["extra"]) for item in group)
for group in groups
}

workspace = tomllib.loads((REPOSITORY_ROOT / "pyproject.toml").read_text())
lock = tomllib.loads((REPOSITORY_ROOT / "uv.lock").read_text())
assert pairs(workspace["tool"]["uv"]["conflicts"]) == expected
assert pairs(lock["conflicts"]) == expected


def _us_and_uk_combinations() -> list[tuple[str, ...]]:
same_package = [
("--package", us_package, "--extra", us_extra, "--extra", uk_extra)
for (us_package, us_extra), (uk_package, uk_extra) in itertools.product(
US_EXTRAS, UK_EXTRAS
)
if us_package == uk_package
]
extra_names = sorted(
{
(us_extra, uk_extra)
for (_, us_extra), (_, uk_extra) in itertools.product(US_EXTRAS, UK_EXTRAS)
}
)
workspace = [
("--all-packages", "--extra", us_extra, "--extra", uk_extra)
for us_extra, uk_extra in extra_names
]
return same_package + workspace


@pytest.mark.parametrize("args", _us_and_uk_combinations(), ids=" ".join)
def test_uv_refuses_every_us_and_uk_combination(args: tuple[str, ...]) -> None:
result = _uv_export(*args)

assert result.returncode != 0
assert "incompatible with the declared conflicts" in result.stderr
4 changes: 3 additions & 1 deletion packages/microcosm-data/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ microcosm-publish-release = "microcosm.data.publish_cli:main"
# so a 1.x engine cannot load an artifact built for this floor and a 1.x
# artifact cannot be loaded by this engine. Cap below 3 to guard the next
# breaking dataset-schema major.
us = ["policyengine-us>=2.2.1,<3"]
# microcosm#1086 (option 2): the US side keeps its own policyengine-core pin,
# the core the certified US default was built with.
us = ["policyengine-us>=2.2.1,<3", "policyengine-core==3.32.5"]
# The UK artifact lives in a PRIVATE repo (UK Data Service licence): loads
# require an authenticated HF token with access. The loader surfaces the
# 401 with that explanation rather than retrying.
Expand Down
8 changes: 7 additions & 1 deletion packages/microcosm-frame/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ microcosm-graph = { workspace = true }

[project.optional-dependencies]
us = ["microunit>=0.1.0"]
policyengine = ["policyengine-us>=2.2.1,<3", "microunit>=0.1.0"]
# microcosm#1086 (option 2): policyengine installs policyengine-us, so it
# carries the US side's own policyengine-core pin.
policyengine = [
"policyengine-us>=2.2.1,<3",
"policyengine-core==3.32.5",
"microunit>=0.1.0",
]
uk = ["policyengine-uk>=2.98.0"]
# The Axiom adapter's PyPI-resolvable dependencies. The engine itself
# (axiom-rules-engine + its dense native extension) is not on PyPI yet and
Expand Down
18 changes: 18 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ version = "0.0.0"
description = "Workspace root for the microcosm stack (not a published package)"
requires-python = ">=3.13"

[tool.uv]
# microcosm#1086 (option 2, ruled 2026-10-07): each country keeps its own
# policyengine-core pin. Nothing imports both engines and CI installs one
# country extra per job, so every extra that installs policyengine-us
# conflicts with every extra that installs policyengine-uk, and uv locks the
# two sides in separate forks.
conflicts = [
[{ package = "microcosm-build", extra = "us" }, { package = "microcosm-build", extra = "uk" }],
[{ package = "microcosm-build", extra = "us" }, { package = "microcosm-data", extra = "uk" }],
[{ package = "microcosm-build", extra = "us" }, { package = "microcosm-frame", extra = "uk" }],
[{ package = "microcosm-data", extra = "us" }, { package = "microcosm-build", extra = "uk" }],
[{ package = "microcosm-data", extra = "us" }, { package = "microcosm-data", extra = "uk" }],
[{ package = "microcosm-data", extra = "us" }, { package = "microcosm-frame", extra = "uk" }],
[{ package = "microcosm-frame", extra = "policyengine" }, { package = "microcosm-build", extra = "uk" }],
[{ package = "microcosm-frame", extra = "policyengine" }, { package = "microcosm-data", extra = "uk" }],
[{ package = "microcosm-frame", extra = "policyengine" }, { package = "microcosm-frame", extra = "uk" }],
]

[tool.uv.workspace]
members = ["packages/*"]

Expand Down
Loading
Loading