Skip to content

Declare the NZ transport and entitlement graphs - #1440

Merged
MaxGhenis merged 20 commits into
mainfrom
nzhub/g8w3b-graph
Nov 2, 2026
Merged

MaxGhenis merged 20 commits into
mainfrom
nzhub/g8w3b-graph

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Status

The build finished at daeafb1c. The hub then added ruling H29 in 182cd234, merged main at 6edab2ac, refreshed the golden in 3e2181c0, took Belgium's S1 at 2c2b467f, made _check_export_contract read the declared units at a873b7b5 (Belgium's fix for a create@2 spec), cleared two roster failures at fe33541b, widened the scan's path pattern at bac6dc22 and merged main at e1d27077.

Correction. An earlier version of this body said CI at 6edab2ac passed 7,731 engine-free tests, "H29's among them". That run showed far less. tools/run_engine_free_tests.sh passes --maxfail=1, so it stopped at its first failure, the golden comparison, and the files after that never ran. Belgium's run found two more failures past that stop, and the hub fixed them at fe33541b.

CI at fe33541b then ran the whole list. Python 3.13 and 3.14 each gave 24,143 passed, 250 skipped and 3 xfailed, none failed, and the other eight checks passed. These logs print progress dots, not node ids, so they can't name a test. Both H29 tests sit in files on the job's list and carry no skip mark.

Results at e1d27077:

  • Merge: the stripped hunks match in both directions, and no file changed on both sides.
  • Kernels: across all 38, only calibration.prepare@1 and takeup.assign@1 move from main, and geography.person_location@1 joins. The three simulate.* kernels stay unchanged. Belgium reproduced this.
  • Money walk: all six NZ money nodes validate at run entry, and VHF selects no factor.
  • Goldens and generators: am, be and nz stay byte-identical. All three builders' --check and both generators' --check pass, the generators against the hub's local artifact.
  • Artifact-gated files, which CI skips: the eight files gave 219 passed and 11 skipped on Python 3.14. The 11 replay atomic_geography_support.json from pinned sources, and this PR changes neither that file nor its builder.
  • Belgium's OK: given at e1d27077 on main 3100f311, with its five Belgian dumps byte-identical to main. It also ran the whole engine-free list with no stop at a873b7b5, on 3.14, and found no real failure.

Independent review: a cloud Opus 5.5 session reviewed e1d27077 on main 3100f311 and returned APPROVE_WITH_NITS, with no P0, P1 or P2 finding and nothing for Max. Its own hash run shows the same moves. It ran the whole engine-free list on Python 3.13 with no stop: 24,145 passed, 249 skipped, 3 xfailed and 1 failed. That failure, test_unreadable_staging_subtree_aborts_before_any_marker, needs a non-root user. The container runs as root, and the test fails the same way on main.

The hub checked each finding against the code:

  • Cents at an exact tie (P3): this reproduces, and G8-x takes it, below. The node does what H28 ruled.
  • Childless-family test docstring (P3), the placement sentence in docs/transport-entitlement.md, and no mixed-couple test through S5: all confirmed. A follow-up PR fixes all three.
  • Empty region code: needs no change. geography.derive@1 can't write "", because _bind lets a layer read only code columns and decode_atomic_support refuses empty codes.

GPT-6.1 Sol's queued review never started behind the disk brake, and it pinned an older head, so the hub cancels it.

Still open: CI at e1d27077, where the two engine-free legs are running and every other check passed.

The build's hand-off report gives file and line for every ruling, the kernel hash table, the mutant table and its test results. It lists H24's placement as open, because that session never saw H29.

What changes

This is G8-w3b, the NZ graph declarations:

  • transport_graph.json and entitlement_graph.json, generated by tools/build_nz_transport_graph.py and tools/build_nz_entitlement_graph.py.
  • Reg 18 cutouts on simulate.solve_zero@1 and the reg 17 base rate as one counterfactual (H23, H24).
  • nz.as.bridge, a FILTER branch of nz.as that carries the shared bridge, because a node on nz.as would re-key nz.terminal (H29). S0, S1, S2, S3 and S5 branch from it.
  • An entitlement gate on the engine's judgment, and MC11's exclude-and-count with S5's reg 17 base rate (H25, H26). accommodation_supplement_entitled_weekly_payment is the new measure.
  • geography.person_location@1, in its own module, so graph_atomic_geography.py stays byte-identical (H27, H27a).
  • Whole-cent nets into the benefit gross-up (H28).
  • Independent branches for V1 to V4 and for VHF, the hold-fixed variant, with supporting resources for inputs, concept mapping, comparators, annualisation, export replay and geography.
  • A synthetic NZ toy helper and end-to-end tests. The toy now runs the shared bridge on the same FILTER branch as the package.

Shared code

  • transport/compose.py: the SKELETON_KERNELS line, the export_contract document field, _check_export_contract and its two calls.
  • calibration_predicates.py: gt and nonnegative_number.
  • transport/registry.py: one import and one call.
  • New transport/person_location_kernels.py.
  • test_spec_only_country_packages.py: the entrypoint scan admits module only at a concept_mapping.json binding and only for a country's RuleSpec YAML path. ConceptMapping.from_dict reads that field and the transport kernels select bindings by it, so a rename would break the format.

The build reports these hash moves: calibration.prepare@1 and takeup.assign@1 move, and geography.person_location@1 joins.

Invariants

  • Bridge FILTER. nz.as.bridge holds the same persons, units and weights as nz.as, and nz.terminal keeps the skeleton's own predecessors, with no bridge node in its ancestry.
  • Cent-exact nets. Both half-cent boundaries next to the exact gross-up root stay clear of the solve margin for every cent-exact weekly net.
  • Gross-up solves. For every four-decimal net, the cents node and the solve return the half-even cent of the exact inverse of 52 times the rounded net.
  • Person location. Every person's region equals their own household's value. Row order and count hold, and a missing value or an unmatched household refuses.
  • Sole-parent cutout. The solved cutout stays within 1e-9 of L + G/s wherever the cutout falls below 4999.
  • Other-unit cutout. Wherever p(501) > 0, the solved cutout equals 500 + p(500)/(p(500) − p(501)).
  • Base rate. It equals the zero-income Jobseeker net times 1 (single) or 2 (partnered), plus credit/52 only with a dependent child.
  • Generated resources. Each packaged resource equals its builder's output. A test runs the three build_nz_*.py --check without the artifact, so the packaged concept mapping stays equal to the adapter's nz.json.
  • Entitlement gate. The entitled payment turns positive only where the payment is positive, the judgment is 1 and, outside S5, the unit isn't superannuitant.

For G8-x, with the real engine

  • family_tax_credits has 21 root inputs. The bridge sets two, and none sit in axiom_input_closure.json. The adapter omits inputs the table doesn't carry and the engine errors on required ones, so the first real run must show the module evaluates.
  • youngest_dependent_child_age reaches the engine as float64 under pandas 3.0.3. The real engine must accept that column for an integer input.
  • money.minor_units@1 rounds each net's shortest decimal, which can't undo error the engine made first. In float64, 528.32 - 0.7*0.05 gives 528.2850000000001, and the node writes 52829 cents; half-even on the exact 528.285 gives 52828. That costs one cent a week, 0.52 a year in the gross-up, and only if the pinned engine computes nets in binary floating point. The first real run must show which arithmetic the engine uses.

🤖 Generated with Claude Code

MaxGhenis and others added 4 commits November 1, 2026 11:03
Declare June 2025 ERP mass, the version 2 receipt contract, pre-candidate solver settings and generated reference exemptions. Activate supported MSD receipt targets and distinct national recipient and historical Accommodation Assistance comparators. Remove the rent-stock scenario, bind gross paid-benefit reporting and label optional export receipt flags as assigned state.

Check active-or-exempt partitioning and exact exemption statuses exhaustively in test_nz_transport_declarations. Check required-reference protection, active receipt targets and bound filters in the same module. Prove disjoint age intervals from their declared bounds and enumerate ages across the working-age boundary. Compare every scenario and gate list, require unsupported VP and verify the separate count and dollar measures. Exercise generator output through --check and retain every inactive-row refusal when reference_selection is absent.

Replay the pre-activation reconciliation from pinned aggregate facts and published rates with exact rational arithmetic. Pin recipient selectors through canonical aggregate_fact_key fields and check their shape exhaustively so the existing spec-only scanner recognizes identifiers. Record headline-rate envelopes and exposure sensitivities while leaving category-weighted after-abatement totals unavailable until the hub obtains joint categories, paid amounts and exposure evidence. Preserve engine-pin, graph, export-enforcement and release-evidence handoffs.

Regenerate the country golden with the repository summary generator and restrict its diff to the fingerprint, hashes and five added resources. Kill all ten named mutants with their declared invariant tests against fresh scratch copies, and prove each copied import through compose.__file__. Preserve the holdout leakage property while correcting its synthetic ratio selector to retain wildcard periods; add deterministic cases for the newly pinned holdouts. Record each validation file's passing counts, failures and outstanding checks in the handoff report.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The small-frame matrix test still expected 234 active rows and built a
frame with no receipt flags and no adult aged 18 to 24, so CI failed on
the 12 receipt cells and NZ Super. Give that frame one adult in the
youngest band and the four flags takeup.assign@1 writes, and expect the
109 rows that stay active.

Pin H19's exact solver settings in the declaration test, because a
changed epoch count passed it before. Correct three descriptions: the
A1 method now names the gross payment columns and H4's Veterans Pension
exclusion, annual-transfer exposure lives in benefit_gross_up.json, and
export.prepare@1 emits a descriptor that materialize_export writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local run of the NZ test files, with no failure cap, found two tests
CI hadn't reached. The IRD rows' parking reason now names G8-r2c, and
removing S4 leaves nine scenarios, so the cell flags number 43 times 9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generate transport_graph.json, entitlement_graph.json and their
supporting resources, including independent variant branches and a
hold-fixed sibling money declaration. Add an opt-in closed roster check
on export and terminal gate slices, and admit gt and finite nonnegative
numbers in calibration predicates.

Work in progress: the build lane stopped before its final validation,
and packaged composition still needs Reg 17/18 assembly and a person
region producer.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis changed the base branch from nzhub/g8w3a-declarations to main November 1, 2026 17:49
G8-w3a landed as a squash, so git saw six files that both sides touched as add/add conflicts. Main's copy of each equals w3a's head b32593d, and main changed no file w3b changed, so w3b's side resolves all six. test_transport_weight_quality.py needed the same by hand: w3b's 43 * 10 returns that pin to its pre-w3a value, so the three-way merge took main's 43 * 9 without a conflict.

git diff main HEAD now equals git diff b32593d 2b44c12 byte for byte.

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

Copy link
Copy Markdown
Contributor Author

Cloud session picked this up at 362f96e; it holds the branch until it posts its hand-off.


Generated by Claude Code

H28: the NZ gross-up now reads whole-cent nets. A money.minor_units@1 node,
nz.benefits.net_cents, rounds each JS, SPS and SLP weekly net half-even to
cents ahead of its solve, and each solve's source term reads the cents column
with coefficient 0.52. The node carries the main_benefits_rates reference, so
composition attributes its outputs to the rates stage, which now lists them as
derived outputs. benefit_gross_up.json discloses the rounding. No kernel
changes.

The rewritten test runs the packaged cents node and then the packaged solve:
rows 801, 802, 901 and 902 all solve and equal the exact half-even inverse of
the rounded net. A companion test shows the unrounded 901/902 tie still
refuses when a solve reads the net directly. A Hypothesis property checks
H13 item 5's bound: for every cent-exact net the half-cent boundaries next to
the exact root keep |F(b)| >= 1/(200q) >= 2.5e-5, 25 times the margin. A
second property solves generated four-decimal nets against the oracle.

H27 as amended by H27a: geography.person_location@1 lives in the new
transport/person_location_kernels.py. It projects a closed canonical map of
household string columns to person string columns through household
membership, keeps person row order and count, and refuses a missing household
value or a membership that names no household. transport/registry.py gains
one import and one call; compose.py gains one SKELETON_KERNELS line;
graph_atomic_geography.py stays byte-identical. A hash script over the
transport registry showed the registry edit moves no hash and adds only
geography.person_location@1. Node nz.geo.person runs on nz.open after
nz.geo.derive and writes person.region_code.

Tests: test_nz_benefit_gross_up.py 16 passed, test_atomic_geography.py 44
passed, test_nz_transport_declarations.py 25 passed and 2 skipped,
test_nz_msd_benefit_rates.py 6 passed.

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

Copy link
Copy Markdown
Contributor Author

M1 is done at 311b814.

  • H28: nz.benefits.net_cents (money.minor_units@1, half-even, 0.01) writes <programme>_net_weekly_payment_cents ahead of each gross-up. Each solve reads the cents at 0.52. The rates stage lists the cents as derived outputs. Rows 801, 802, 901 and 902 now solve against the exact inverse of the rounded net, and a Hypothesis property checks H13 item 5's bound.
  • H27/H27a: geography.person_location@1 lives in transport/person_location_kernels.py, and node nz.geo.person writes person.region_code. The registry edit moves no kernel hash.

Next: M2, the reg 17/18 bridge (H23, H24).


Generated by Claude Code

H23: as_rate_bridge.json now declares reg 18 as method solve_zero. A
simulate.counterfactual@1 node evaluates jobseeker_support_gross_weekly_rate
under the resource's gross_rate overrides and writes it for sole parents
only. A simulate.solve_zero@1 node then solves jobseeker_support_total_income
on [0, 5000] with tolerance 1e-9 and 43 iterations, with three family terms:
the net payment for other units, minus the reduction for sole parents, and
the gross-rate column. jobseeker_support_single reads a derived single,
no-dependent-child column, so sole parents fall to Income Test 3, as reg
18(3)(b) requires. The resource keeps the harness expressions as its
equivalence statement and drops "awaiting D1".

H24: reg 17 is one counterfactual over main_benefits_rates and
family_tax_credits: the zero-income net payment once for a single unit and
twice for a partnered one, plus the prescribed amount over 52 for a unit
with a dependent child. The youngest-child override carries a mask on the
has-child column, so a childless family keeps the closure default 0. One
calibration.prepare@1 node per population writes the two family masks.

Placement: an extension node on nz.as re-keys the skeleton's nz.terminal
FILTER, and compose.py refuses that, so the shared masks, gross rate and
cutout run on nz.as.bridge, a FILTER branch of nz.as that S0, S1, S2, S3
and S5 descend from. Each variant repeats them on its housing population,
and every branch runs its own base rate. The PR carries a hub-ruling
request on this placement.

New test_nz_as_bridge.py runs the packaged declarations through the real
kernels on labelled fixture engines that copy the formula shapes. It checks:
golden-08's base rate and cutout within 1e-9; Hypothesis properties for the
sole-parent cutout L + G/s and the other-unit anchor extrapolation; Income
Test 3 for sole parents; the gross-rate overrides over the unit's own
schedule; the sum rule for the base rate; and the masked youngest-child
override. With pandas 3.0.3 the unmasked override hands the engine NaN,
whose int64 cast raises under numpy's invalid-value check. The packaged
graph now composes with only engine pins and the three money producers
added, and CREATE declares no bridge or region column.

Tests: test_nz_as_bridge.py 14 passed, test_nz_transport_declarations.py
24 passed and 2 skipped. test_nz_axiom_input_closure.py keeps the same three
as_area dtype failures it had at 362f96e.

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

Copy link
Copy Markdown
Contributor Author

Hub ruling needed

H24 places nz.as.bridge.masks, the step 1 gross rate and nz.as.bridge.cutout on population nz.as, through entitlement_graph.json nodes. Composition refuses that. transport/compose.py:3284-3294 (in compose_transport_graph) requires each skeleton node's compiled predecessors to stay unchanged. The skeleton's nz.terminal is a FILTER with base nz.as, so every node on nz.as becomes one of its predecessors. Adding extension nodes there fails with "An extension would re-key skeleton node 'nz.terminal'; branch a new FILTER population instead." Changing that rule would add a shared hunk outside H27a's list.

Interim at 246ae91, easy to undo: the three shared nodes run on nz.as.bridge, a FILTER branch of nz.as (tools/build_nz_entitlement_graph.py, SHARED_BRIDGE). The S0, S1, S2, S3 and S5 boundaries now take base nz.as.bridge instead of nz.as. That keeps one evaluation shared by every entitlement scenario. Variants and per-branch base rates follow H24 unchanged. scenarios.json tiers.entitlement.branch_point stays nz.as. Please confirm this placement or rule otherwise.

M2 is otherwise done at 246ae91: reg 18 runs through simulate.solve_zero@1, reg 17 is one counterfactual, the youngest-child override is masked, and test_nz_as_bridge.py gives 14 passed, golden-08 included. Next: M3 (H25, H26).


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hub ruling H29, on the placement question above. Please apply this at your next convenient point in M3 or M4.

Confirmed: keep the interim at 246ae91. nz.as.bridge, a FILTER branch of nz.as, carries the three shared nodes, and S0, S1, S2, S3 and S5 take base nz.as.bridge. Variants and per-branch base rates follow H24 unchanged. This replaces H24's placement sentence. compose.py's rule stays as it is.

Two additions:

  1. Change scenarios.json tiers.entitlement.branch_point to nz.as.bridge, because the entitlement scenarios now branch there. No shared code reads this field. Update the assertion at test_nz_spec_package.py:2304-2307, and regenerate through whichever generator owns that file. If scenarios.json has no generator, edit it directly.
  2. Add one test: on the toy run, nz.as.bridge holds the same persons, units and weights as nz.as, and nz.terminal's compiled predecessors equal the skeleton's own. So the FILTER drops nothing, and export and terminal gates can't reach a bridge scratch column.

If you've already passed this point, apply it before your hand-off. Everything else in the brief stands.

H25: rulespec-nz's rounded AS payment never reads the module's non-rate
entitlement judgment, so each branch's positive_payment node now holds only
where the payment exceeds zero and the judgment equals 1, the code the Axiom
adapter's decoder gives "holds". Undetermined (0) fails. A new
{prefix}.as.entitled counterfactual reruns the AS engine with no overrides
and writes accommodation_supplement_entitled_weekly_payment, the rounded
payment masked on that indicator. H2, the reporting measure, the gap node's
inputs, cell declarations, bands and gates read the entitled column, while
H1 keeps the indicator. The column joins export_contract
formula_owned_excluded and the formula_owned_export gate's list. The target
reference generator reads it, and holdout_references.json carries the same
four edits by hand; the hub runs both generators' --check.

H26: every branch but S5 adds receipt_group in [beneficiary, other] to the
conjunction (holdout_comparator_bindings central). That one predicate
defines the excluded set, so it equals the superannuitant cell, and
reporting_classifiers.json lists the structural zero for V1 to V4 and VHF as
well. S5 selects the s5 bindings without that clause. Its own masks node,
run after its receipt_group node, crosses single and partnered with
superannuitant, and its base rate takes nz_super_weekly_amount_before_tax
(max, coefficient 1 or 2) for superannuitant units. as_rate_bridge.json
states the before-tax reading and the mixed-couple simplification.

Tests in test_nz_as_bridge.py run the packaged positive-payment, entitled,
receipt-group, mask and base-rate nodes on labelled fixture engines:
failing and undetermined judgments read zero; S1's asset-test knob, through
InputClosure.group_knobs, lifts a unit over the asset limit that S0 zeroes;
a superannuitant unit reads zero in S0 to S3, V1 to V4 and VHF but keeps its
payment in S5; the excluded set equals the superannuitant cell; S5 pays from
NZS with coefficient 2 for a couple. test_nz_spec_package.py also gets
fixes for four failures that predated this session: VHF in the scenario
list, the concept-mapping format id, and the annual-gross classifier
columns.

Tests: test_nz_as_bridge.py 32 passed, test_nz_spec_package.py 1203 passed,
test_nz_transport_declarations.py 24 passed and 2 skipped,
test_nz_target_references.py 16 passed and 38 skipped.

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

Copy link
Copy Markdown
Contributor Author

M3 is done at 0e457e9.

  • H25: positive_payment now requires payment > 0 and the judgment code 1 ("holds"). A new {prefix}.as.entitled counterfactual writes accommodation_supplement_entitled_weekly_payment, which H2, the reporting cells, the gap node and the gates read. H1 keeps the indicator.
  • holdout_references.json carries the four generator edits by hand. The hub still needs to run --check on tools/generate_nz_target_references.py and tools/generate_nz_msd_pre_activation_reconciliation.py.
  • H26: every branch but S5 adds the receipt-group clause, and structural_zero_cells now covers V1 to V4 and VHF. S5 runs its own superannuitant masks and pays superannuitant units an NZS base rate before tax (coefficient 2 for a couple).

Next: M4, packaged composition, the toy run and the golden.


Generated by Claude Code

The packaged spec's activation preflight names exactly the two engine pins
and the three pending money quantile maps, so _GAPS in
test_transport_composed_graph.py stays as it was. A new test shows
_check_export_contract needs no id or membership column in a slice:
compile_graph refuses one with "reads household.household_id, which no
node owns", because the executor projects ids for every sliced entity.

The country-spec golden now carries the settled resource hashes, from the
brief's regeneration script. Every NZ JSON resource already appears in
country_package.json.

Two test fixes cover failures that predated this session. The toy
pending-money test now matches the preflight's actual message ("awaits a
donor that carries it"). The axiom input closure tests now give as_area as
the string codes the state bindings compare.

Tests: test_country_spec.py 143 passed, test_nz_axiom_input_closure.py 17
passed and 1 skipped, test_transport_entitlement_graph.py 67 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FLymaTkqTaw45hgwZ
The test cut the export slice to person.age alone. That dropped the
household and family entities, so the executor projected none of their ids
and the earlier "required columns" refusal fired first. The test now drops
one person roster column and keeps every entity, so only the
exact-slice check can refuse. This failure predated this session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FLymaTkqTaw45hgwZ
Two tests in test_nz_money_normalization.py failed on this branch before
the main merge. They asserted that no graph reads the normalized money and
that the exchange rate lives in one resource. The packaged graph now reads
donor money, so the preflight test asserts the three pending quantile maps
by name. The VHF sibling declaration copies the central rate, so the rate
test lists both locations and checks the two rates are equal.

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

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: daeafb1c (includes the merge of origin/main at dfa5fe39, #1436). The PR stays a draft. One item waits on a hub ruling: H24 placement, in the earlier comment and below.

Milestones and rulings

M1 (311b814)

  • H28. tools/build_nz_benefit_gross_up.py:26-31 (NET_ROUNDING) and :89-117 (node nz.benefits.net_cents, money.minor_units@1, half_even, quantum 0.01) write <programme>_net_weekly_payment_cents for JS, SPS and SLP.
    • :118-127: each solve's source term reads the cents column with coefficient 0.52. Fraction(str(0.52)) == 52/100 exactly.
    • The node carries the main_benefits_rates reference, and the rates stage in precalibration_stages.json lists the three cents columns as derived_outputs. That's how composition attributes stage nodes (compose.py _validate_precalibration_ancestry, the derived/consumed checks).
    • benefit_gross_up.json net_rounding discloses the rounding. No kernel changes.
  • H27 + H27a. New module transport/person_location_kernels.py:1-156 defines geography.person_location@1. implementation_hash is source_hash(type(self), canonical_json, dependencies=("numpy","pandas")).
    • Node nz.geo.person sits at tools/build_nz_transport_graph.py:633-648, on nz.open after nz.geo.derive.
    • graph_atomic_geography.py is byte-identical to main, and register_atomic_geography_kernels is unchanged.

M2 (246ae91)

  • H23. as_rate_bridge.json outputs[1]: method: solve_zero, the reg 18(3)(b) legal basis, each branch's residual, and the harness expressions kept as equivalence. "Awaiting D1" is gone.
    • Generator tools/build_nz_entitlement_graph.py:208-259 (cutout_nodes) writes two nodes. A counterfactual {prefix}.bridge.sole_parent_gross_rate, under the gross_rate overrides and masked to sole parents. Then {prefix}.bridge.cutout: solve_zero@1, solve_input = jobseeker_support_total_income, bracket [0, 5000], tolerance 1e-9, 43 iterations, three family terms with first aggregation.
    • jobseeker_support_single reads accommodation_supplement_bridge_single_without_dependent_child.
  • H24. tools/build_nz_entitlement_graph.py:262-320 (base_rate_node): primary component main_benefits_rates, component family_tax_credits. Terms: JS net ×1 masked is_partnered == false, ×2 masked == true, and the credit ×1/52 masked on accommodation_supplement_bridge_has_dependent_child.
    • The youngest-child override carries that mask.
    • :165-205 (bridge_masks) is one calibration.prepare@1 node per population, with bindings at as_rate_bridge.json mask_bindings.
    • The closure entries' node and reason now read nz.scn.S0.bridge.base_rate and nz.as.bridge.cutout. docs/transport-entitlement.md (the reg 17/18 paragraph) and the generator docstring are updated. bridge_evaluations is removed.
  • Hub ruling needed (placement). An extension node on nz.as re-keys the skeleton FILTER nz.terminal, and compose.py:3284-3294 refuses that. Interim: the shared masks, gross rate and cutout run on nz.as.bridge, a FILTER branch of nz.as (SHARED_BRIDGE, generator :36). S0, S1, S2, S3 and S5 descend from it, so the evaluation stays shared. Each variant repeats the three on nz.v.<V>.housing, and every branch has its own base rate.

M3 (0e457e9)

  • H25. comparator_bindings (generator :552-585): payment gt 0, judgment eq 1 (the decoder's "holds"; adapters/axiom.py:543, −1 not holds, 0 undetermined), and outside S5, receipt_group in [beneficiary, other].
    • New {prefix}.as.entitled (:500-516) writes accommodation_supplement_entitled_weekly_payment.
    • H2's measure, the reporting measure (scenarios.json gap_conventions.reporting_cells.measure, set by the generator), the gap inputs, the bands and the gates read it.
    • The column joins export_contract.json formula_owned_excluded, via the transport generator, and the formula_owned_export gate list.
    • tools/generate_nz_target_references.py changes the H1 notes, indicator_input_column, H2 measure and the H2 notes. holdout_references.json carries the same four edits by hand.
  • H26. holdout_comparator_bindings.json now holds central (with the receipt clause) and s5 (without it). reporting_classifiers.json structural_zero_cells and the reported_cell_support note cover S0 to S3, V1 to V4 and VHF.
    • S5 adds nz.scn.S5.bridge.superannuitant_masks (:323-345), single/partnered crossed with superannuitant and read after receipt_group. Its base rate (as_rate_bridge.json superannuitant_units.s5_base_rate) takes nz_super_weekly_amount_before_tax with max, ×1 or ×2, plus the credit as in H24.
    • The before-tax reading and the mixed-couple simplification are stated there.

M4 (15fa011, cab751b, merge 5e14e25, daeafb1)

  • The packaged graph composes and compiles with only engine pins and three synthetic money producers (test_nz_packaged_graph_compiles_with_only_pins_and_money_producers). CREATE declares no bridge or region column.
  • _check_export_contract needs no id or membership column in a slice: compile_graph refuses one ("reads household.household_id, which no node owns"), shown by test_nz_export_slices_need_no_ids_because_compile_refuses_them.
  • No new resource file was needed; every NZ JSON is already in country_package.json. The golden is regenerated with the brief's script.

Activation gaps at head

axiom_rules_bindings.engine.commit; axiom_rules_bindings.engine.wheel_sha256;
money column household.rent awaits quantile map <- C1 …;
money column person.employment_income awaits quantile map <- P1 …;
money column person.liquid_financial_assets awaits quantile map <- P3 …, C2 …

_GAPS needed no change.

Shared hunks (head versus origin/main)

  • transport/compose.py:
  • calibration_predicates.py: 84-85 (gt), 170-171 and 185-190 (nonnegative_number).
  • transport/registry.py: 50 (import) and 378 (call beside register_atomic_geography_kernels).
  • New transport/person_location_kernels.py: 1-156.

Hash table. Each tree ran in its own process, comparing merge-base dfa5fe39 with head; the registry is built as build_transport_registry builds it. Only calibration.prepare@1 and takeup.assign@1 move, and the one addition is geography.person_location@1.

All 37 kernels
kernel merge-base dfa5fe3 head moved
calibrate.ordered_adam@1 0d4577effa14 0d4577effa14
calibration.prepare@1 e8a2f92b8217 8067f5faf275 yes
concepts.encode@1 bcb84fa73efc bcb84fa73efc
concepts.encode_groups@1 55da7a2fda7d 55da7a2fda7d
diagnostics.calibration@1 bfc9b52d391d bfc9b52d391d
export.prepare@1 cbfb50d207b1 cbfb50d207b1
export.readback@1 09a5b029b928 09a5b029b928
gates.battery@1 582019ff5e83 582019ff5e83
geography.assign_atomic@1 d60f7ec38890 d60f7ec38890
geography.derive@1 d60f7ec38890 d60f7ec38890
geography.gate@1 d60f7ec38890 d60f7ec38890
geography.person_location@1 — 3efcbe3741d0 yes
geography.support_from_facts@1 1a7b43faf985 1a7b43faf985
geography.support_import@1 d60f7ec38890 d60f7ec38890
money.annual_transfers@1 c8a067cd750e c8a067cd750e
money.minor_units@1 c8a067cd750e c8a067cd750e
money.normalize@1 1f7de88104a1 1f7de88104a1
reporting.carry_income_decile@1 80335f0183c2 80335f0183c2
reporting.income_decile@1 80335f0183c2 80335f0183c2
reporting.receipt_group@1 80335f0183c2 80335f0183c2
simulate.counterfactual@1 2fa535e6fd26 2fa535e6fd26
simulate.rules_by_ref@1 04d813bf092b 04d813bf092b
simulate.solve_zero@1 0bf07c613edf 0bf07c613edf
takeup.assign@1 69b37ac54107 004a574efcac yes
takeup.bands@1 1ceca3ba7c7c 1ceca3ba7c7c
takeup.cell_diagnostics@1 1ceca3ba7c7c 1ceca3ba7c7c
takeup.compare@1 ad0dac8a4fbe ad0dac8a4fbe
takeup.gap@1 eda13ae5fadd eda13ae5fadd
targets.compile@1 ed90611bb98a ed90611bb98a
targets.problem@1 c6096e3a5f33 c6096e3a5f33
transport.boundary@1 ccf0801a9bd8 ccf0801a9bd8
transport.create@1 b4833d4b3801 b4833d4b3801
transport.currency@1 5d1815597fd9 5d1815597fd9
transport.package@1 8e685c71c808 8e685c71c808
transport.quantile_map@1 f0a4e85c1487 f0a4e85c1487
transport.scenario_override@1 55da7a2fda7d 55da7a2fda7d
transport.unit_attributes@1 a1ec47db6dc4 a1ec47db6dc4

Tests at head, after the merge

Each file ran on its own, -n 4 --dist loadfile.

file result
test_nz_as_bridge.py (new, registered; ci_test_plan.py verify ok) 32 passed
test_nz_transport_toy_graph.py 19 passed
test_nz_transport_declarations.py 25 passed, 2 skipped
test_nz_benefit_gross_up.py 16 passed
test_atomic_geography.py 44 passed
test_nz_joint_geography.py 21 passed, 12 skipped
test_nz_target_references.py 16 passed, 38 skipped
test_transport_composed_graph.py 462 passed
test_transport_entitlement_graph.py 67 passed
test_nz_spec_package.py 1203 passed
test_country_spec.py 143 passed
also test_nz_axiom_input_closure.py 17 passed, 1 skipped
also test_nz_msd_benefit_rates.py 6 passed
also test_nz_money_normalization.py 56 passed
also test_transport_run_entry_factors.py 67 passed

Skips:

  • test_nz_transport_declarations.py: 2 tests, "Pinned feed not staged".
  • test_nz_joint_geography.py: 11 tests, "Set NZ_AS_AREA_SOURCES and NZ_JOINT_GEOGRAPHY_SOURCES for pinned-source replay"; 1 test, "Set NZ_CHRONICLE_ARTIFACT to replay the pinned published TA ERP facts".
  • test_nz_target_references.py: 38 tests, "NZ_CHRONICLE_ARTIFACT is unset; public NZ feed audit unavailable".
  • test_nz_axiom_input_closure.py: 1 test, "needs axiom_rules_engine with its dense extension and a rulespec-nz checkout".

Also passing: ruff check and ruff format --check on changed files, graph_acceptance_burndown.py --verify, git diff --check, and --check on all three NZ generators. I haven't read CI for daeafb1 yet.

Failures that predated this session, now fixed in the tests only:

  • test_nz_spec_package.py: the scenario list without VHF, the concept-mapping format id, and the annual-gross classifier columns.
  • test_nz_transport_toy_graph.py: the pending-money regex.
  • test_transport_composed_graph.py::test_closed_export_contract_enforces_exact_entity_slices: it now keeps every entity and drops one column.
  • test_nz_axiom_input_closure.py (3 tests): as_area as string codes.
  • test_nz_money_normalization.py (2 tests): they asserted that no graph reads money and that the rate has one copy; the VHF sibling copies the rate.

Mutants

All 22 are red. Each ran on a copy under /tmp/mut/<name> with the copy's packages/*/src and root first on PYTHONPATH. A pytest plugin imported the touched module and asserted that its __file__ lay inside the copy; every log line shows MUTANT_IMPORT … /tmp/mut/<name>/….

mutant red test
recompute_deciles_in_variant test_nz_transport_toy_graph.py::test_variant_decile_recomputation_refuses (DID NOT RAISE)
carry_by_row_order …toy_graph.py::test_income_decile_carry_joins_reordered_units
row_level_ess …toy_graph.py::test_source_household_ess_survives_a_clone_split (2.247 ≠ 1.923)
reporting_alias_measure …toy_graph.py::test_as_reporting_alias_and_count_dollar_comparators_use_declared_measures
variant_normalizes_twice test_nz_transport_declarations.py::test_nz_every_variant_id_reaches_entitlement_rows_with_explicit_populations
hold_fixed_uses_awe …declarations.py::test_nz_hold_fixed_has_identity_families_in_its_own_normalization_scenario
hold_out_upstream_of_calibration …declarations.py::test_nz_s5_ancestry_keeps_as_holds_and_inventory_after_each_calibration
profile_selection_ignores_knob …declarations.py::test_nz_money_profile_selection_equals_each_scenarios_own_knobs
region99_from_ta999 …declarations.py::test_nz_executable_support_keeps_region99_separate_from_ta999
export_slice_includes_scratch_cents test_transport_composed_graph.py::test_closed_export_contract_refuses_scratch_reporting_and_formula_outputs[scratch_cents]
sole_parent_uses_income_test_1 test_nz_as_bridge.py::test_sole_parent_reaches_income_test_3_not_income_test_1 (off by 51.43)
sole_parent_uses_own_schedule test_nz_as_bridge.py::test_sole_parent_cutout_uses_the_gross_rate_overrides_not_the_units_schedule
partnered_coefficient_one test_nz_as_bridge.py::test_base_rate_sums_jobseeker_once_or_twice_and_the_credit_only_with_a_child (unit 101)
credit_without_child_mask same test (unit 100)
youngest_age_unmasked test_nz_as_bridge.py::test_a_childless_family_keeps_the_closure_default_youngest_child_age (INT64_MIN reaches the engine)
payment_not_gated test_nz_as_bridge.py::test_a_unit_failing_the_non_rate_judgment_reads_zero
undetermined_passes test_nz_as_bridge.py::test_an_undetermined_judgment_reads_zero
superannuitant_not_excluded test_nz_as_bridge.py::test_a_superannuitant_unit_reads_zero_outside_s5 (all 9 branches)
s5_keeps_exclusion test_nz_as_bridge.py::test_s5_keeps_a_superannuitant_units_payment
s5_uses_jobseeker_rate test_nz_as_bridge.py::test_s5_pays_a_superannuitant_unit_from_its_nzs_base_rate
gross_up_reads_unrounded_net test_nz_benefit_gross_up.py::test_fractional_cent_nets_solve_against_the_exact_inverse_of_the_rounded_net (tie refuses rows 901, 902)
person_region_from_wrong_household test_atomic_geography.py::test_person_location_gives_every_person_their_own_households_code

Invariants and the tests that run them

  • Cent-exact nets. For every cent-exact weekly net, both half-cent boundaries next to the exact gross-up root keep |F(b)| ≥ 1/(200q) ≥ 2.5e-5 = 25m. Run by test_cent_exact_nets_keep_every_half_cent_boundary_clear_of_the_margin (Hypothesis, 300 examples, exact Fractions).
  • Gross-up solves. For every four-decimal net, the packaged cents node then the packaged solve returns the half-even cent of the exact inverse of 52 × the rounded net. Run by test_any_four_decimal_net_solves_against_its_rounded_exact_inverse and the 801/802/901/902 test.
  • Person location. For every person, the region equals their own household's value; person row order and count hold; a missing household value or an unmatched household refuses, naming the rows. Run by the four test_person_location_* tests.
  • Sole-parent cutout. For every L in [0, 400], s in [0.05, 1] and G in [1, 1500] with a cutout below 4999, the solved cutout is within 1e-9 of L + G/s. Run by test_solved_sole_parent_cutout_equals_lower_plus_rate_over_slope.
  • Other-unit cutout. Wherever p(501) > 0, the solved cutout equals 500 + p(500)/(p(500) − p(501)). Run by test_solved_other_unit_cutout_equals_the_harness_anchor_extrapolation.
  • Golden-08. The base rate and the cutout reproduce differential_expectations within 1e-9. Run by test_fixture_run_reproduces_golden_08_base_rate_and_cutout.
  • Base rate. It equals the zero-income JS net × (1 single | 2 partnered), plus credit/52 only with a dependent child. Run by test_base_rate_sums_….
  • Childless families. Every childless family hands the engine the closure default 0 for the youngest age. Run by test_a_childless_family_keeps_….
  • The entitlement gate. The entitled payment is positive only where the payment is positive, the judgment is 1 and, outside S5, the unit isn't superannuitant. The excluded set equals the superannuitant cell. Run by the H25/H26 tests in test_nz_as_bridge.py.
  • Binding obligations 1 to 6. They keep their named tests, which pass. I changed assertions only where a ruling did: .bridge.base_rate replaces .bridge.evaluate.reg17_1, and the comparator conjunction and the entitled measure follow H25 and H26.

Memory: the two CREATE executions (obligation 7)

transport/cli.py:116-214 (_create_inventory) runs transport.create@1 once as a probe, keeps only the Owned inventory tuple and drops the frame when the function returns. The executor then runs CREATE again. The two runs are sequential, so peak residency is about the larger of one CREATE frame and graph execution, not their sum. The cost is a second full donor read and its CPU time. CPython's allocator may keep the probe's freed arenas, so RSS can sit near one extra CREATE frame. A warm build hashes the sources and skips the probe. I added nothing to the graph.

Judgment calls

  1. The H24 placement on nz.as.bridge, above.
  2. The mask columns, the S5 masks and the S5 base rate live inside as_rate_bridge.json. holdout_comparator_bindings.json nests central/s5.
  3. The youngest-age override path, measured under pandas 3.0.3: Int64.to_numpy() yields float64 with NaN, so even the masked override reaches the engine as a float64 column (0.0 for the childless family). The unmasked NaN casts to INT64_MIN and raises only under numpy's invalid-value check. Brief: the adapter's dtype-kind branch (axiom.py:1419-1428) then takes the float branch. G8-x should check that the real engine accepts a float64 column for this integer input.
  4. indicator_input_column moved to the entitled column.
  5. The entitled column joins the formula-owned exclusion lists; I updated the test that pinned them to the bound outputs.
  6. The family_tax_credits module has no axiom_input_closure entries. The fixture supplies its care units; a real run needs closure coverage for it.
  7. The two test_nz_money_normalization.py assertions follow the graph now reading money.

Not done, for the hub

  • Run tools/generate_nz_target_references.py --check and tools/generate_nz_msd_pre_activation_reconciliation.py --check against the private artifact. I hand-edited holdout_references.json to match the generator.
  • The tests that skip without that artifact.
  • The H24 placement ruling.

Nothing was cut from the mutant list.


Generated by Claude Code

MaxGhenis and others added 8 commits November 1, 2026 16:46
Ruling H29 keeps the three shared bridge nodes on nz.as.bridge, a FILTER
branch of nz.as, because any node on nz.as would re-key nz.terminal.
The cloud build placed them there but never saw the ruling, so its two
additions land here.

scenarios.json's entitlement branch point now names nz.as.bridge. The
synthetic NZ toy takes the packaged shape, so its scenarios descend from
the branch. Two new tests hold the ruling: the FILTER keeps every person,
unit and weight, and nz.terminal keeps the skeleton's own predecessors,
with no bridge node anywhere in its ancestry.

Local pytest waits on free disk, so CI runs these first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The golden pins every resource's hash, so H29's scenarios.json edit moves
that hash and the spec fingerprint. Nothing else in the golden changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/microcosm-build/src/microcosm/build/transport/compose.py
_check_export_contract read benefit_unit_rule directly, so a create@2 spec
with an export contract refused for the missing resource. A stale legacy
rule beside two declared partitions was worse: the check took the legacy
schema and missed the second membership column, which the export node
still receives. Belgium plans a create@2 spec with a contract, found this
by running it and supplied the fix.

The check now takes the schema from the CREATE declaration, with the same
fallback as the relation check, so create@1 behaves as before. New tests
cover one declared unit with no legacy rule, and two partitions with and
without a stale rule, for a silent, allowing and forbidding roster.

No kernel attests compose.py, so no hash moves. CI runs these first: local
pytest waits on free disk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Belgium's run at a873b7b found two failures on this tree that main doesn't
have. CI couldn't show them, because the engine-free run stops at its first
failure.

- A packaged concept mapping spells each binding's RuleSpec file as `module`.
  ConceptMapping.from_dict reads that field and the transport kernels select
  bindings by it, so the entrypoint scan now admits it, only at a
  concept_mapping.json binding and only for a country's RuleSpec YAML path.
  A new test shows every other shape stays refused.
- NZ's closed legacy_json roster gains the eleven resources this PR declares.

No test ran the graph builders' --check, so a later edit to the adapter's
nz.json would have left the packaged mapping behind. The new test needs no
artifact, and a mutant of nz.json on a copy turns the check red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Belgium's check of rulespec-be found 32 of its 116 rule files outside the scan's path pattern: regional trees such as be-vlg/ and one article with a hyphen. Widen the pattern to those two shapes. A segment still can't hold a dot, so .. stays refused.

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

Copy link
Copy Markdown
Contributor Author

Ready for hub review

This report comes from the NZ hub's cloud review of the finished build. It isn't the hub's verdict: the hub reads it and decides.

VERDICT: APPROVE_WITH_NITS
Reviewed head: e1d2707
Needs Max: no

Base 3100f311cefea759d8a3c9fc0776d1de3681cf5f, head e1d270779c80a289ca1563055249c9574d895691. I read the delta with git diff 3100f311 e1d27077 and ran everything from /tmp/w3b/base and /tmp/w3b/head worktrees, each with that tree first on PYTHONPATH and the checkout's venv (Python 3.13.18, pandas 3.0.3, numpy 2.4.6). Line numbers below are at head.

Findings, most severe first

No P0, P1 or P2 finding. Every ruling H23 to H29 holds where I tested it. These are P3 items and nits.

  1. P3, H28: the cents node rounds the float's shortest decimal, so an engine net that carries binary-float error at an exact half cent rounds away from half-even. MoneyMinorUnitsKernel.run (transport/money_kernels.py:461-467) reads each net through _exact_value (frame/exact_money.py:48-72), which takes a float's shortest round-trip decimal, then _half_even. That is exact on the decimal the float prints, as H28 asks, but it can't undo error the engine made before the value arrives. Concrete input: a gross of 528.32 abated by 0.7 over 0.05 of income above the threshold. In float64, 528.32 - 0.7*0.05 is 528.2850000000001, which the node writes as 52829 cents; the exact net 528.285 is a tie whose half-even cent is 52828. I ran both through the kernel's own helpers at head. The outcome is one cent a week (0.52 a year into the gross-up), and only if the pinned engine computes nets in binary floating point. H28's own finding says such lattice ties recur ("cents times 0.7 or 0.35"), so the disclosure in benefit_gross_up.json net_rounding ("rounds each weekly net payment half-even to cents") holds only for nets the engine computes exactly. G8-x can check this with the engine pin it already has to check for m.
  2. P3, test strength, H24: the childless-family tests check a conversion the adapter doesn't make for this column. The masked override returns a float64 column: _evaluate (frame/rules_kernels.py:490-508) maps the nullable Int64 source through to_numpy() (float64 with NaN under pandas 3), then np.where with the existing input column, so the engine input arrives as float64, 0.0 for a childless family and the age as a float elsewhere. _batch_from_table (frame/adapters/axiom.py:1800-1832) then takes the kind == "f" branch and never calls to_numpy(dtype=np.int64). test_a_childless_family_keeps_the_closure_default_youngest_child_age (test_nz_as_bridge.py:730-734) and test_unmasked_youngest_child_override_carries_a_missing_age_to_the_engine (:737-748) both cast with to_numpy(dtype=np.int64) themselves. The first still proves the value 0 arrives, which is what H24 needed. The second's FloatingPointError can't happen in the real adapter: unmasked, the engine receives NaN in a float column instead. This is the known float64-for-integer check the brief defers, so I name it only because the test's docstring claims the adapter raises.
  3. Nit, docs: docs/transport-entitlement.md:180-194 describes the bridge placement H29 replaced. It says that "on each population that runs the Accommodation Supplement engine" a mask node, a gross-rate node and a zero-solve run. S0, S1, S2, S3 and S5 run the AS engine but read the cutout from the shared nz.as.bridge population (tools/build_nz_entitlement_graph.py:590-597), and the doc never names nz.as.bridge. The generator docstring (:4-13) and as_rate_bridge.json outputs[1].placement state it correctly.
  4. Nit, H27: geography.person_location@1 treats an empty string as a value. person_location_kernels.py:134-141 refuses any non-str household value (NA, NaN, numbers) but passes "", so a household with household_region_code == "" gives its persons "" with no refusal. Whether geography.derive@1 can emit "" I didn't establish.
  5. Nit, test coverage, H26: no test runs a mixed couple through S5's base rate. test_a_partnered_superannuitant_unit_takes_coefficient_two_in_s5 (test_nz_as_bridge.py:917-926) builds a couple with both partners 70. The resource's mixed-couple statement (one partner on a main benefit takes the NZS amount through max) follows from _map_values' max aggregation (rules_kernels.py:1198-1213) and the fixture engine returning 0 below 65, but nothing pins it.

1. Reg 18 cutout (H23)

cutout_nodes (tools/build_nz_entitlement_graph.py:208-259) writes {prefix}.bridge.sole_parent_gross_rate (counterfactual, the gross_rate overrides, term masked is_sole_parent == true) and {prefix}.bridge.cutout (simulate.solve_zero@1, solve_input person.jobseeker_support_total_income, bracket [0, 5000], tolerance 1e-9, 43 iterations). The solve's terms are as_rate_bridge.json outputs[1].solve.terms: net payment ×1 masked is_sole_parent == false, reduction ×−1 masked is_sole_parent == true, and the gross column as a source term ×1. Source terms take no mask (rules_kernels.py:370-393 allows only five fields), and the step-1 node already zeroes the column off sole parents, so every row reads exactly one branch.

  • Sole parents reach Income Test 3: the solve overrides jobseeker_support_single with accommodation_supplement_bridge_single_without_dependent_child (false for them), partner_receives_main_benefit with is_partnered (false), and the other two partner flags with false, so the reduction falls to its final else. is_sole_parent is ~is_couple & with_children (frame/unit_construction.py:900) and the mask column is is_single == true and n_dependent_children == 0, so the two never disagree for a one-adult unit.
  • Monotone. Sole parent: G − max(0, x − L)·s is non-increasing. Every other unit: its own net payment under Income Test 3 (single) or 4 (partnered), max(0, G − r(x)), non-increasing with a zero plateau. SimulateSolveZeroKernel returns the first non-positive point (rules_kernels.py:812-843, outputs hi), so the plateau yields the cutout the law means.
  • No root below 5000: the node refuses as a whole, "solve_zero bracket upper endpoint has a positive residual" (:812-823); it never returns a number. test_upper_bracket_refuses_a_positive_residual covers it.
  • first groups person values by membership after a stable sort on person_id (rules_kernels.py:1198-1213), so it reads the unit's lowest person id. Every Jobseeker input the three formulas read is overridden with a unit value or a constant, and the trial broadcasts to every member (:509-518), so all members agree and the member choice can't change the value.
  • Golden-08: 160 + 521.52/0.7 = 905.0286 and 521.52 + 7921/52 = 673.8469 match differential_expectations; test_fixture_run_reproduces_golden_08_base_rate_and_cutout passes.

2. Reg 17 base rate (H24)

base_rate_node (:262-320) builds the terms from outputs[0].terms: Jobseeker net ×1 masked is_partnered == false, ×2 masked is_partnered == true, the credit ×1/52 masked on accommodation_supplement_bridge_has_dependent_child (_coefficient turns "1/52" into a float, :111-112). Primary main_benefits_rates, component family_tax_credits. That matches the ruling.

Youngest-child override. For a childless family: the family column is Int64 NA (unit_construction.py:882-885); _evaluate reads it with to_numpy(), which gives float64 NaN under pandas 3.0.3; the mask keeps the existing person input column on that row; np.where returns float64. The engine input arrives as float64 0.0 (and float64 ages for families with children). I reproduced the dtype with the same calls. See finding 2 for the adapter path.

Family shapes. I found no shape that gets a base rate other than what the ruling and the harness define. The age-25 constant for under-25s stays the open item the resource discloses. A sole parent whose youngest child is 14 or older takes the 14-or-older rate in reg 17 (the unit's own youngest age reaches the base rate), while reg 18(3)(b)'s gross rate pins age 0; that follows the harness on both sides.

3. Entitlement gate (H25, H26)

  • comparator_bindings (:552-580) writes gt 0 on the payment, eq 1 on the judgment, and outside S5 receipt_group in [beneficiary, other]. The adapter's judgment codes arrive as int64 (axiom.py:1914-1942), so the values: [-1, 0, 1] domain matches Python ints from tolist().
  • {prefix}.as.entitled (:500-518) reruns the AS engine with no overrides and masks the rounded payment on the indicator. Positive only where all three clauses hold. test_a_unit_failing_the_non_rate_judgment_reads_zero, test_an_undetermined_judgment_reads_zero, test_a_superannuitant_unit_reads_zero_outside_s5 (nine branches) and test_s5_keeps_a_superannuitant_units_payment pass.
  • Readers. Across scenarios.json and entitlement_graph.json, only three node kinds read the rounded payment: .as (writes it), .positive_payment (the conjunction H25 names) and .as.entitled. The gap nodes, gap_conventions.reporting_cells.measure, H2's measure in holdout_references.json and the generator all read the entitled column. The remaining mentions of the rounded column are exclusion lists (export_contract.json formula_owned_excluded, gates.json formula_owned_columns, the income-decile excluded_columns).
  • S5 mixed couple. receipt_group is superannuitant when any member has receives_nz_super (reporting_classifiers.json receipt_group), so a mixed couple takes accommodation_supplement_bridge_partnered_superannuitant and 2 × max over members of nz_super_weekly_amount_before_tax, with the NZS component run on members' own inputs (input_overrides: []). That is the resource's statement. See finding 5.

4. Person location (H27, H27a)

I read transport/person_location_kernels.py whole. Each person's value is values[positions], where positions comes from household_ids.get_indexer(person_household_id) (:122-142), so it's the person's own household in any row order, and the result Series is keyed by person_id, which the executor validates by id set (graph/executor.py:1134-1157). An unmatched household id refuses (:127-128), a duplicate household id refuses (:123-124), and a missing or non-string household value refuses naming the household rows (:133-141). See finding 4 for "". git diff --quiet 3100f311 e1d27077 -- …/graph_atomic_geography.py exits 0: byte-identical.

5. Cents rounding (H28)

nz.benefits.net_cents (money.minor_units@1, half_even, quantum 0.01) writes the three _cents columns; each gross-up source term reads the cents column with coefficient float(Fraction(52, 100)) = 0.52 (tools/build_nz_benefit_gross_up.py:89-127), which the quantized solve reads as exactly 13/25. test_fractional_cent_nets_solve_against_the_exact_inverse_of_the_rounded_net passes on my control and goes red when the JS solve reads the unrounded net (mutant below). The node carries main_benefits_rates' engine_ref, and precalibration_stages.json lists the three cents columns as derived_outputs of nz.precal.main_benefit_rates, so composition attributes them to the rates stage. On "exact decimal": see finding 1.

6. Placement (H29)

  • nz.as.bridge is transport.boundary@1, FILTER, base nz.as (:590-593); its kernel keeps every person (population_kernels.py:344-357). S0, S1, S2, S3 and S5 boundaries take base nz.as.bridge; each variant runs masks, gross rate and cutout on nz.v.<V>.housing and every branch its own base rate (test_every_as_population_runs_its_own_bridge_and_every_branch_its_base_rate).
  • No export or terminal gate slice in export_contract.json names a mask, the gross rate, the base rate or the cutout, and nz.terminal's ancestry runs through nz.as, never through nz.as.bridge. nz.as.bands and nz.package take scenario results through artifact edges, not columns. Nothing below nz.terminal needs a branch-only column: the packaged graph compiles.
  • tiers.entitlement.branch_point is nz.as.bridge, which matches the composed boundaries. No code in packages/*/src or tools/ reads branch_point.
  • compose.py:3351-3361 already refuses any change to nz.terminal's predecessors, so the new terminal test can fail only if that guard goes; my mutant removing the guard turns it red.

7. Shared code

  • calibration_predicates.py. Newly accepted: operator gt (before: "Unsupported calibration predicate 'gt'") and domain {"kind": "nonnegative_number"} (before: "has an unsupported domain"). One input changes meaning: a domain carrying both "kind": "nonnegative_number" and a values list used to validate against the list and now validates as any finite number ≥ 0 (_input_domain_kind, :182-193). Nothing newly refuses. No resource outside NZ uses either form: a repo-wide grep for "operator": "gt" and "kind": "nonnegative_number" finds only NZ's holdout_comparator_bindings.json, the NZ generator and two NZ tests. So UK, US and Belgian specs see only the hash move.
  • transport/compose.py. _check_export_contract returns at once unless the transport graph document names export_contract, which only nz/transport_graph.json does. "export_contract" in _DOCUMENT_FIELDS only stops refusing that field. The _WEIGHT_COLUMN_SUFFIX import adds no behaviour. SKELETON_KERNELS is an allow-list checked in _node (:3032-3036) and widened in entitlement._KERNELS; nothing iterates it to add nodes, so it can't admit a node a country didn't declare.
  • transport/registry.py. One import, one register_person_location_kernels(kernels) call in build_transport_registry; register_atomic_geography_kernels is untouched, so the UK registry gains nothing.

8. Hash claim

Holds. I built the registry the way build_transport_registry does, from git archive copies under .hub-scratch/base and .hub-scratch/head, each in its own process, with no engine: the three simulate.* kernels were built without engines and an empty adapter tuple, the same way in both trees (this changes their absolute hashes, not whether they move). Each process printed its own tree's microcosm/build/__init__.py and transport/registry.py. 37 kernels at base, 38 at head. Moves: calibration.prepare@1 (0929b16c0be4 → c13e76497b8d) and takeup.assign@1 (2f072b6bf09f → 53caf4ae91fc). Addition: geography.person_location@1 (3efcbe3741d0). The other 35 are identical, including the four geography.* kernels at d60f7ec38890. My base is 3100f31, not the hand-off's dfa5fe3, so some absolute values differ from its table; the move set is the same.

9. Test strength

Each mutant ran on a copy under /tmp/mut/<name> (copied packages/microcosm-build, other packages from head), copy first on PYTHONPATH, with a -p mutproof plugin that imported the touched module and asserted its real path lay in the copy. The unmutated control copy ran all nine target test ids: 9 passed.

mutant change result
person_region_from_wrong_household values[positions] → by row position red: test_person_location_gives_every_person_their_own_households_code (['01','99','13','13','13'] != ['13','01','99','13','01'])
gross_up_reads_unrounded_net JS solve reads the unrounded net ×52, in benefit_gross_up.json and transport_graph.json red: test_fractional_cent_nets_solve_against_the_exact_inverse_of_the_rounded_net (ambiguous certificate, rows 901, 902)
undetermined_passes judgment eq 1 → ge 0, central and S5 red: test_an_undetermined_judgment_reads_zero ({101: 200.0})
export_slice_includes_scratch_cents drop or expected & scratch red: …[scratch_cents] and …[reporting] (DID NOT RAISE)
sole_parent_uses_own_schedule gross-rate node takes the unit's is_single, n_dependent_children and masked youngest age red: test_sole_parent_cutout_uses_the_gross_rate_overrides_not_the_units_schedule (480.1 ≠ 521.52). A first version that kept the age-0 override survived; it's an equivalent mutant for every sole parent, because the age override alone picks the rate.
mine: bridge FILTER drops a person transport.boundary@1 drops the first person on nz.as.bridge only red, as a setup error: the executor's mass check refuses nz.as.bridge (117.315… -> 116.813…) before test_bridge_filter_keeps_every_person_unit_and_weight asserts. The test's own assertions can fire only for a dropped zero-weight person or a reordering, so it adds little over that guard.
mine: terminal re-keyed remove the compose.py re-key refusal and move nz.as.bridge.masks to nz.as red: test_nz_terminal_filter_keeps_the_skeletons_predecessors_and_ancestry ({'nz.as', 'nz…', 'nz.as.inputs'} != {'nz.as', 'nz.as.inputs'})

No test I read asserts over an empty set. _entitle writes the AS engine's outputs straight into the frame instead of running the packaged .as node (test_nz_as_bridge.py:795-811), but it then runs the packaged .positive_payment and .as.entitled nodes, which are the code under test. See finding 2.

10. Claims

  • The changelog fragment and the transport-entitlement.md paragraph match the code, except the placement sentence in finding 3. "Keep the package inactive" matches the activation gaps.
  • Hand-off invariants: each one I tested held.
  • "Failures that predated this session": each test edit follows a code change made on purpose. as_area became a string code in axiom_input_closure.json ("value": "1"); the income-decile classifier moved to *_annual_gross ×1; VHF joined the scenario list (so 43 * 10 in test_transport_weight_quality.py); the packaged graph now reads donor money (so the two test_nz_money_normalization.py assertions); the exact-slice test now drops a column rather than an entity, so the check it names is the one that refuses. H25 adds the entitled column to formula_owned_excluded. test_bridge_reads_only_unit_attributes_the_unit_rule_declares now subtracts the bridge's own derived columns and admits receipt_group for the S5 masks, which H24 and H26 create. I found no test that gave way.

11. Delta since the hand-off

  • _check_export_contract schema. create@1 lifts benefit_unit_rule through create_unit_declaration(CREATE_V1_REF, …); create@2 reads the CREATE node's unit_rule and ignores a legacy benefit_unit_rule (compose.py:1291-1301, :1512-1527). The executor projects the id of each sliced entity and every group membership onto a person slice (graph/executor.py:638-645), which is the set the check treats as automatic. I found no spec whose verdict differs from its declared roster. test_closed_export_contract_covers_every_declared_partition[False|True] and test_closed_export_contract_on_one_declared_unit_needs_no_legacy_rule cover the three cases.
  • _is_compiler_identity_field. A value that fully matches the regex has no . except the final .yaml, no :, no leading / and no .. segment, and the location must end at concept_mapping.json.bindings[N]. I found no matching value that a Python entrypoint loader could import.
  • legacy_json set. The test asserts set equality with spec.resource_rows filtered on legacy_json; it passed in the full run below (22 passed in test_spec_engine_country_bundles.py), so the closed set equals the package's legacy_json rows.
  • Builders' --check. Each of the three main()s loops over every document documents() returns and compares each file it would write (build_nz_transport_graph.py:897-909, build_nz_benefit_gross_up.py:332-344, build_nz_entitlement_graph.py:671-692). All three --checks pass at head.

12. The whole engine-free list, with no stop

I copied tools/run_engine_free_tests.sh to /tmp/w3b/run_ef.sh, removed --maxfail=1, used -n 3, and ran the same 691-file list (tools/ci_test_plan.py list-job engine-free) from /tmp/w3b/head, with -o addopts=--import-mode=importlib. The script's /usr/bin/time -v isn't installed in this container, so I used bash time. The run printed LOADED /tmp/w3b/head/packages/microcosm-build/src/microcosm/build/__init__.py. Log: /tmp/w3b/engine-free.log.

Final line: 1 failed, 24145 passed, 249 skipped, 3 xfailed, 2582 warnings in 6538.41s (1:48:58).

  • Failed: packages/microcosm-build/tests/engine_free/us/test_us_trade_imdb_bulk.py::test_unreadable_staging_subtree_aborts_before_any_marker (DID NOT RAISE <class 'PermissionError'>). The container runs as root, and chmod doesn't stop root reading the directory. It fails the same way at /tmp/w3b/base (__file__ proof: /tmp/w3b/base/packages/microcosm-build/src/microcosm/build/__init__.py). This failure is environmental and comes from neither tree's NZ change.
  • No failure or error occurs only at head.
  • Skips: 249 in all. In the NZ files, test_nz_transport_declarations.py skips 2 ("Pinned feed not staged"), test_nz_joint_geography.py 12, test_nz_target_references.py 38 and test_nz_axiom_input_closure.py 1, which matches the hand-off's skip list.

Closed-list counts from this run: test_nz_as_bridge.py 32 passed; test_nz_transport_toy_graph.py 20; test_nz_transport_declarations.py 29 passed, 2 skipped; test_nz_benefit_gross_up.py 16; test_atomic_geography.py 44; test_transport_composed_graph.py 465; test_nz_spec_package.py 1203. The hand-off's lower counts (19, 25 and 462) come from its earlier head, before the hub's H29 and delta tests.

I fixed nothing and pushed nothing.


Generated by Claude Code

@MaxGhenis
MaxGhenis marked this pull request as ready for review November 2, 2026 03:47
@MaxGhenis
MaxGhenis merged commit b6356ac into main Nov 2, 2026
10 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged by the NZ hub (session local_e407a527) at e1d2707 with --squash. Gates: gh pr checks exit 0; MERGEABLE; not draft; no CHANGES_REQUESTED review. Reviewed head: e1d2707. Independent review: cloud Opus 5.5 at e1d2707 returned APPROVE_WITH_NITS with no P0, P1 or P2 finding and nothing for Max (comment 6568519775); the hub checked each finding against the code. Belgium gave its OK on the shared hunks at e1d2707 on main 3100f31, with its five dumps byte-identical to main. Local results at e1d2707: stripped hunks equal both ways, 38 kernel hashes, goldens, builders, generators, and the artifact-gated files at 219 passed and 11 skipped. A follow-up PR takes the nits, and G8-x takes the cents tie. The whole engine-free list on 3.13 with no stop gave 1 failure, the root-only permissions test, which also fails on main.

@MaxGhenis
MaxGhenis deleted the nzhub/g8w3b-graph branch November 2, 2026 04:21
MaxGhenis added a commit that referenced this pull request Nov 2, 2026
* Fix the NZ bridge doc and tests the #1440 review flagged

The bridge paragraph predated H29 and never named nz.as.bridge. The childless-family
tests cast the column themselves and claimed the adapter raises, but the masked
override returns float64, so _batch_from_table takes its float branch. Both tests
now read that function. A new test runs a mixed couple through S5's base rate in
both person orders, because a `first` aggregation passes one order and fails the other.

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

* Correct the other stale bridge claims the review named

The generator's docstring and one test name repeated the wording the doc paragraph
lost, and the graph never matched it. Two as_rate_bridge.json reasons gave the
adapter's int64 read as why the youngest-child override takes a mask, which the
childless-family tests now show doesn't happen on this path: a null age would reach
the engine input as NaN. The golden moves only for that resource's hash.

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

* Fix the last wording findings on the NZ bridge review

The changelog credited the generator docstring with a change this PR did
not make: the docstring already named the shared population, and the PR
only dropped the sentence that contradicted it. The docstring's "every
coefficient" holds for the bridge alone, so it now says so.

benefit_unit_rule.json said the youngest-child age column holds 0 for a
childless family. unit_construction.py writes a null there, which is why
the bridge override takes a mask, so the description now says null. The
golden picks up that resource's new hash and the fingerprint.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants