Skip to content

Commit 3e59b13

Browse files
MaxGhenisclaude
andcommitted
Merge origin/main; re-cut the UK spec and gate-battery digests over the union (third application)
Main moved the attested surfaces again (#743 first calibrated UK candidate, #766 CI lane, #764 rename), so the merge re-pins the UK spec_sha256, re-cuts the three gate-battery digests into the microcosm-data contract and its test mirror, and regenerates the release-input coverage manifest over the union - the d70ea39 pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 parents d3a4b46 + 2263df3 commit 3e59b13

59 files changed

Lines changed: 6799 additions & 531 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/test.yml‎

Lines changed: 271 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,251 @@ on:
77
branches: [main]
88

99
jobs:
10-
test:
10+
changes:
11+
runs-on: ubuntu-latest
12+
outputs:
13+
shared: ${{ steps.filter.outputs.shared }}
14+
us: ${{ steps.filter.outputs.us }}
15+
uk: ${{ steps.filter.outputs.uk }}
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Classify changed paths
19+
id: filter
20+
env:
21+
GITHUB_TOKEN: ${{ github.token }}
22+
run: |
23+
python3 - <<'PY'
24+
from __future__ import annotations
25+
26+
import fnmatch
27+
import json
28+
import os
29+
import subprocess
30+
import urllib.request
31+
32+
event_name = os.environ["GITHUB_EVENT_NAME"]
33+
event = json.loads(open(os.environ["GITHUB_EVENT_PATH"]).read())
34+
repo = os.environ["GITHUB_REPOSITORY"]
35+
token = os.environ["GITHUB_TOKEN"]
36+
37+
def api_json(url: str) -> object:
38+
request = urllib.request.Request(
39+
url,
40+
headers={
41+
"Accept": "application/vnd.github+json",
42+
"Authorization": f"Bearer {token}",
43+
"X-GitHub-Api-Version": "2022-11-28",
44+
},
45+
)
46+
with urllib.request.urlopen(request) as response:
47+
return json.loads(response.read())
48+
49+
def pull_request_files() -> list[str]:
50+
number = event["pull_request"]["number"]
51+
files: list[str] = []
52+
page = 1
53+
while True:
54+
payload = api_json(
55+
f"https://api.github.com/repos/{repo}/pulls/{number}/files"
56+
f"?per_page=100&page={page}"
57+
)
58+
if not payload:
59+
return files
60+
files.extend(item["filename"] for item in payload)
61+
page += 1
62+
63+
def push_files() -> list[str]:
64+
before = event.get("before")
65+
after = event.get("after")
66+
if before and after and set(before) != {"0"}:
67+
payload = api_json(
68+
f"https://api.github.com/repos/{repo}/compare/{before}...{after}"
69+
)
70+
return [item["filename"] for item in payload.get("files", [])]
71+
output = subprocess.check_output(
72+
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", after],
73+
text=True,
74+
)
75+
return [line for line in output.splitlines() if line]
76+
77+
changed = pull_request_files() if event_name == "pull_request" else push_files()
78+
79+
filters = {
80+
"shared": {
81+
"include": [
82+
"uv.lock",
83+
"pyproject.toml",
84+
"packages/*/pyproject.toml",
85+
"conftest.py",
86+
# Every shard's src, not just build+frame: microcosm-build
87+
# depends on frame, fit, and calibrate (and on data via the
88+
# us extra), and 19 country test files import calibrate/fit
89+
# — e.g. uk_runtime/diagnostics.py imports microcosm.calibrate.
90+
# The country-runtime excludes below keep country-scoped
91+
# changes classifying narrowly.
92+
"packages/*/src/**",
93+
# The quiet shards' tests execute in the engine tier (us-am),
94+
# so a change to them must open that lane. fnmatch has no
95+
# brace expansion, hence three lines.
96+
"packages/microcosm-calibrate/tests/**",
97+
"packages/microcosm-data/tests/**",
98+
"packages/microcosm-fit/tests/**",
99+
"specs/**",
100+
".github/workflows/**",
101+
"tools/**",
102+
],
103+
"exclude": [
104+
"packages/microcosm-build/src/microcosm/build/us_runtime/**",
105+
"packages/microcosm-build/src/microcosm/build/uk_runtime/**",
106+
"tools/*us*",
107+
"tools/*uk*",
108+
],
109+
},
110+
"us": {
111+
"include": [
112+
"build/us/**",
113+
"packages/microcosm-build/src/microcosm/build/us_runtime/**",
114+
"packages/microcosm-build/tests/test_us_*",
115+
"packages/microcosm-frame/tests/test_policyengine_us_*",
116+
"packages/microcosm-frame/tests/test_rules_engine_contract.py",
117+
"tools/*us*",
118+
],
119+
"exclude": [],
120+
},
121+
"uk": {
122+
"include": [
123+
"build/uk/**",
124+
"packages/microcosm-build/src/microcosm/build/uk_runtime/**",
125+
"packages/microcosm-build/tests/test_uk_*",
126+
"packages/microcosm-frame/tests/test_policyengine_uk_adapter.py",
127+
"tools/*uk*",
128+
],
129+
"exclude": [],
130+
},
131+
}
132+
133+
def matches(path: str, include: list[str], exclude: list[str]) -> bool:
134+
included = any(fnmatch.fnmatch(path, pattern) for pattern in include)
135+
excluded = any(fnmatch.fnmatch(path, pattern) for pattern in exclude)
136+
return included and not excluded
137+
138+
outputs = {
139+
name: any(
140+
matches(path, spec["include"], spec["exclude"])
141+
for path in changed
142+
)
143+
for name, spec in filters.items()
144+
}
145+
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
146+
for name, value in outputs.items():
147+
print(f"{name}={str(value).lower()}", file=output)
148+
print("changed paths:")
149+
for path in changed:
150+
print(path)
151+
print("filters:", outputs)
152+
PY
153+
154+
lint:
155+
runs-on: ubuntu-latest
156+
steps:
157+
- uses: actions/checkout@v4
158+
- uses: astral-sh/setup-uv@v6
159+
with:
160+
python-version: "3.13"
161+
- name: Verify CI test groups
162+
# python3 (not bare `python`): guaranteed on the Ubuntu image, and this
163+
# step runs before any sync so it must hold with the stdlib alone.
164+
run: python3 tools/ci_test_groups.py --verify
165+
- name: Sync workspace
166+
run: uv sync --all-packages --locked
167+
- name: Lint
168+
run: uv run --no-sync ruff check .
169+
170+
fast:
11171
runs-on: ubuntu-latest
12172
strategy:
173+
fail-fast: false
13174
matrix:
14175
python-version: ["3.13", "3.14"]
176+
group: [trade, spine-uk, rest]
15177
steps:
16178
- uses: actions/checkout@v4
17179
- uses: astral-sh/setup-uv@v6
18180
with:
19181
python-version: ${{ matrix.python-version }}
20-
- name: Sync workspace with US engine metadata
21-
# The pool-input sweeps are live PolicyEngine-US ownership and consumer
22-
# existence guards. Installing the US extra makes an upstream formula or
23-
# consumer change fail here instead of being skipped until a remote build.
24-
run: uv sync --all-packages --extra us
25-
- name: Behavioral contract suite + unit tests
26-
# One pytest process per shard. A single process accumulates the
27-
# engine plus every shard's fixtures and repeatedly OOM-killed the
28-
# 7 GB runner at ~91% (the runner-shutdown signature, three runs in
29-
# a row); per-shard processes bound residency and give per-shard
30-
# attribution if one shard ever outgrows the runner on its own.
182+
- name: Sync workspace without engines
183+
run: uv sync --all-packages --locked
184+
- name: Engine-free tests (${{ matrix.group }})
31185
run: |
32-
for shard in packages/*/; do
33-
uv run pytest "${shard}tests" -p no:cacheprovider
34-
done
35-
- name: Lint
36-
run: uv run ruff check .
186+
mapfile -t files < <(uv run --no-sync python tools/ci_test_groups.py --list "${{ matrix.group }}")
187+
/usr/bin/time -v uv run --no-sync pytest "${files[@]}" -p no:cacheprovider
188+
189+
engine-shared:
190+
# Deliberately does NOT depend on `changes`: it reads no classifier output,
191+
# and a dependency-skip (the inline script makes live API calls with no
192+
# retry) would take the unconditional engine lane down with it.
193+
runs-on: ubuntu-latest
194+
strategy:
195+
fail-fast: false
196+
matrix:
197+
python-version: ["3.13", "3.14"]
198+
steps:
199+
- uses: actions/checkout@v4
200+
- uses: astral-sh/setup-uv@v6
201+
with:
202+
python-version: ${{ matrix.python-version }}
203+
- name: Sync workspace with US and UK engines
204+
run: uv sync --all-packages --locked --extra us --extra uk
205+
- name: Shared/spec engine tests
206+
run: |
207+
mapfile -t files < <(uv run --no-sync python tools/ci_test_groups.py --list shared-spec)
208+
/usr/bin/time -v uv run --no-sync pytest "${files[@]}" -p no:cacheprovider
209+
210+
engine-us:
211+
needs: changes
212+
if: github.event_name == 'push' || needs.changes.outputs.us == 'true' || needs.changes.outputs.shared == 'true'
213+
runs-on: ubuntu-latest
214+
strategy:
215+
fail-fast: false
216+
matrix:
217+
python-version: ["3.13", "3.14"]
218+
group: [us-p, us-qs, us-not, us-am]
219+
steps:
220+
- uses: actions/checkout@v4
221+
- uses: astral-sh/setup-uv@v6
222+
with:
223+
python-version: ${{ matrix.python-version }}
224+
- name: Sync workspace with US and UK engines
225+
run: uv sync --all-packages --locked --extra us --extra uk
226+
- name: US engine tests (${{ matrix.group }})
227+
run: |
228+
while IFS= read -r proc; do
229+
mapfile -t files < <(uv run --no-sync python tools/ci_test_groups.py --list "${{ matrix.group }}:${proc}")
230+
/usr/bin/time -v uv run --no-sync pytest "${files[@]}" -p no:cacheprovider
231+
done < <(uv run --no-sync python tools/ci_test_groups.py --procs "${{ matrix.group }}")
232+
233+
engine-uk:
234+
needs: changes
235+
if: github.event_name == 'push' || needs.changes.outputs.uk == 'true' || needs.changes.outputs.shared == 'true'
236+
runs-on: ubuntu-latest
237+
strategy:
238+
fail-fast: false
239+
matrix:
240+
python-version: ["3.13", "3.14"]
241+
group: [uk]
242+
steps:
243+
- uses: actions/checkout@v4
244+
- uses: astral-sh/setup-uv@v6
245+
with:
246+
python-version: ${{ matrix.python-version }}
247+
- name: Sync workspace with US and UK engines
248+
run: uv sync --all-packages --locked --extra us --extra uk
249+
- name: UK engine tests
250+
run: |
251+
while IFS= read -r proc; do
252+
mapfile -t files < <(uv run --no-sync python tools/ci_test_groups.py --list "uk:${proc}")
253+
/usr/bin/time -v uv run --no-sync pytest "${files[@]}" -p no:cacheprovider
254+
done < <(uv run --no-sync python tools/ci_test_groups.py --procs uk)
37255
38256
wheels:
39257
# The charter-mandated packaging gate: editable installs hide packaging
@@ -43,6 +261,7 @@ jobs:
43261
# semantics, and run the suite against the installed constellation.
44262
runs-on: ubuntu-latest
45263
strategy:
264+
fail-fast: false
46265
matrix:
47266
python-version: ["3.13", "3.14"]
48267
steps:
@@ -106,5 +325,39 @@ jobs:
106325
run: env -u PYTHONPATH /tmp/wheels-venv/bin/python -I tools/spec_envelope_digests.py be uk
107326
- name: Run the suite against the installed wheels
108327
run: |
109-
env -u PYTHONPATH /tmp/wheels-venv/bin/python -I -m pytest \
328+
/usr/bin/time -v env -u PYTHONPATH /tmp/wheels-venv/bin/python -I -m pytest \
110329
packages/*/tests -p no:cacheprovider
330+
331+
ci-ok:
332+
# The single aggregate context for branch protection. Per-leg matrix names
333+
# cannot be required: `jobs.<job_id>.if` is evaluated before the matrix
334+
# expands, so a skipped lane reports one bare `engine-us` check and the
335+
# per-leg contexts are never created — a required `engine-us (3.13, us-p)`
336+
# would wait forever on a UK-only PR. This job also closes the seam where a
337+
# dependency-skip lets a failed `changes` take the engine lanes down without
338+
# any required check going red: a lane skip is accepted only when
339+
# classification itself succeeded.
340+
needs: [changes, lint, fast, engine-shared, engine-us, engine-uk, wheels]
341+
if: always()
342+
runs-on: ubuntu-latest
343+
steps:
344+
- name: Require every lane to have passed (or legitimately skipped)
345+
run: |
346+
set -euo pipefail
347+
fail=0
348+
require_success() {
349+
if [ "$2" != "success" ]; then echo "::error::$1 = $2 (expected success)"; fail=1; fi
350+
}
351+
require_success changes "${{ needs.changes.result }}"
352+
require_success lint "${{ needs.lint.result }}"
353+
require_success fast "${{ needs.fast.result }}"
354+
require_success engine-shared "${{ needs['engine-shared'].result }}"
355+
require_success wheels "${{ needs.wheels.result }}"
356+
for lane in "engine-us:${{ needs['engine-us'].result }}" "engine-uk:${{ needs['engine-uk'].result }}"; do
357+
name="${lane%%:*}"; result="${lane#*:}"
358+
case "$result" in
359+
success|skipped) echo "$name = $result" ;;
360+
*) echo "::error::$name = $result (expected success or skipped)"; fail=1 ;;
361+
esac
362+
done
363+
exit "$fail"

‎CLAUDE.md‎

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,55 @@ the PEP 420 namespace `microcosm.<x>`: `frame`, `fit`, `calibrate`, `build`,
1616

1717
```bash
1818
uv sync --all-packages # set up the whole workspace
19-
uv sync --all-packages --extra us # include live US ownership/consumer guards
19+
uv sync --all-packages --locked --extra us --extra uk # CI engine env
2020
uv run pytest # behavioral contract suite + unit tests (all shards)
2121
uv run ruff check . # lint
2222
```
2323

24-
PR CI (`.github/workflows/test.yml`) uses the US-extra sync so pool-input
25-
ownership and consumer existence are checked against the locked PolicyEngine-US,
26-
then runs the test and lint commands plus a wheel-packaging gate (build every
27-
shard's wheel, install into a clean venv under the lock's constraints, import
28-
and test). CI runs the test suite one pytest process per shard (a single
29-
suite-wide process OOM-kills the 7 GB runner); locally `uv run pytest` is
30-
still fine. Spec identities (`spec_sha256` pins, seed digests) attest kernel
31-
source and locked RNG-library versions, so they legitimately move when main
32-
changes an attested module or dependency — CI tests the merge ref, so merge
33-
main and re-pin rather than hunting for an environment leak. Editable installs
34-
hide packaging breaks — if you touch packaging, build wheels locally before
35-
pushing.
24+
PR CI (`.github/workflows/test.yml`) has four lanes — `lint`, `fast`,
25+
`engine` (three jobs), and `wheels` — fed by a `changes` job that classifies
26+
the diff into `shared`/`us`/`uk`. `lint` verifies
27+
`tools/ci_test_groups.py --verify`, syncs with `--locked`, and runs ruff.
28+
`fast` runs the full tracked test-file inventory without engine extras in
29+
three groups (`trade`, `spine-uk`, `rest`); engine-gated tests skip there
30+
through whichever guard they carry — the `requires_*` markers, or the
31+
`importorskip` calls that remain the norm on the US side. `engine-shared` always syncs
32+
`--extra us --extra uk` and runs the shared/spec group. `engine-us` and
33+
`engine-uk` use statically named matrix jobs and job-level `if` conditions
34+
based only on the `changes` job outputs: country jobs run on main pushes or
35+
when that country or shared paths changed. A country PR that merges over a
36+
fresh change to the other country is certified by main's push run; watch main
37+
after merging. The `wheels` lane remains the packaging gate: build every
38+
shard's real wheel, install into a clean uv-export-constrained venv, assert
39+
the wheel/import boundary and spec digests, and run the suite against installed
40+
wheels.
41+
42+
`requires_us` and `requires_uk` are registered pytest markers. Mark new tests
43+
that need a live PolicyEngine engine with the appropriate marker; the root
44+
collection hook skips them when that engine is absent, and the marker also
45+
makes `-m requires_uk` a real selector. Do not add new module-local skip
46+
aliases. Existing `importorskip` guards (still the norm across the US files)
47+
keep working and were deliberately left in place — convert one only when you
48+
are already editing that test for another reason.
49+
50+
**Adding a test file.** It must sit directly in `packages/<shard>/tests/` — flat,
51+
no subdirectories; `fixtures/` and `golden/` hold data only — and be named
52+
`test_*.py`. The lanes run explicit file lists built from a flat pathspec, while
53+
local `uv run pytest` and the wheels lane discover recursively, so a test parked
54+
next to its fixtures would run locally and stay green in CI without ever
55+
executing against an engine. `--verify` fails on such a file rather than letting
56+
it hide. Build tests that exercise a country engine must be named `test_us_*` or
57+
`test_uk_*` so they land in that country's lane; an engine-dependent file named
58+
anything else falls into the always-on `shared-spec` group and runs on every PR.
59+
After adding one, check `tools/ci_test_groups.py --verify`: your file should
60+
appear in the group you expect and never under `[defaulted]`. `tools/ci_test_groups.py` is the partition
61+
authority for CI file groups; update it and keep `--verify` green whenever
62+
test files move or new grouped lanes are added. Spec identities
63+
(`spec_sha256` pins, seed digests) attest kernel source and locked
64+
RNG-library versions, so they legitimately move when main changes an attested
65+
module or dependency. CI tests the merge ref, so merge main and re-pin rather
66+
than hunting for an environment leak. Editable installs hide packaging breaks;
67+
if you touch packaging, build wheels locally before pushing.
3668

3769
## The PR-CI / certification boundary
3870

‎DESIGN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ decomposition:
301301
2. **Conditional structure** — a fitted model carrying P(y|x) from data the
302302
contributor holds (`fit` artifacts). Private sources contribute *only*
303303
this way: certified conditional models, never microdata.
304-
3. **Facts** — targets with standard errors (`calibrate`; Ledger's lane).
304+
3. **Facts** — targets with standard errors (`calibrate`; Chronicle's lane).
305305
Calibration is uncertainty-weighted evidence combination, not exact-hit.
306306

307307
**The merge operator is the sound comparison, institutionalized:** a

0 commit comments

Comments
 (0)