Skip to content

E-G fix round: feed-free label tests, the stand-in rename and R2C-26 - #1486

Closed
MaxGhenis wants to merge 89 commits into
nzhub/g8r2c-g-toyfrom
nzhub/g8r2c-g-fix1
Closed

MaxGhenis wants to merge 89 commits into
nzhub/g8r2c-g-toyfrom
nzhub/g8r2c-g-fix1

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

G8-r2c (#1183): the hub's two fix rounds on G. Round 1 answers the E-G review (comment 6599546057, REQUEST_CHANGES at G 64ef20af). Round 2 fixes the failures that CI found when it first ran on a merge of main and the stack, in draft probes #1487 (this branch) and #1489 (this branch at 20507ad8 without --maxfail=1; it never merges). Draft, with the toy branch as its base (#1483, nzhub/g8r2c-g-toy at 85d55af). Rulings R2C-26 and R2C-27 (#1183 comment 6600262214) cover round 1. Paths: B/ is packages/microcosm-build/src/microcosm/build/, T/ is B/transport/, N/ is B/nz/, and tests sit in packages/microcosm-build/tests/engine_free/shared/.

WIP. Plan: brief 213 reviews the toy commits and both rounds together, and Belgium's hub clears the shared lines below. After that the hub fast-forwards G (#1478) to this head, retargets #1478 to main and lands it on green CI. This PR then closes, since #1478 carries its commits.

Commits

Round 1

Commit Fix owed Change
06760993 3 (F) test_nz_target_references.py: test_each_coarse_row_selects_its_native_rows_committed_labels, the feed-free check of R2C-16's membership
0506c9d6 4 (E) test_nz_transport_declarations.py: test_each_wage_cut_follows_from_its_committed_label, the feed-free check of each wage cut against its label
044440a7 Belgium's nit test_transport_composed_graph.py: test_declared_scenarios_scale_national_cuts_with_their_own_factor becomes test_declared_scenarios_scale_the_standin_cuts_with_their_own_factor
d1d72353 1 Merge main (4a36edcf): eleven PRs the stack lacked, #1472's holdout test among them. Its tree equals git merge-tree --write-tree (52aafc74)
20507ad8 1 test_transport_holdout_separation.py: NZ's expected S5 bindings gain nz_super (R2C-26)

Fix 2 is hub commit 85d55af on #1483. Fix 5 is R2C-27. Fix 6 changes PR bodies only.

Round 2

microcosm's CI runs only on PRs into main, so the stack had none until the hub opened the probes. At 20507ad8, #1487 passed lint, wheels, select-countries, integration-uk and both engine-uk and engine-us legs. Both engine-free legs stopped at their first failure (--maxfail=1), in test_calibration_columns.py, after 10,231 passed. #1489 ran every engine-free test at 20507ad8 without --maxfail=1. Each leg, Python 3.13 and 3.14, failed the same 20 tests, with 24,863 passed, 286 skipped and 3 xfailed. Seventeen sit in test_calibration_columns.py, all from one cause, and f43e8fbb fixes them. The other three, in test_ci_integration_job.py, read the CI workflow itself. They fail only on #1489, whose own commit drops --maxfail=1 and switches off the US, UK and integration jobs.

Commit Failure Change
f43e8fbb test_calibration_columns.py: 17 tests fail. 9 raise Interval outputs need the effective interval document. (B/calibration_columns.py:106-107), 7 expect another refusal and fail "Regex pattern did not match", and 1 Hypothesis test reads refused where it expects prepared. Brief 214 counted the 17 at 891cb9f1, and #1489 lists the same 17 at 20507ad8 _bindings() drops the 12 entries of NZ's packaged bindings that carry interval (G's stand-in band indicators b01 to b12), since these tests pass no interval document; the 257 other entries stay. New test_packaged_interval_outputs_need_the_effective_interval_document pins the refusal on the packaged bindings. With the change the whole file passes (106), so the 12 interval entries cause all 17.

Evidence

All runs used the hub's venv (Python 3.14.7, pytest 8.4.2) with -n 0. Each mutant reproduces the review's.

Mutant Reads the feed Files Result
F: drop the last member label from both b05 rows, expected_member_count −1 no test_nz_target_references.py, test_nz_reference_intervals.py, test_nz_spec_package.py 1 failed (the new fix-3 test), 1237 passed, 42 skipped
E: wage row_107 upper_cent +$1; the builder rewrites earnings_bands.json and its --check agrees no test_nz_transport_declarations.py, test_nz_earnings_bands.py 1 failed (the new fix-4 test), 63 passed, 2 skipped
G7: in VSI, a FILTER boundary nz.v.VSI.review on nz.v.VSI, after the stand-in in the row list; on it, a copy of the stand-in that marks every output rewrite and drops measurement_id; prepare, compile and problem move onto it. tools/build_nz_entitlement_graph.py writes it, and its --check agrees yes test_nz_transport_declarations.py, test_transport_entitlement_graph.py at 85d55af 1 failed, 128 passed. Only test_every_calibration_node_has_its_own_standin_as_the_only_writer fails: it finds nz.v.VSI.review_rewrite beside the stand-in among the writers of person.ird_taxable_income_proxy_cents in nz.v.VSI.calibrate's lineage. Peak RSS 1.69 GB

Without a mutant, at this head, reading the feed: 1388 passed, none skipped or failed, in 391 s at peak RSS 1.97 GB. The run covers test_transport_holdout_separation.py (41), test_nz_transport_declarations.py (62), test_nz_target_references.py (61), test_nz_earnings_bands.py (4), test_nz_reference_intervals.py (16), test_nz_spec_package.py (1203) and the renamed test (1).

Round 2, in the same venv at f43e8fbb, one file at a time: test_calibration_columns.py 106 passed (30 s, peak RSS 1.46 GB). Three more files that read NZ's packaged resources outside G's closed list: test_be_target_references.py 156 passed and 32 skipped (they need BE_CHRONICLE_ARTIFACT), test_spec_source_citations.py 138 passed, and test_spec_engine_reemission.py 7 passed with 1 failure that comes from the hub's venv, not the code: its editable install puts a second microcosm checkout on the namespace path, and worker_identity._module_source_index refuses the differing country_spec.py. CI passed that file at 20507ad8. #1487 reruns the full CI at f43e8fbb.

Shared files

  • Belgium's six. Against main, this head changes the lines Belgium cleared (patch-id 8e90b9bb380ec92a over 3e5f5edb..64ef20af), the same lines in the same order, plus the rename's two. Main's own edits to three of the files moved the context, so the patch-id over 4a36edcf..20507ad8 reads 3025914a344ef3b3.
  • The seventh. test_transport_holdout_separation.py changes one line (R2C-26).
  • The eighth (round 2). test_calibration_columns.py at f43e8fbb: :38 (the loader becomes _packaged_bindings()), :47-60 (_bindings() drops the interval entries) and :101-107 (the new refusal test).

Notes

  • Fix 4's parse. "$a - $b" is the right-closed band (a − 1, b] in dollars. A label whose lower endpoint repeats the previous band's upper one starts at that cut, and the test finds such rows from the labels and pins them to rows 27, 57 and 87.
  • R2C-27's limit. Composition still doesn't recompute the edges. The node's bands_sha256 is the SHA-256 of the whole file, so any edit moves the node key, but it authenticates nothing (T/population_kernels.py:13-15). CI's tests hold the chain from labels to edges.

🤖 Generated with Claude Code

DTrim99 and others added 30 commits September 28, 2026 14:38
The local lane left takes_up_snap_if_eligible and takes_up_tanf_if_eligible
missing on every ACS SPM unit, and the reviewed-null fill gave them the
engine default (universal take-up) in both the materialize pass and the
consumer export.

- New us_runtime.acs_local_take_up fills only missing ACS-row cells:
  receives_snap reporters take SNAP up, non-reporters draw to the
  snap_take_up stage's national FNS rate; TANF uses the take-up contract's
  seeded Bernoulli rate. Draws use take_up._stable_unit_draws.
- The release tool runs it before both fill_reviewed_nulls calls, records
  the seed and an assignment digest in run_identity.json, and the consumer
  export must reproduce that digest; calibrate refuses a pre-#1019
  checkpoint.
- fill_reviewed_nulls refuses NaN in either take-up column.
- New acs_local_take_up_signal gate is a finalize hard failure: completeness
  and non-constancy on both spines, rate band and receipt anchor on the ACS
  spine only (donor cells are graded by the donor release);
  package requires it passing and bound to the packaged bytes.
- Other ACS take-up flags stay a reviewed limitation (#1022); TODO left in
  multispine_pool.seed_multispine_pool_inputs, which default-fills SNAP the
  same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
 review)

Both measures are informational and never graded.

- acs_local_take_up: the TANF program receipt gains receipt_crosstab, a
  weighted 2x2 of transferred receives_tanf by assigned
  takes_up_tanf_if_eligible on ACS units, with the weighted share of
  receives_tanf units that drew False. The TANF draw stays independent of
  receipt; recording the table leaves the assignment digest unchanged.
- Release tool do_qa: the chunked engine probe now reads the SNAP
  variables in the same engine pass as SSI and writes
  snap_take_up_among_eligible to spine_qa.json: take-up and positive-benefit
  rates among is_snap_eligible SPM units, overall, by household type
  (elderly or disabled, with children, childless adults, with earnings;
  overlapping), by spine and type, and by state.
- Tests: hand-checked cross-tab cells and share; a two-chunk do_qa run with
  a stub engine checks every rate; an empty eligible set reports None.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The multispine pool's seed stage (the successor builder) still gives a
missing takes_up_snap_if_eligible cell the engine default, universal
take-up, because its spine-blind take-up contract has no SNAP seed. Its
output is now refused before release.

- h5_io: us_multispine_pool_defaulted_take_up_failures reads the seed
  stage's receipt (stage_receipts.seed.programs.<variable>.defaulted_rows)
  for US_MULTISPINE_POOL_RELEASE_BLOCKING_DEFAULTED_TAKE_UP
  (takes_up_snap_if_eligible). A positive count, a programs block without
  the variable, or a malformed count fails. It reads receipt counts only, so
  it stays spine-blind; a seed receipt with no programs block (not written
  by the seed stage) is not evaluated.
- Both release loaders (load_simulation_ready_us_multispine_pool, used by
  the fiscal refresh release builder, and
  load_authenticated_us_multispine_pool_for_release, used by --base-h5 and
  release preflight) refuse such a pool. The scoring-only loader still
  loads it as evidence.
- The multispine_pool TODO(microcosm#1019) now points at the gate; the
  changelog fragment says so.
- Tests: both release loaders refuse a pool with 3 defaulted cells and
  load it at 0; the scoring loader keeps it; the receipt reader's
  missing, malformed and negative cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
 review)

This reverts commit f8da490. Refusing pool releases with engine-defaulted
SNAP take-up would block the national multispine pool's release loaders,
including the fiscal refresh release builder, for every pool with ACS rows.
That is a decision for the pool lane's owners, not this local-lane PR. The
ACS local lane is already gated: its take-up stage seeds SNAP on ACS rows
and finalize/package refuse a release without a passing take-up receipt.
The pool lane's missing SNAP seed stays tracked by the TODO(microcosm#1019)
in multispine_pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1051 had fallen 755 commits behind main and conflicted with it. Two
files conflicted; both sides are kept.

tools/build_us_acs_local_release.py (7 blocks):
- Module docstring: main's sparse state_cd materialize/calibrate text,
  plus the #1019 take-up seeding step.
- Imports: both the acs_local_take_up and the microcosm.data.contract
  imports.
- Helpers: #1019's _with_local_take_up/_recorded_take_up and main's
  CD-holdout and pro-rata population helpers.
- do_materialize: take-up is seeded on the full staging frame, then
  main's development rung is drawn, so a rung keeps the flags the full
  build assigns.
- run_identity.json: main's soi_mode, cd_holdout and sampling keys and
  the #1019 acs_local_take_up receipt.
- do_calibrate: main's rewritten calibrate; the #1019 pre-solve refusal
  of a checkpoint without a take-up receipt is re-added to it.
- Consumer export: re-derive and verify the take-up assignment, then
  re-draw main's development rung (the same order as materialize).

tests/engine_free/us/test_us_acs_local_release_tool.py (1 block):
- Main's new sparse-era finalize/package test, and the stack's
  _package_args_with_hours with its take_up_state parameter.

Semantic fix: main's new do_calibrate penalty-settings test writes a run
identity without the #1019 take-up receipt, which the stack refuses
before solving. The test now records one, and also pins that a
pre-#1019 identity is refused before any solve.

Main replaced tools/ci_test_groups.py with tools/ci_test_plan.py; the
stack's tests verify under `ci_test_plan.py verify`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on rate (#1051 review)

The QA table counted one row per December-eligible SPM unit at its
household weight and called the result the quantity FNS participation
rates measure. FNS rates are among eligible people over a federal
fiscal year, so it was not that.

- Each cell now reports a person-weighted rate beside the unit rate:
  eligible persons who take up over eligible persons, each unit
  weighted by its household weight times its member count, as the frame
  weights people. The unit fields are renamed unit_* and the person
  fields are person_*, with eligible_persons alongside eligible_units.
- spine_qa.json's key is now snap_december_eligible_snapshot, labelled
  "December-eligible snapshot (unit- and person-weighted)", with a
  not_fns_participation_rate statement. The definition says the
  receiving rate counts a positive annual benefit, a different window
  from the December eligibility.
- The module and do_qa docstrings, the QA note, the
  acs_take_up_engine_defaults limitation and the changelog fragment say
  the same thing. The table stays informational and is never graded.
- Tests: the existing QA test reads the new keys (person rates equal
  unit rates for one-person units), and a new test runs the review's
  example through do_qa: an eligible one-person unit that does not take
  up and an eligible four-person unit that does, at equal weights, give
  a unit rate of 0.5 and a person rate of 0.8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 38640801867 failed test_resume_refuses_weights_without_the_run_identity_stamp
in main's test_us_acs_local_sparse_cd.py. That test drives do_calibrate on a
stubbed checkpoint whose run identity has no ACS take-up receipt, so the
stack's pre-solve take-up refusal fired before the run-identity stamp check
the test targets. test_us_acs_local_target_loss_weights.py, which CI never
reached after --maxfail=1, failed five tests for the same reason.

The refusal order stays as it is: a checkpoint that predates #1019 must be
re-materialized, which also invalidates any weights, so its refusal should
come before the weights-stamp refusals. The fixtures change instead.

- New _record_current_materialization(module, args, identity) in the
  release-tool tests records what a current materialize stage leaves (here
  the take-up receipt in run_identity.json). The stack's later layers
  extend this one helper as they add staging or identity requirements.
- The sparse-CD resume test and the target-loss-weight calibrate_run
  fixture load it, the way the sparse-CD tests already load the
  batched-materialization fixtures, so each still reaches the refusal it
  tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Main moved 47 commits since the last merge (1cfbf7d): mostly UK data
ports, the high-value homes build, NZ spec citations, the emitter's PyPI
release (microcosm-emitter 0.2.1) and the Modal ACS multispine staging
tool (#1267). No file overlaps this branch, and the merge had no
conflicts.

Checked after the merge: ruff check, tools/ci_test_plan.py verify, the
release-tool test set (every test file that loads or drives
tools/build_us_acs_local_release.py) and main's new
test_us_modal_stage_acs_multispine_staging.py.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in 364 commits of main, including #1264 (microcosm#1263: the ACS
SPM independence role, the package-stage SPM measurement gate, the interim
drop of group-quarters children, the inherited Social Security erasure
limitation and the live stored inputs) and microcosm#1335 (one label per
US geography).

tools/build_us_acs_local_release.py conflicted in three places:
- After UnregisteredNullError both sides added a refusal: this branch's
  DeniedDefaultFillError and NEVER_DEFAULT_FILLED (take-up draws,
  microcosm#1019), main's DeclaredSourceInputNullError and its remedy.
  Both are kept.
- fill_reviewed_nulls: the docstring names both refusals. The take-up
  refusal still runs first; main moved the CountryTaxBenefitSystem import
  under `if system is None`, so the branch's top-level import is dropped.
- consumer_export.json records both "acs_local_take_up" and main's
  "live_stored_inputs".

No other change was needed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The adapter should start the separately installed microcosm-emitter
command through the interface's EmitterClient, with the telemetry, Orrery
and Hugging Face modules, and keep the build running when the command is
missing. These tests fail against the adapter that runs the pinned
emitter in-process.

Adds an in-memory EmitterClient for build tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
microcosm-build now depends on microcosm-emitter-interface (>=0.7.0,<1)
instead of the exact microcosm-emitter==0.2.1 pin. The build adapter
starts the separately installed microcosm-emitter command as the build's
child process through the interface's EmitterClient, with the telemetry,
Orrery and Hugging Face modules. The child inherits the build's
environment, so it finds the same Hugging Face credential; its settings
go on standard input. Telemetry requests keep their fields. A missing or
failing emitter leaves the build running without it.

Graph requests made without a build emitter start a short-lived emitter
that runs only the Orrery module.

uv.lock drops microcosm-emitter, alembic, mako and sqlalchemy and adds
microcosm-emitter-interface 0.7.0; APPROVED_UV_LOCK_SHA256 is re-pinned.
Test support replaces the emitter's in-process lifecycle with the
documented request-to-event rules over an in-memory client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The version 1 and version 2 staging writers should hand their run files
to the build's emitter client instead of uploading them with HfApi: one
hand-over per cycle, the runs index merged by the emitter, read-back done
by the emitter, delivery counts taken from its receipts, and three
hand-overs in a row that the emitter does not accept pausing the run's
hand-overs until its final state. The US and UK builds should pass their
emitter's client to the writers, and the US build should hand over a
failed state before its telemetry outcome shuts the emitter down.

These tests fail against the writers that upload each file themselves.
The in-memory client can share a fake Hub's mapping, so UK tests still
see the run files and the staged dataset in one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The version 1 and version 2 staging writers hand their run files to the
build's emitter client (microcosm-emitter-interface's stage_run_files)
instead of uploading each file with HfApi. The emitter copies the bytes,
commits every pending path of a repository in one commit per cycle with
the build's credential, merges the version 1 runs index itself, and
reads the run back on request.

- staging_storage: RunFileHandOver replaces BestEffortUploadSession. It
  never raises, counts the emitter's receipts, and pauses after three
  hand-overs in a row that the emitter does not accept.
- Version 2 delivery counts are the emitter's as of each document's
  hand-over; read-back is the emitter's comparison. The final state is
  handed over once more after a pause; the emitter retries commits, so
  the writer no longer sleeps between attempts.
- The US build and the UK rowwise and spine builds pass their emitter's
  client to the writers. The US build hands over a failed staging state
  before its telemetry outcome shuts the emitter down.

Contract validation stays in Microcosm. Staged dataset bundles still
upload directly (H2b).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The image should install microcosm-emitter with `uv tool install`,
outside the environment synced from uv.lock, check that the command
runs, and name it in MICROCOSM_EMITTER_COMMAND for the stage tool. This
test fails against the image that has only the synced environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The image installs microcosm-emitter (>=0.7.0) with `uv tool install`
into /usr/local/bin after syncing the build's environment from uv.lock,
and fails to build unless `microcosm-emitter version` runs. The image's
environment names the command in MICROCOSM_EMITTER_COMMAND, which the
stage tool inherits. Each plan commit's image gets the newest emitter
release when it is built; the stage check reports the installed version
and refuses an image whose command does not run, and the runner records
the version in its identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README, the graph publication and UK staging operations docs, the
agent guide and the Modal runbook now say that builds start the
separately installed microcosm-emitter through the interface, how to
install and upgrade it, which settings reach it and how, and what the
staging delivery counts mean now that the emitter commits the run files.
Adds the changelog fragment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The emitter compares the Hub with the bytes it committed, so after the
run's final hand-over was not accepted the Hub can match an older state
and the read-back must still fail. This test fails: read-back passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verify_remote now fails, as READ_BACK_FAILED, when the emitter did not
accept the run's current files, instead of asking the emitter to compare
the Hub with an older committed state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Emitter 0.6.0 is the first release with the Hugging Face module that takes
staging run files and its queue in events.handovers.sqlite3. An older
emitter that accepts protocol 1 (0.4.0 to 0.5.1) starts without that module,
so the build's staging hand-overs would all fail with only a warning each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The adapter reads the started emitter's release from its hello answer
(EmitterClient.emitter_version). Below 0.6.0, the first release with the
Hugging Face module and its events.handovers.sqlite3 queue, it closes that
emitter and prints the installed and required versions with the upgrade
command; the build continues as it does without an emitter. The same check
covers the short-lived Orrery emitter. 0.7.0 added only release publication,
which builds do not use, so the Modal comment that called 0.7.0 the first
release with the Hugging Face module is corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/modal_us_stage._emitter_version() runs the image's emitter path,
/usr/local/bin/microcosm-emitter, so a test that reaches _runner_identity()
or check_stage() ran whatever command a machine has there. The test puts a
stand-in command at that path and checks that it never runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
load_app() now replaces tools/modal_us_stage._emitter_version() with the
recorded answer of the image's emitter, so tests that reach
_runner_identity() or check_stage() never run a command found at
/usr/local/bin/microcosm-emitter on the test machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An emitter's telemetry module records the run's start when the emitter
starts, before it can answer hello (every release from 0.4.0 to 0.7.0
initialises its modules before it opens its socket). An emitter the adapter
starts and then stops after reading its version from hello has already
recorded that start; with a credential, it delivers it to the collector when
it is closed, so the build monitor shows a run that started and never
finished.

The tests name a stand-in command that answers `microcosm-emitter version`
and check that an older release is never started, that a release from 0.6.0
on is started from the same command whose version was read, and that the
short-lived Orrery emitter follows the same rule. Every test in the file now
names a missing command by default, so none finds an emitter on PATH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The adapter read the release from the started emitter's hello answer and
closed an emitter older than 0.6.0. By then that emitter had already
registered the run and recorded its start: its telemetry module does both
when it initialises, before the host opens its socket. Closing it delivered
that start to the collector with no later event, so the build monitor showed
the run as running for six hours (no heartbeat ever arrived) and then as gone
silent, never as failed.

The adapter now runs `microcosm-emitter version` (every release since 0.4.0,
the first with the command), which prints the same description as hello and
starts no module, and starts the same command only from 0.6.0 on. An older
emitter, or one with no usable answer, is never started; the warning says so
and the build continues as it does without the command. A missing command is
still reported by the interface. README, the Orrery publication notes, the
Modal image comment and the changelog fragment say the build does not start
an older emitter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_the_app_never_runs_an_emitter_installed_where_the_image_puts_it calls
_runner_identity(), which also checks for /root/microcosm/uv.lock, the image
clone's lock file. On a GitHub runner /root is unreadable, and Python 3.13's
Path.exists() raises PermissionError there, so engine-free (3.13) failed on
6a69406 and on 9b8e42e with that error (1 failed, 5923 passed). It
passes on macOS, which has no /root, and on Python 3.14, whose Path.exists()
returns False. The test now points IMAGE_REPO_ROOT at an empty folder under
its temporary directory, as the other tests that reach _runner_identity() do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps both sides: the take-up refusal and main's ESI premium refusal in
fill_reviewed_nulls; main's self-employment identity check runs before the
take-up stage at materialize and export; the take-up checkpoint refusal
runs before main's county solver profile at calibrate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The SPI income stage now moves the 2022-23 tape's interest and other
income to HMRC's published 2023-24 Table 3.7 amounts band by band, with
the tape banded by its own total income, before any draw. The OBR's
savings income growth then carries both columns to the frame's year,
the path the Table 3.7 band targets take.

Before, each column moved by its engine index from 2022 to 2024: other
income by GDP per head (x1.09), interest by ONS household interest
(x2.27). SPI's own amounts grew x1.65 and x2.69 in the single year to
2023-24. The frame carried about 70 percent of the savings rows at
design weights, and the solve met them by stretching the few households
that carry them. On the tape, the £1m-and-over band's other income
outweighs its interest, as in the published table; the two indices had
flipped that split.

- uk/hmrc_spi_table_3_7_savings_rows.json vendors the 26 published rows
  from the feed pin, declared in the vendoring register and the country
  package.
- spi_income.rebase_spi_donor_savings scales the prepared tape. A band
  the tape carries none of is left as it is and named in the receipt.
  The forest and the band-donor resample both read the rebased tape.
- _spi_income_uprating_factors grows the two columns from 2023-24 on
  hmrc_uprating.uk_obr_savings_income_level. Both columns now run on the
  projection's GDP per head from the frame's year on
  (SPI_HISTORY_INDEX_DIFFERS_FROM_PROJECTION).
- Tests cover the rebase, the published rows, the OBR factor and the
  stage wiring, and the engine lockstep tests are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first spine built from 667fe53 stopped at uk_stage_uc_take_up.
Single people's caseload ended 0.0568 of DWP's count further from it
than before the redraw, against 0.0523 on PR C's spine and a tolerance
of 0.055.

The rebased SPI savings feed the UC reporter redraw's investment-income
predictor. The few SPI benefit units whose report changes move the take-up
stage's pool merging, which joins adjacent award bands until each pool
rests on enough sources. Single people's £1,000 to £1,100 award band
moved from a reachable pool into one whose DWP target exceeds the
entitled units. The reachable pools still realize DWP's counts within
0.005.

The tolerance goes to 0.06 (María's ruling of 2026-10-30), as PR A took
it from 0.05 to 0.055 for the gilt reshuffle. Pool boundaries that move
with small upstream changes are a follow-up on #1334. The gate's note,
the committed-parameter test and the data contract's mirrored digests
are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The national built from f3cb3dd fitted every SPI Table 3.7 interest
and other-income band but one, the £1m-and-over other-income row
included. It missed the £1m-and-over interest amount row outside the
uk_target_fit band.

By María's ruling of 2026-10-30, the row is held out of the objective as
a measured distortion, as the ten HMRC £1m-and-over cells are, and
measured on every evaluation. The register entry carries no figures for
the band. How the rebase sits in the top bands at design weights is a
follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 18 commits November 2, 2026 14:53
G8-w4b: transport weights audit and its checked gate
G8-w4c: donor support roster, producers and checked gate
G8-w4d: wire the release-evidence producers into the NZ graph and toy
Place each ACS rent draw on its own household's head
G8-r2c B: transport.quantile_map@2 with a fitted grouped Pareto top band
G8-r2c C: measure.combine@1 sums declared signed cent components
* Pin five G8-w4 fixes that no test covered

The G8-w4 review (#1448, comment 6586844221) ran mutants that undo five
fixes, and every test stayed green. These tests turn each of those mutants
red. They change no source file.

C1 codec rule: test_a_rulespec_name_with_another_codec_is_uncovered pins
that composition treats a source as the RuleSpec tree by its codec, so
rulespec_nz with a toy codec comes out uncovered and fails the gate. It
goes red when compose.py also skips any source whose name starts with
rulespec.

E2 with an aggregate: test_an_aggregate_total_is_checked_against_its_own_component
pins that the support kernel checks a household total of 150 in component
a against a's bands, so its three member rows count as outside. It goes
red when the kernel checks aggregate totals against the pooled intervals.

Gate empty-sources line: test_a_forged_derived_row_without_sources_fails
feeds the support gate a forged derived row with sources [] and asserts
its failure line. It goes red when gate_bindings.py drops that line.

S1 person slice: test_relation_columns_on_another_entitys_slice_are_a_gap
pins that the closure partition reads relation columns from the person
slice only; the same columns on the household or family slice give a
gap. It goes red when evidence_contract.py reads them from any slice.

S1 unmapped root: test_a_relation_on_an_unmapped_root_entity_is_refused
pins that engine_root_inputs refuses a relation whose root_entity
entity_names does not map. It goes red when the refusal becomes continue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6KNfb66b2ZhYpeyLD8

* Pin the quantile map's refusal of mixed component codes

transport.quantile_map@1 refuses an aggregate whose members carry two
component codes (population_kernels.py:555-567), but no test pinned it.
Under component bands, that check is the only guard: the kernel hands
frame's quantile_map one entity group per component without the codes, so
frame's own member check can't see them, and each component's map would
rank a split household's partial total as a whole household. The support
kernel's E2 fix judges each member's aggregate total by that member's own
code, so it relies on the same refusal.

test_aggregate_map_refuses_members_with_two_component_codes runs the
household aggregate map with component_column "sex" under bare and under
component bands. The toy donor's households mix sex codes, and both shapes
refuse with the kernel's message. When each member takes its household's
first sex flag, both shapes run.

Mutant: with the kernel's check disabled, the component-band case raises
nothing, and the bare-band case refuses with frame's "Members must repeat
one entity component label." instead, so both mixed cases go red. The two
controls pass. No source file changes, so no kernel hash moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Run both component codes in the mixed-code pin's controls

The review of #1471 found that _one_sex_per_household made every toy
person male, so the "both shapes run" controls declared one code. A mutant
that refuses any frame holding two codes at all
(pd.Series(component).nunique(dropna=False) > 1) passed every test, and that
mutant would refuse every real mixed-sex population.

The helper now gives each household one flag, alternating across
households, and the test asserts the frame holds two codes. So the controls
pin "per household": no household mixes codes, the frame holds both, and
both shapes run.

Ran test_aggregate_map_refuses_members_with_two_component_codes: 4 passed.
Under the over-strict mutant, [False-bare] and [False-component] go red with
the kernel's message. Under the disabled check, [True-bare] and
[True-component] go red as before. No source file changes, so no kernel hash
moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Catch an under-strict mixed-code check with a one-household case

Belgium's hub found a mutant that no test caught: `.all()` for `.any()` at
the mixed-code check in `T/population_kernels.py`. Both toy households mix
codes, so the two agree on every fixture. A check using `.all()` fires only
when every household mixes codes, which a real population almost never does.

The third `mixed` value gives the first household one sex flag and leaves the
other household mixed. The existing case ids stay the same.

The invariant: the kernel refuses when any one household mixes component
codes. Under the `.all()` mutant, `[one_household-bare]` and
`[one_household-component]` fail and the other four cases pass. With no
mutant, the file gives 38 passed, and ruff check and format pass.

The patch comes from Belgium's hub, sha256 25b21404d466e1e1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
G8-r2c A: summed-band money rows and the measurement proof
G8-r2c D: per-band support diagnostics and the measurement_prefit gate
Both tests pass NZ's packaged spec to prepare_transport_build and expect activation's "not activated" refusal. prepare_transport_build checks factor currency on today's UTC date before activation, and NZ's selected factors carry valid_until 2026-11-03. From 2026-11-04 the currency check refuses first, with a different message, so both tests fail on main and on every PR. Each test now pins cli._today to 2026-10-24, inside the factors' validity, as test_cli_main_refuses_the_packaged_spec_and_writes_nothing already does.

With the clock forced to 2026-11-05 for the whole run, both tests failed before this change and pass after it. They also pass on today's clock.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Refuse calibration rows that measure a declared holdout

A holdout_references row tests the calibrated model against a fact it
never saw. Composition accepted a calibration, pre-calibration or mass row
on the same measure, so only Belgium's package test kept a country from
calibrating to its holdout.

validate_transport_activation now refuses any row of another reference
document it knows, mass included, whose measure equals a holdout row's
measure on any entity, and names both rows and both documents. The
pre-calibration stage check keys on the same measure, on any entity.
Specs that declare no holdout_references rows keep row-level holdouts.

The new test module checks the toy reproductions, the pre-calibration,
mass and stage variants, every packaged spec's reference documents, a
Hypothesis property against an independent oracle, and agreement with
Belgium's _separation_violations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Kavn5bQZwSxfXaM75

* Give the toy holdouts measures no calibration row shares

The new holdout separation guard refused two synthetic specs. The NZ toy
inherits the composed toy's holdout on (person, receives_main), and its
receipt strata add pre-calibration rows on receives_main. The reference
selection tests gave every document, holdout included, person_count.

The NZ toy's holdout rows now measure toy_main_eligible, a dedicated
eligibility column no other reference document measures; no NZ toy node
reads holdout_references. The reference selection tests give their
holdout documents a holdout_indicator measure. The NZ toy graph and
reference selection modules pass (83 tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Kavn5bQZwSxfXaM75

* Skip non-text measures on both sides of the guard (B1)

_holdout_separation_gaps tested a non-holdout row's measure for membership
in the holdout dict without checking its type, so a list or dict measure
raised TypeError out of validate_transport_activation and hid every other
activation gap. The non-holdout side now skips a non-text measure with the
same _is_text test the holdout side already applies.

Invariant: a list or dict measure on a calibration, pre-calibration or
holdout row of a spec that declares holdout_references raises no TypeError,
and activation's outcome equals its outcome with the guard patched out.
test_non_text_measure_keeps_activation_outcome pins it; its calibration and
pre-calibration cases go red with TypeError when the line is reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FbNxkHFc5E9GDKFKe

* Import Belgium's test module before any @given runs (R1)

_be_test_module executed Belgium's test module on first use, and the
generated-documents differential first used it inside its own @given.
Belgium's module defines @given tests at import, so Hypothesis refused the
nesting whenever that test ran first: alone with -k, by node id, or under
pytest-xdist's default load distribution. The module now imports Belgium's
module once at module level. The health check stays on and Belgium's file
is unchanged.

Invariant: the differential passes in any order. Checked with -k
generated_documents, by node id, and the whole file under -n 4 with the
default load distribution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FbNxkHFc5E9GDKFKe

* Test the guard's mass, parked-holdout and view branches (R2)

Three branches of the guard had no test, so mutants M5, M6 and M4 survived.

- test_unselected_mass_document_joins_the_guard drops node 0's mass params
  from the composed toy and adds a mass row on the holdout's measure; it
  kills M5, which deletes the unselected-mass branch.
- test_parked_holdout_still_guards makes the holdout pending with its
  exemption; it kills M6, which counts only active holdouts.
- test_profile_selected_views_reach_the_guard gives the toy a scenario with
  knobs so activation selects a view, and patches transport_resource so the
  view carries a row on the holdout's measure; it kills M4, which drops the
  separation_views append for selected views. No real profile can add a
  measure: resolve_reference_profiles rebuilds rows by name and never writes
  measure, so the docstring says the test pins only that views reach the
  guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FbNxkHFc5E9GDKFKe

* Run the stage guard on every packaged country's stages (R3)

Activation refuses every packaged spec today, so no test reached
_precalibration_stages with a country's real stages.
test_packaged_precalibration_stages_pass_the_stage_guard calls it on each
packaged spec: AM, BE, UK and US return no stages and NZ returns
income_tax_schedule_1 and main_benefits_rates with no refusal.
test_nz_stage_output_on_a_holdout_measure_refuses_across_entities points
NZ's family holdout at jobseeker_support_annual_gross, a person stage
output, and expects the refusal; an entity-keyed stage guard lets it
through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FbNxkHFc5E9GDKFKe

* Describe what the packaged activation test shows (R4)

Every packaged spec refuses activation on missing bindings, and activation
joins the guard's gaps into that same refusal. The module docstring and
test_packaged_activation_outcome_is_unchanged now say the refusal holds no
guard line and the guard leaves it unchanged, list the new branch and
non-text tests, and point to the stage-guard test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FbNxkHFc5E9GDKFKe

---------

Co-authored-by: Claude <noreply@anthropic.com>
Add one person-level transform kind, CoresidentAdult, serialized as
{"kind": "coresident_adult", "min_age": <int>}. A person's value is true
when at least one other person row with the same household id has
age >= min_age. A null age never qualifies, and a person with a null
household id shares a household with nobody. It binds no country.

Fix _matched_ids so that a nullable integer id column holding a null no
longer reaches matching as float64, where ids beyond 2**53 round
together. It now returns such a column as Python ints beside pd.NA, and
null-free integer columns as int64, as before. Belgium's hub wrote that
fix; the NZ hub applied it unchanged.

Invariant: for an integer id column, _matched_ids never returns a float
array, so two distinct ids never compare equal and a null id never
matches a non-null id. A null person household id can still find a
household row whose own id is null in the same nullable dtype, as on
main.

Reviews: #1183 comments 6592486915 (the kind) and 6595462928 (the
_matched_ids fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nactive (#1463)

generate_be_target_references.py writes 31 more precalibration rows and
a new legacy_json resource, quantile_surfaces.json. Five active wage rows
count RSZ/ONSS T17 employed workers by known-pay daily-wage class at the
end of 2026-Q1, each summing its 24 age and sex cells. Twenty-six parked
pension rows pin SFPD's group 1 amount bands of January 2025. They sum to
the published 2,449,233 and refuse to compile until shared rank_only
support lands. Each surface's band document matches the bands parameter
of transport.quantile_map@1. No graph reads the surfaces, and Belgium
stays inactive.

The generator refuses an SFPD band that ends below its floor, so floors
rise strictly and every surface band spans at least one cent. B3's
artifact-free checks in the Belgian test file now accept several rosters
and record sets on a base selector, with a declared member count.

GPT-6.1 Sol reviewed ba34140 (Subfleet run
20261102-131234-microcosm1463-review), and an Opus 5.5 agent reviewed
the floor check in 3df4596. 58102db adds the first-band test that
review asked for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The E-G review's fix 3. Only the Chronicle feed, which CI doesn't load,
checked R2C-16's membership, so a coarse row that lost a member label
passed every feed-free test. The new test reads the committed
income_diagnostic_references.json: each coarse row's labels equal, in
order, the labels of the native rows its metadata.native_rows names, its
selector otherwise equals theirs, and each measure's rows tile 50 to 244
once.

Without the feed, the review's b05 member drop turns this test red and no
other in test_nz_target_references.py, test_nz_reference_intervals.py or
test_nz_spec_package.py (1 failed, 1237 passed, 42 skipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The E-G review's fix 4. Only the feed checked the wage table's cents
against their labels, so a $1 change to one upper_cent passed every
feed-free test, even after the builder rewrote earnings_bands.json to
match. The new test reads each band's committed label as the right-closed
band (a - 1, b] dollars, so the cut below it is 100(a - 1) cents and its
upper_cent is 100b. A label whose lower endpoint repeats the previous
band's upper one starts at that cut, and the test pins those rows to 27,
57 and 87.

Without the feed, the review's row_107 mutant turns this test red and no
other in test_nz_transport_declarations.py or test_nz_earnings_bands.py
(1 failed, 63 passed, 2 skipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Belgium's hub asked for this rename when it cleared E to G's shared test
lines: since F, the test checks the coarse stand-in table, not the
national one. Only the name changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stack last met main at D's head (91bf4c0). Since then main has landed
eleven PRs whose content the stack lacks: #1432, #1442, #1410, #1428,
#1051, #1444, #1471, #1479, #1472, #1477 and #1463. #1472 brings the
holdout test whose pinned NZ stage set the next commit extends (R2C-26).
git merge-tree resolves the merge without conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
G's nz.precal.nz_super (R2C-2) adds the nz_super binding to NZ's
precalibration stages. At main + G, this test failed on its pinned set
alone, and the stage guard itself accepts the stage:

  test_packaged_precalibration_stages_pass_the_stage_guard[nz]

Hub ruling R2C-26 lifts the freeze for this one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
G's 714e10d adds twelve stand-in income band outputs to NZ's
calibration_bindings.json, each an "interval" entry. The tests in
test_calibration_columns.py call prepare_calibration_columns on that
file without the effective interval document, so the check at
packages/microcosm-build/src/microcosm/build/calibration_columns.py:106-107
refuses them with "Interval outputs need the effective interval
document." Brief 214 counted 17 such failures at 891cb9f. The prepare
kernel passes the document
(packages/microcosm-build/src/microcosm/build/transport/target_kernels.py:679);
these tests call the function directly.

_bindings() now drops the interval entries, and a new test pins the
refusal: NZ's packaged bindings declare interval outputs, and
prepare_calibration_columns refuses them without the document. The
file's interval tests still cover interval outputs on their own
fixture. With the fix the file gives 106 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Closing as included: #1478 landed this PR's commits on main in merge commit f8f543a4, at G's reviewed head f43e8fb. The NZ hub reviewed the stack as one change (review 213 v4, #1183 comment 6615417145).

🤖 Generated with Claude Code

@MaxGhenis MaxGhenis closed this Nov 4, 2026
@MaxGhenis
MaxGhenis deleted the nzhub/g8r2c-g-fix1 branch November 4, 2026 00:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants