Repository navigation
Open
Description
Part of #343. This is the last package of the NZ graph build. G1–G7 have merged: #1118, #1119, #1120, #1122, #1125, #1130, #1138, #1139, #1159, #1161 and #1181, plus the review follow-ups #1180 and #1182.
Why it is needed
Every NZ ledger reference on main is an authoring placeholder:
- 8 calibration targets (
build/nz/target_references.json); - 6 pre-calibration references (
precal_references.json); - 6 hold-out comparators (
holdout_references.json).
None of them has activation_status: active, and compile_ledger_target_references refuses any reference that isn't active (packages/microcosm-build/src/microcosm/build/ledger_targets.py, _require_executable_reference). So no NZ endpoint can run on real data until these references point at pinned Chronicle cells.
Scope
- Feed pins. Add
build/nz/chronicle_feed.json, pinning the Chronicle artifact by commit, SHA-256 values and row counts in the shape ofbuild/uk/chronicle_feed.json. Declare it inbuild/nz/country_package.json, so the spec-only package test accepts it. - References. Replace the placeholders with harvested, cell-pinned references for the approved calibration target set. Only the approved set is activated.
- Hold-outs. Hold-out references stay out of the calibration set. That covers Accommodation Supplement counts and dollars, Working for Families dollars, and the appropriation comparators. Activate them only as comparators.
- Thresholds. Write the approved gate thresholds into
build/nz/gates.json, and the approved scenario parameters intobuild/nz/scenarios.json.
Acceptance
compile_ledger_target_referencesresolves every active reference against the pinned artifact.- The NZ spec tests and the spec-only package test pass.
- A local real-data run compiles every target.
- Add the NZ transport contract and real-Axiom Frame bridge #820 is closed, with each of its acceptance items checked against
mainand linked to the PR that delivered it, or carried forward here.
Blocked on
The method card.Adopted on 2026-10-10, including the weight-quality thresholds (MC15): source-household ESS ≥ 4,000 and max/median ≤ 5 release-blocking, per-cell support with suppression, and the ±20% targets tracked. Enforce source-household weight-quality gates and per-cell support for the NZ graph (G8a) #1202 (G8a), merged 2026-10-12, writes them intobuild/nz/gates.jsonand enforces them.The remaining Chronicle NZ packages.Merged on 2026-10-10: IRD wage and salary distribution (Add IRD wage and salary distribution for TY2026 (chronicle#210 I2) chronicle#328), Stats NZ household net worth (Add Stats NZ household net worth for the year ended June 2024 chronicle#329) and the Budget 2026 Estimates for Vote Social Development (Add Budget 2026 Estimates for Vote Social Development benefits chronicle#330).- Census 2023 regional household structure: partly unblocked. Add Stats NZ 2023 Census household and family counts by regional council chronicle#335 merged on 2026-10-11 with 288 Census 2023 counts from Stats NZ's Place Summaries, for New Zealand, the 16 regional councils and the Area Outside Region: households and families, tenure (three categories), family type (three) and household size (eight). Still missing, and nothing approximates it: a regional household-composition table, and a social-renting split. Those need the Aotearoa Data Explorer table
CEN23_FHH_038.
Metadata
Metadata
Assignees
Labels
No labels
Activity
MaxGhenis commentedon Oct 12, 2026
Progress, 2026-10-12.
Merged
chronicle build-bundle --suite nz, a 48-package NZ roster with a drift guard. It also fixes the NZ Treasury packages' R2 key prefix. A real build of the suite on that branch produced 12,089 facts from 48 packages, none skipped.In review
nz/as_area_crosswalk.json, which had none, so no household could be given an Accommodation Supplement area. Each territorial authority's shares across Areas 1–4 come from public Stats NZ layers: Area Unit 2017 polygons, TA 2025, and 2023 Census population by SA1. Round 2 is in progress:Decided
gates.jsonwhen it activates those gates.Next
Still missing: a liquid-asset distribution for MC10. Without it, only the asset-test-off bound can run.
MaxGhenis commentedon Oct 12, 2026
Progress, 2026-10-12, NZ hub.
Merged today
--suite nz(chronicle#340).build-bundle --suite nzfrom merged main gives 66 packages, 0 skipped, 12,251 facts, and a valid consumer artifact.In review (each re-run by the hub at its head; reviews re-dispatching after today's restarts)
Next
The rulespec-nz waiver exits (d1055 = b) run on the Axiom side: the first reviewed candidate, WEP s75, is waiting on a waiver-refresh re-pin chain. They don't block the G8 graph work.
MaxGhenis commentedon Nov 1, 2026
G8-w3 split into three PRs: G8-w3a (#1437, NZ declarations and row activation), G8-w3r (#1436, run-entry money factors) and G8-w3b (next, the graph and the toy run).
New package G8-y (post-v0 reference activation), starting after G8-x. It owns the closing step for the rows v0 can't support: the WFF tripwires, MBIE P2, P3, Veterans Pension and the Treasury AN 24/01 hold-out. Each needs inputs v0 doesn't have, so naming the package doesn't promise activation.
reference_selection.jsonnames each row's prerequisites.🤖 Generated with Claude Code
MaxGhenis commentedon Nov 2, 2026
Ready for hub review
G8-r2c design
RESULT: READY
Needs Max: yes
Main is at
a6bcd1ab319302cadd924ab989d380b283854534. Every file:line below refers to that commit unless it names a w4 branch:g8w4cisorigin/nzhub/g8w4c-supportat3e72df35andg8w4disorigin/nzhub/g8w4d-nz-wiringatd81280bb.B/,T/andF/are as in the brief, andN/isB/nz/. Labels: Read means I read the code, Ran means I executed it this session, and Inferred means my reasoning. The embedded card, proposal and review are claims. I couldn't reach the pinned Chronicle feed (it's on R2), and the proxy refused ird.govt.nz (HTTP 403), so this report reproduces no feed value. Every IRD count or amount below comes from the proposal, labelled as a claim.What I ran. 400 tests passed and 39 were skipped (the skips need the Chronicle feed) in
test_nz_reference_intervals.py,test_reference_intervals.py,test_reference_profiles.py,test_nz_target_references.py,test_nz_money_normalization.pyand the frame'stest_transport_quantile.py. I also ran four probes against the packaged code. Each is quoted where it's used.Needs Max covers one non-blocking item, Q9: whether a Pareto top band fitted to the destination's own counts becomes the cross-country default. NZ's build doesn't wait on it.
1. State on main
N/precal_references.json:10onward; exempt atN/reference_selection.json:3331onwardird_wage_salary_people_by_band__row_7to__row_241, familyearnings. They are TY2026 facts (period_value2026), declared for period 2027, as person counts only: measurepeopleand no amount companion. Status isrequires_g8r2c_wage_quantile_producer, and the parking reason is that r2c must connect "the reviewed TY2026-to-TY2027 wage bridge, intervals and coverage" to a producer. The bands are 200 × $1,000 up to $200,000, 20 × $5,000 up to $300,000, 14 × $50,000 up to $1 million, then "Over $1 million" (Ran: label parse, no gap or overlap). There's no nil row: the first band is "$1 – $1,000".N/target_references.json; 474 of the 664 exemptions inN/reference_selection.jsonird_taxable_income_people_by_band(measurepeople) and 237ird_taxable_income_total_by_band(measuretaxable_income_nzd). All arerequires_prerequisites, withreference_prerequisites["reference_profile:aging_index","measurement:ird_taxable_income_ty2027_v1"](tools/generate_nz_target_references.py:90-99,652-657).measurement:(Read: nothing insrcdoes.B/reference_profiles.py:422-453satisfies only the profile half and leavesreference_prerequisites_remainingset). A producer for the columntaxable_income_nzd, which none writes.N/reference_aging.json:4810(central)p1_ty2026_to_ty2027_low/central/high= 1.026690 / 1.028580 / 1.030467, purposeprofile_input, source 2026 and target 2027. No profile transform uses them:N/reference_profiles.jsoncarries only the IRD transform at:84851.N/reference_aging.json:1216; transform atN/reference_profiles.json:84851-85334ird_ty2024_to_ty2027_*= 1.115056 / 1.117109 / 1.119158.scale_money_and_intervalsscales the 474 rows and the national table, underrequired_measurement_binding: ird_taxable_income_ty2027_v1.N/reference_aging.json:9590;N/money_normalization.json:29-40,64-97donor_cy2024_to_ty2027_*= 1.077090 / 1.079073 / 1.081052, purposemoney_normalization_input. The donor money period is declared as calendar 2024 (basis: declared_convention). This settles the card's "NZ money year" item at the calendar-2024 factor.N/reference_intervals.json:8(national),:2162(age); semantics inB/reference_intervals.py:91-109,289-433,679-685positive_band_closure: right,nil_band: present,negative_income_policy: floor_zero,first_lower_cut_cents: 0, with companions on every band. Age: 183 bands and no companions. There is no wage table and no coarse table. Ran: a 12-band table withfirst_lower_cut_cents: 4000000andnil_band: absentvalidates; scaling by 1.117109 gives a first cut of exactly 4,468,436 cents; and classification puts 4,468,436 below the first cut and 4,468,437 in band 1.N/precalibration_stages.json:1-84; validatorT/compose.py:1785-1920; guard:2183-2536nz.precal.main_benefit_ratesandnz.precal.benefit_gross_up. Every stage must name an Axiomrules_bindingwhoserulespec_pathmatches (:1824-1845).F/transport.py:271-391;T/population_kernels.py:411-480(_surface_bands),:483-606(kernel)employment_incomehas the money routequantile_mappedand awaits P1 (N/money_normalization.json:99-106).pending_outputsnames "P1 earnings …" (N/transport_graph.json:4332).nz.money.normalize,N/transport_graph.json:423money.normalize@1normalizes 7 person and 3 household columns and writes_centscompanions, with FX 1.654 and the calendar-2024 donor factors. The VHF sibling holds the factors at 1 (N/money_normalization_hold_fixed.json).nz.benefits.annual_transfers,N/transport_graph.json:1714;T/money_kernels.py:488-600*_annual_gross_transfer_cents, with exposurecentral: 1,full_year_stock_equivalent. The main-benefit gross-ups invert Schedule 1 (thenz.gross_up.*solves). There's no VP output. Nothing reads these four columns yet.T/evidence_contract.py:562-644; g8w4cT/evidence_kernels.py:890-1050; g8w4dT/compose.py:1626-1648,1742-1916quantile_mapped,normalized,currency,structural,closure_default,derived,pending,copiedanduncovered. Onlytransport.quantile_map@1maps toquantile_mapped, and the roster is the battery slice plus the problem's measures.person.employment_incomeis in neither:N/export_contract.jsonlists the normalized person columns andhousehold.rent, but notemployment_incomeorliquid_financial_assets.ird_taxable_income_proxy_centsandird_taxable_income_proxy_universe_status(tools/generate_nz_target_references.py:98-99).2. The earnings map (MC4, P1)
Kernel: a new
transport.quantile_map@2, with@1left unchanged.@1meets every need but one.@1takes literal band edges and a literal alpha (F/transport.py:75-92). It derives shares from unfiltered person count facts on the precal surface (T/population_kernels.py:433-441) and ranks at the pre-calibration weights (:569).@2adds one thing.interpolation = {"method": "pareto", "estimator": {...}}computes alpha from the same surface facts_surface_bandsalready resolves for the shares, and records the inputs and result in its receipt. Everything else delegates toF.quantile_map, so no new interpolation code exists.source_hashdigests a kernel's whole defining module (packages/microcosm-graph/src/microcosm/graph/kernel.py:524-563). So@2goes in a new module (T/quantile_kernels.py), with the estimator in a newB/tail_estimators.py. Adding the class topopulation_kernels.pywould re-key CREATE@1/@2, boundary, currency and quantile map@1.quantile_supportreads onlyinterpolation["method"]and the band rows (T/evidence_contract.py:576-600), so w4's support producer accepts@2's params unchanged.The wage bridge scales the band edges, not the mapped values.
Read. Scaling the values would need a second producer of
person.employment_incomein one lineage, and composition refuses that (T/compose.py:892-897). Onlymoney.normalize@1or a quantile map may own a donor money column after CREATE (:192,:913-918).Ran. The two are the same arithmetic. Uniform bands give
fL + u(fU − fL) = f(L + u(U − L)). The Pareto top givesfL·exp(offset)with the offset independent of scale. Mapping 2,000 lognormal values with a Pareto top, once on edges × 1.02858 and once on unscaled edges with the outputs × 1.02858, differs by at most 3.9e-16 relative.Where it's applied (Inferred, my proposal): a generated
N/earnings_bands.jsonholds, for eachaging_indexprofile, the effective edges in dollars. Each edge isfloat(cut_cents × factor / 100), computed exactly inFractionand rounded once. The document also records the factor id, its receipt SHA-256 and the source interval table's SHA-256. Each map node selects its profile's band list. Composition recomputes every edge from the wage interval table and the selected profile's P1 factor, and refuses any difference. V1 and V2 select low and high.Arithmetic on one band. "$50,001 – $51,000" is (5,000,000, 5,100,000] cents in TY2026 dollars.
Read. No FX rate or donor factor ever touches wages. Ranks don't change under positive scaling, so the 1.654 rate and the CY2024 factor would cancel. This is why
N/money_normalization.json:3already reserves earnings.Nil band, zero and negative wages, and closure.
employment_incomeis unsigned with lower bound 0 (F/concepts.py:737-761,931-942), so a negative wage can't reach the map.F/transport.py:254-268). That fits IRD's table, which has no nil row and counts only people with wages.ird_wage_salary_national_ty2026toN/reference_intervals.json, declared and validated with no companions: person,employment_income_cents, right-closed,nil_band: absent,negative_income_policy: refuse,first_lower_cut_cents: 0, open tail over $1,000,000.scale_money_and_intervals, because that refuses a table without companions (B/reference_profiles.py:262-266) and the wage rows have no amount companions. So it serves as the single source of cuts for the generator and the composition check above.quantile_mapis left-closed at a band edge. A rank equal to a cumulative share boundary goes to the next band at fraction 0 (F/transport.py:193-204) and maps to that band's lower edge. IRD's convention is right-closed.money.minor_units@1then rounds them half-even to cents (T/money_kernels.py:389-480). A value within half a cent above a whole-cent cut, such as 51,429.004, rounds onto 5,142,900 cents. The map put it in the upper band; the right-closed table puts it in the lower one.@1/@2's closure alone, and have the post-fit wage diagnostic (G8-x) count mass within 100 cents of every wage cut, as r2b does for income.The open top band: a Pareto alpha fitted to NZ's own counts.
grouped_pareto_mle@1, in a newB/tail_estimators.py. Take the declared anchor bands (a_k, b_k] with counts n_k, and the open top above c with count n_T. Write L0 = a_1 and S(x) = (x/L0)^(−α). Then p_k = (a_k/L0)^(−α) − (b_k/L0)^(−α), p_T = (c/L0)^(−α), and ℓ(α) = Σ n_k ln p_k + n_T ln p_T.aggregate_fact_keyunder the feed pinned inN/chronicle_feed.json.donor_tail. It would give NZ's top tail a US shape. The hub's position stands.Support evidence.
quantile_mapped, with the owner the map node.expected_maxis null, because the Pareto top is open (H39).expected_minis also null, because the negative side now gets the same open top.realized_minis 0, since wages are nonnegative.inside_quantile_supporttestslower ≤ |v| ≤ upperon each band (g8w4cT/evidence_contract.py:630-644).records_outsideis 0. The check proves only that the producer wrote within its declared range, which here is all of [0, ∞).person.employment_incomeisn't on the support roster, because the export contract and the battery slice omit it. w4 would see it only as a covered source of derived rows, and would never range-check it.N/export_contract.jsonand both slices (Q6).T/compose.py:158-187(SKELETON_KERNELS),:192,:910,:980, and g8w4dT/compose.py:1626-1632must list@2beside@1.3. The stand-in income measure (d1952 A)
Measure. Use main's names: binding
ird_taxable_income_ty2027_v1, columnsird_taxable_income_proxy_centsandird_taxable_income_proxy_universe_status. It's a person-level, annual, TY2027-NZD statistical measure, never described as IRD taxable income. A newN/income_measurements.jsondeclares it. A new shared kernel computes it in integer cents:measure.combine@1, in a newT/measure_kernels.py, so nomoney.*hash moves.Components, signs and money basis (all inputs are int64 cents columns that exist on main or that r2c adds):
employment_income_cents(newmoney.minor_units@1node after the map, half-even)nonfarm_self_employment_income_centsmixed_incomeAWE factor (H3)farm_self_employment_income_centsinterest_income_centscapital_incomeAWE (H3)dividend_income_centsrental_income_centsmax(0, r): losses not deducted*_annual_gross_transfer_centsnz_super_annual_gross_transfer_centsprivate_pension_income(the taxable-pension share is unknown),realized_capital_gains, Veterans Pension (Read: receipt is unsupported under H4,N/receipt_contract.json:142, and the VP rows stay parked), Youth and Young Parent Payment, ACC, student allowances, PPL, trusts, overseas income, LTC, partnership and Māori-authority allocations, taxable property disposals, and deductions and losses beyond those already netted.includedif x > 0 andexcludedif x ≤ 0.unresolvedapplies only where an input is null, and r2b refuses it.ird_taxable_income_proxy_nzd= max(0, x)/100 as the amount measure.B/reference_intervals.py:24-32), so the kernel doesn't floor twice.Coarse bands, counts and amounts. Add a new table
ird_taxable_income_standin_ty2024toN/reference_intervals.json:nil_band: absent,negative_income_policy: floor_zero,first_lower_cut_cents: 4000000;Ran: every coarse band tiles native rows exactly.
dimension_valuesholding that band's member labels, andexpected_member_count):identity.linear_combinationwithweight= factor.B/ledger_targets.py:3692-3752).scale_money_and_intervalsneeds a singletonaggregate_fact_keyand anidentityrow (B/reference_profiles.py:645-660). Belgium hit the same wall (tools/generate_be_target_references.py:104-118). Shared hunk A fixes it.B/reference_profiles.py:630), while r2b keeps cuts as exact rationals. The amount right-hand side carries about 1e-16 relative rounding. That's harmless, but the receipt should say so.income, measureird_taxable_income_proxy_nzd.taxable_income_nzd, which no producer writes. That's a mismatch with what r2c needs.Universe and nil band. Membership means "positive stand-in income", not IRD filing.
B/reference_intervals.py:24-32).Stress test. VSI, a new calibration variant on the raw
nz.openbranch: the measure without the four transfer addends. A sibling declaration inN/income_measurements.jsondefines it, with the same flags, payments and entitlement rules, and recomputed universe, bands and weights.N/msd_pre_activation_reconciliation.json:1101makes r2c own the declared annual-transfer exposure sensitivity, so add a second variant, VEX. It changesexposure.centralfrom 1 to a declared value in the annual-transfers node; the kernel accepts exact decimal text (T/money_kernels.py:525-545). Its value is Q5.Published gaps and diagnostics. Before matrix construction, record:
Replacing the 474 rows, not adding beside them.
N/target_references.json.N/income_diagnostic_references.json, drop their exemptions fromN/reference_selection.json, and strip companions from the national table, so it becomes declared-and-validated only, like the age table.aging_indextransform at the 24 rows and the coarse table._validate_companionsrequires a scaled roster to contain every row of its scope (B/reference_profiles.py:166-199; the scope excludesdimension_values), so the 474 native rows couldn't stay in the same document as unscaled companions._validate_interval_companionsrequires every band of a scaled table to name a roster pair (:247-292).requires_prerequisitesuntil composition proves the measurement producer (shared hunk A). Without that proof, compilation refuses (fails closed), rather than an exemption hiding them.N/reference_inventory.jsoncounts anddocs/nz-calibration-targets.md:150-180update with them.Places the proposal no longer matches main:
N/money_normalization.json:29-40,N/reference_aging.json:9590). 1.117109 survives only as the IRD target-ageing factor.N/money_normalization.json:99-106).N/precalibration_stages.json:41-82). Annual transfers exist asmoney.annual_transfers@1.ird_taxable_income_proxy_v1becomes main'sird_taxable_income_ty2027_v1.taxable_income_nzdon main and must becomeird_taxable_income_proxy_nzd.linear_combinationwith amount weights = factor" doesn't resolve on main (hunk A).N/gates.json:222,310).N/axiom_input_closure.json:1049). This now enters the measure: about 8.65% of the denominator is NZS (claim), and the review's scale table puts all-singles-alone at +41% against MSD spending (claim). See Q4.4. The route through S5
Entity, order and the stage list. Everything is on
person, on thenz.centralpopulation, upstream ofnz.calibrate, in this order:nz.open(geography) →nz.centralnz.earnings.map(@2, rewritesperson.employment_income), alongsidenz.money.normalizenz.earnings.cents(money.minor_units@1, writesemployment_income_cents)nz.encode.person, which already readsemployment_income(N/transport_graph.json:588), so main-benefit income tests see NZ wagesnz.takeup.assignnz.benefits.annual_transfersnz.income.standin(measure.combine@1)nz.targets.prepare(12 interval outputs, plus the effective interval companion that composition injects,T/compose.py:3092-3105) → compile → problemnz.calibrateEach calibration variant (V1–V4, VHF, VSI, VEX, VPT) repeats steps 2, 3 and 8 with its own profile, from
nz.open.Is
main_benefits_ratesre-run after calibration? Yes.main_benefits_rates, a listed S5 binding, runs again in the entitlement extension, innz.as.bridge.sole_parent_gross_rateandnz.as.bridge.cutout(N/entitlement_graph.json:64,181).T/compose.py:2269-2272), so this is allowed.How S5's guard treats NZ's person-entity bindings.
Read. The guard's checks of benefit-unit semantics apply only to bindings on unit entities (
T/compose.py:2192-2195,2396-2470). NZ's only unit entity isfamily: with CREATE@1,_unit_entitiesfalls back tobenefit_unit_rule.json(:1504-1509).Read. For person stages the guard does two things. It requires every derived output to come from a stage node upstream of calibration (
:2331-2340). And it refuses any reader of a stage node's column that isn't public (:2344-2357,2482-2483).Inferred. So the stand-in passes whether or not it's listed. S5's schema can't list it anyway, because every stage needs a
rules_binding(:1824-1845).Recommendation. Leave the measure off S5, and add
nz.precal.nz_superas a person stage:entitled_to_new_zealand_superannuation(purposeclassification) andnz_super_weekly_amount_before_tax(purposecalibration_measure);nz_super_weekly_amount_before_tax_cents, withnz.gross_up.super_centsgainingengine_refandperiodselections ofnz_super, asnz.benefits.net_centsdoes (N/transport_graph.json:1384).That records every engine-derived input of the measure on the declared list, which is what d1952's "shared route" asks, without a guard exemption. The alternative is a non-engine stage kind in S5, which Belgium would have to clear (Q2).
5. Gates
Gates and families r2c turns on (no gate kind is new except the last bullet):
calibration_per_family_fit(N/gates.json:10):incomegets its 24 rows at 5%.national_and_region_admin_aggregates(:51): the 24 rows are national anchors at 5%.target_profile_coverage(:83): theincomerequirement is met.calibration_reference_inventory(:74): the 24 rows must enter the matrix.T/gate_bindings.py:692-752.donor_support_bounds(:110) gets new roster rows: the stand-in's cents, NZD and universe columns, the 12 indicators, and, if Q6 is adopted,person.employment_incomeasquantile_mapped.reported_cell_support(:222) andreported_cell_support_target_a(:310): add VSI, VEX and VPT torequired_scenarios, as the card's "name the stress test in the scenario-support gates" requires.measurement_prefit, release-blocking. It holds the d1952 acceptance rules, frozen before any candidate (Q3).T/target_kernels.py:875-900), plus per-band support that hunk D adds.What
gates.jsonmust declare:measurement_prefitentry and its parameters;incomefamily lists, unchanged.The escalation triggers for VSI (a change above 10% in AS recipients or expenditure, or above 5% of the positive population crossing the floor) are review triggers, not gates. Declare them as diagnostic entries.
What stays red after r2c:
donor_support_bounds.nz.encode.personreadshousehold.rentandperson.liquid_financial_assets(N/transport_graph.json:588), and both staypendinguntil C1 and C2. So every derived row through encode stays red, including the stand-in, because its transfer sources run through encode.macro_realism_nz(:373) stays not applicable.weight_effective_sample_size(≥ 4,000) andweight_max_to_median_ratio(≤ 5). G8-x measures this.6. The split
The PRs are stacked on w4d. A to D are shared; E to G are NZ only.
A. Summed-band money scaling and measurement prerequisites (shared).
B/reference_profiles.py(:422-453,:640-668),T/compose.py(measurement proof),test_reference_profiles.py,test_support/microcosm_build/reference_intervals.py.identitymoney row (noaggregate_fact_key, listdimension_values,expected_member_count), and emit one operand{dimension_values, expected_member_count, weight}.measurement:<id>only when composition finds exactly one node upstream of calibration that declaresmeasurement_id: <id>and owns the interval table's income and universe columns on its entity. Otherwise the rows stay parked and compilation refuses.statbel_net_taxable_income_proxy_v1needs both (tools/generate_be_target_references.py:104-118,769-777).reference_profiles:targets.compile@1,calibration.prepare@1,targets.problem@1,transport.create@1and@2,transport.quantile_map@1,takeup.assign@1,takeup.gap@1,geography.support_from_facts@1andgates.battery@1(T/target_kernels.py:568-581,603-618,967-980;T/population_kernels.py:161,506;T/column_kernels.py:730;T/takeup_kernels.py:628;T/geography_kernels.py:284;T/gate_kernels.py:724). Editingcompose.pymoves none (H42).B.
transport.quantile_map@2andgrouped_pareto_mle@1(shared).T/quantile_kernels.pyandB/tail_estimators.py; registration;T/compose.py:158-187,192,910,980; g8w4dT/compose.py:1626-1632.compose.py's producer sets only.@1doesn't change.@2is new.@2with its estimated α is bitwise equal to@1with that α as a literal.@2's params and reportsexpected_max: null.C.
measure.combine@1(shared).T/measure_kernels.py, registration,T/compose.pySKELETON_KERNELS.D. Pre-fit support diagnostics and the
measurement_prefitgate (shared).B/reference_intervals.py(per-band distinctsource_household_idand Kish ESS, for count and amount contributions),T/gate_bindings.py,T/gate_kernels.py, tests.calibration.prepare@1andtargets.problem@1(both listreference_intervals,T/target_kernels.py:614,975), and the battery's binding identity. Land it in the same re-key window as A.E. NZ wage map (NZ only; needs B).
N/reference_intervals.json(wage table), newN/earnings_bands.json;N/precal_references.json(235 rows active),N/reference_selection.json(235 exemptions dropped),N/money_normalization.json(status text);N/transport_graph.json(the map and cents nodes;pending_outputs),N/scenarios.json,N/export_contract.json(if Q6);tools/generate_nz_target_references.py,tools/build_nz_transport_graph.py,tools/build_nz_entitlement_graph.py(which writesscenarios.json,:639).test_nz_target_references.py: the wage cuts match the labels; the feed audit skips here.test_nz_transport_declarations.py: the map precedes encode; edges equal cuts × factor for every profile; mutants for a hand-edited edge or a stale factor receipt.test_nz_transport_toy_graph.py: the support row isquantile_mapped, with 0 outside.packages/microcosm-build/tests/golden/nz_country_spec.json.F. NZ coarse references (NZ only; needs A).
N/target_references.json(24 rows), newN/income_diagnostic_references.json(the 474);N/reference_intervals.json(the coarse table; companions stripped from the national table),N/reference_profiles.json,N/reference_selection.json(474 exemptions dropped);N/reference_inventory.json,N/country_package.json;docs/nz-calibration-targets.md.G. NZ wiring, variants and gates (NZ only; needs C, D, E and F).
N/income_measurements.json;N/transport_graph.json(nz.income.standin; prepare and problem inputs;super_centsengine_ref),N/calibration_bindings.json,N/precalibration_stages.json(nz.precal.nz_super);N/scenarios.json(VSI, VEX, VPT; knob domains),N/gates.json,N/country_package.json;test_support/microcosm_build/transport_nz_toy.py.target_profile_coveragepasses onincome, andmeasurement_prefitruns.nz_supercolumn is refused.7. Questions for the hub
measurement:(B/reference_profiles.py:422-453).satisfied_measurementslist in the profile declaration.T/compose.py:1824-1845).nz.precal.nz_superas a person stage.:2192-2195).N/gates.json, new entry). Adopt the item 5 values as written, or change them now. They must be frozen before G8-x, because d1952 fixes every threshold before any candidate.N/axiom_input_closure.json:1049).nz_super_shares_residence_with_person_at_least_adult_agefrom household composition (another adult in the household), in r2c E or G.N/transport_graph.json:1756).N/msd_pre_activation_reconciliation.json:1101).person.employment_income(N/export_contract.json). Add it to the export and both slices, so the support gate range-checks the map.income_diagnostic_references.json.reference_inventory.json.grouped_pareto_mle@1) become the default for any country's open top band, against Belgium'sdonor_tail?Left out:
measurement_prefitand the composition proofs in A and E;Rulings the hub owes
measurement:by composition proof (blocks every income target).nz_superperson stage.measurement_prefitthresholds before G8-x.employment_income.Generated by Claude Code
MaxGhenis commentedon Nov 2, 2026
G8-r2c: hub rulings on the design (2026-11-02)
Source: the G8-r2c design, comment 6580994762 above (RESULT: READY). The hub checked it against main
e226192ada. Each ruling below is a single reading inside Max's d1952 (option A, 2026-10-24), so the hub makes it under his 2026-10-20 delegation and logs it here.The hub checked two of the design's mechanism claims on main
e226192ada:T/compose.py:1837). So the stand-in, which has no binding, can't sit on the list.nz_super_shares_residence_with_person_at_least_adult_agedefaults tofalse(N/axiom_input_closure.json, entry after:1049). Its own reason says this "overstates NZS for singles who share a home".The rest the hub takes from the report's own file:line evidence.
R2C-1 (Q1). Composition proves
measurement:Adopt (a). Composition satisfies
measurement:<id>only when it finds exactly one node upstream of calibration that declaresmeasurement_id: <id>and writes the interval table's income and universe columns on its entity. Otherwise the rows stay parked and compilation refuses. It fails closed, and Belgium'sstatbel_net_taxable_income_proxy_v1reuses it. Shared hunk A.R2C-2 (Q2). The S5 route: the stand-in stays off the list, and NZS joins it
Adopt (a).
measure.combine@1) stays a declared transport node, off S5's list. Hunk A's proof makes it the measurement's only producer.nz.precal.nz_super(person), with these outputs and changes:entitled_to_new_zealand_superannuation(purposeclassification);nz_super_weekly_amount_before_tax(purposecalibration_measure);nz_super_weekly_amount_before_tax_cents;nz.gross_up.super_centsgainsengine_refandperiodselections, asnz.benefits.net_centshas.main_benefits_rates, and NZS through the new stage. The stand-in's own arithmetic is one declared node.R2C-3 (Q3). The pre-fit rules, frozen now
The hub freezes every rule below before any candidate run (d1952: "fixed before we look at any candidate … no rule is loosened to pass"). A test pins each number. A change needs a new hub ruling, which the hub logs here before any candidate run, never after one.
nz.calibrate, against the aged IRD reference the matrix uses. Unweighted counts describe support only.R2C-4 (Q8). The 474 native rows
Adopt (a): a new non-executable
N/income_diagnostic_references.json. The 24 coarse rows replace the parked rows in the required target list; they don't sit beside them, as the card's "replace the required target list" asks. A test proves that no native IRD row sits in an executable document.R2C-5 (Q4). NZS's living-alone default goes
Adopt (a), in hunk G. The stand-in sums NZS, so the default now feeds a calibration target, and its own reason says it overstates NZS for singles who share a home.
nz_super_shares_residence_with_person_at_least_adult_agefrom household composition. Read the RuleSpec's definition of the input first, and bind to that definition, including its adult age.R2C-6 (Q6). Export
person.employment_incomeYes, in hunk E: add it to the export contract and both slices, so the support gate range-checks the wage map.
R2C-7 (Q5). VEX exposure 0.75
Adopt (b) as a declared judgment, disclosed as such. Main has no measured exposure (
N/msd_pre_activation_reconciliation.json:1101). VSI already covers exposure 0, so VEX should be a plausible alternative, not a second extreme. If MSD publishes counts of distinct annual recipients alongside the point-in-time stock, their ratio replaces 0.75 by re-declaration (a G8-x item).R2C-8 (Q7). Wage-earner count
A diagnostic only for v0: the sum of the 235 IRD counts against the model's weighted earners, before and after the fit.
R2C-9 (Q9). The tail: NZ's own band now, and the default question on Belgium's card
NZ. NZ declares
grouped_pareto_mle@1for its own open top band. It anchors on rows 231–241 ($500k to $1M plus the top), with sensitivity variant VPT, and it refuses α ≤ 1 and a bracket without a sign change.Belgium wants it too (Belgium's read, 2026-11-02). Belgium took a Pareto tail with no donor-tail mode, and proposes, under d1902 item 24, fitting its alpha with this estimator from RSZ 2026-Q1's top two classes. The closed form gives α = 3.348955.
The default question goes to Max on d1902, not on a new card. With two users, whether a fitted Pareto becomes the default for any country's open top band is a cross-country pattern, so it's Max's. The hub joined d1902 with NZ's view:
The caveat on that card holds: Belgium's anchor class holds 27% of known-pay workers, so it fits the body more than the tail. NZ's anchors start at $500,000.
NZ's build doesn't wait on that ruling. NZ's declaration stands on d1952's declared-convention pattern either way.
R2C-10. The design's corrections and the measure name
ird_taxable_income_proxy_nzd, and the amount rows stop namingtaxable_income_nzd, which no producer writes.measure.combine@1declares its floor, scale and output names in its resource, at Belgium's request, so a floor never has two homes. NZ declares floor 0 and scale 100.reference_profiles.py:630writes the factor weight as a binary64 float, while r2b keeps exact rationals. The receipt states the ~1e-16 relative rounding this adds.R2C-11. Build order
T/compose.py,T/gate_bindings.pyandT/evidence_contract.py, the files A to D touch. Building on its head avoids a second merge-up.@2support line._SUPPORT_PRODUCERSgainstransport.quantile_map@2in hunk B, together with the test that the w4 support producer accepts@2's params.reservation.py statusfor any hold on shared hashes. A movestargets.compile@1,calibration.prepare@1,targets.problem@1,transport.create@1/@2,transport.quantile_map@1,takeup.assign@1,takeup.gap@1,geography.support_from_facts@1andgates.battery@1. Other countries run those kernels too, so their stores re-run.MaxGhenis commentedon Nov 2, 2026
Cloud build session picked up G8-r2c shared hunks A and D.
Generated by Claude Code
MaxGhenis commentedon Nov 2, 2026
Cloud build session picked up G8-r2c shared hunks B and C.
Generated by Claude Code
MaxGhenis commentedon Nov 2, 2026
Hub ruling needed
G8-r2c shared hunk D: the
measurement_prefitgate kind needs one line in a frozen fileQuestion. Hunk D adds a release-blocking
measurement_prefitgate whose thresholds come from the country'sgates.jsonentry. The frozen loader refuses that entry.GateSelectionSpec.from_mapping(packages/microcosm-build/src/microcosm/build/country_spec.py:528, at intake w4dd81280bb) rejects anygateoutsideALLOWED_GATE_FUNCTIONS(country_spec.py:115-152), andmeasurement_prefitisn't on that list. Bothgate_kernels.py(GatesManifest.from_mapping,:404and:866) and the spec loader go through that check, so no country can declare the gate until the name is on the list. The brief listscountry_spec.pyas frozen.Options.
"measurement_prefit"toALLOWED_GATE_FUNCTIONSincountry_spec.py. That's one line, and it moves no kernel hash:country_specisn't among the modules any kernel'simplementation_hash()digests. I'd check that with the hash table before landing it.gates.json.Recommendation: (a), in hunk D, as the one frozen-file line. Until you rule, I'm building (c): the binding, its registry entry, the per-band support diagnostics and every test that calls the binding directly. A pinned test records that the loader refuses the entry today, and it flips when (a) lands.
Everything else in A and D goes ahead. The stack keeps building.
Generated by Claude Code
MaxGhenis commentedon Nov 2, 2026
Hub ruling R2C-12 (answers comment 6582256373)
Take (a): add
"measurement_prefit"toALLOWED_GATE_FUNCTIONSincountry_spec.py, in hunk D. It's the only frozen-file line this round may touch. Once it lands, delete the pinned "the loader refuses the entry" test, and test that agates.jsonentry naming the gate loads.One correction: the line does move kernel hashes. The hub read these at intake w4d
d81280bb50:gates.battery@1listscountry_spec_modulein itssource_hash(T/gate_kernels.py:765).targets.compile@1andtakeup.compare@1list it through_COMPILE_MODULES(T/target_kernels.py:568-581, used at:925and:1073).All three also list
reference_profiles, so hunk A already moves them. The line therefore adds no kernel to A and D's re-key window.takeup.compare@1is missing from the design's list of A's moves: put it in your hash table.UK's kernels leave
country_specout of their hashes on purpose (uk_runtime/target_compile_modules.py:59,EXCLUDED_DIRECT_IMPORTS), so they don't move. Show that in the hash table rather than taking it from me.List the
country_spec.pyline among D's shared hunks in your hand-off. Belgium's hub clears it with the rest.MaxGhenis commentedon Nov 2, 2026
Ready for hub review
G8-r2c shared B and C
Both PRs are drafts stacked on #1452's intake head
d81280bb. #1448 had no**Ready for hub review**/Fix roundcomment at hand-off, so I didn't merge the fix round in, and the hub merges up later. Each PR body has the full invariant, mutant, hash and per-file test tables.B/ispackages/microcosm-build/src/microcosm/build/,T/isB/transport/, and the tests live inpackages/microcosm-build/tests/engine_free/shared/.PRs
nzhub/g8r2c-b-pareto-mapnzhub/g8w4d-nz-wiringatd81280bba401e22fefb1abfee94592e7c639dc606aef0165nzhub/g8r2c-c-measurenzhub/g8r2c-b-pareto-mapc29e4871d9a46560af70574b1d3c8eba58f5ba6eB.
transport.quantile_map@2andgrouped_pareto_mle@1c7b7a747(the change) anda401e22f(the registration-count pin).B/tail_estimators.py:1-372.grouped_pareto_score(:311-319) computes ℓ′.grouped_pareto_mle(:322-372) bisects the declared bracket for the declared iterations, rounds once to 6 decimals half-even (_round_half_even,:160-168) and carries α as p/10^6.bracket_without_sign_change(:340) andalpha_not_above_one(:358).:109-143) carries the implied top-band mean αc/(α − 1) as an exact fraction.T/quantile_kernels.py:1-158.estimated_alpha(:54-112) reads the anchor counts from the surface_surface_bandsvalidates.run(:133-149) delegates toTRANSPORT_QUANTILE_MAP.runwith the fitted α as a literal.test_transport_quantile_map_v2.py:test_property_the_score_changes_sign_once_on_the_bracket.test_property_recovers_alpha_within_the_declared_tolerance. The PR body justifies the tolerance.test_property_scaling_every_count_leaves_alpha_unchanged.test_property_moving_mass_upward_lowers_alpha.test_belgium_rsz_counts_give_the_closed_form_alphaandtest_property_one_band_and_the_top_match_the_closed_form.@2is bitwise@1at the fitted α:test_v2_equals_v1_with_its_estimated_alpha_as_a_literalandtest_property_v2_is_bitwise_v1_at_the_fitted_alpha.test_anchor_counts_are_the_band_rows_counts.expected_max: null:test_the_support_bounds_kernel_reports_a_null_expected_max_for_v2.test_v2_support_bounds_both_signs_for_a_bare_band_list, which isxfail(strict=True, reason="needs the w4 fix round's E1"). The merge-up must remove the mark.test_belgium_rsz_counts_give_the_closed_form_alphagoes red;test_refuses_an_alpha_at_or_below_one_by_name;test_refuses_a_bracket_without_a_sign_change_by_name;test_anchor_counts_are_the_band_rows_counts;@1: the differential tests;@2from_SUPPORT_PRODUCERS,SKELETON_KERNELSor the registry:test_v2_is_registered_beside_v1;test_composition_accepts_v2_as_the_quantile_mapped_money_producer.C.
measure.combine@1ca9c8adf(the change),e7326250(import order),b905eddd(merge of B, no rebase) andc29e4871(the registration-count pin).T/measure_kernels.py:1-393.validate_measure_declaration(:88-161) reads the entity, components, signs, add-backs, floor, scale and output names from the declaration. The module holds none of them.combine_measure(:177-274) sums in Python integers and refusesint64_overflow(:217-221). A null component leaves the recordunresolved, with a null measure and a NaN unit. x ≤ 0 isexcluded.run(:295-384) carriesmeasurement_idinto the receipt.test_transport_measure_kernels.py:test_property_components_plus_add_back_equal_the_measure.test_property_the_universe_statuses_partition_the_records.test_property_the_output_does_not_depend_on_component_order.test_property_the_unit_is_max_floor_x_over_scale.test_property_differential_against_an_independent_integer_sum.test_a_null_component_leaves_the_record_unresolved_not_zero;test_the_add_back_ignores_an_add_back_components_losses;test_an_int64_overflow_is_refused_by_name;test_records_at_or_below_zero_sit_outside_the_positive_universe;test_the_floor_and_scale_come_from_the_declaration;test_a_negative_sign_subtracts;test_the_measurement_id_is_carried_not_used;test_measure_combine_is_registered_in_the_skeleton.Shared hunks, at the new heads, for Belgium's clearance
At B
a401e22f:B/tail_estimators.py:1-372andT/quantile_kernels.py:1-158;T/compose.py::182(SKELETON_KERNELS);:217-218(_QUANTILE_MAPS,_MONEY_PRODUCERS);:845-848(docstring);:944-956(_money_producer_gaps's expected producers and message);:1017(_check_single_money_normalization's kernel-to-route map);:1634(_SUPPORT_PRODUCERS);T/registry.py:56(import),:535(docstring count),:542(registration);test_transport_quantile_map_v2.py, andtest_transport_population_kernels.py:143-163(the registration pin; it pins refs, not hashes).At C
c29e4871(adds to B):T/measure_kernels.py:1-393;T/compose.py:191(SKELETON_KERNELS);T/registry.py:53(import),:536(docstring count),:547(registration);test_transport_measure_kernels.py, andtest_transport_population_kernels.py:143-164.None of these touches the code the fix round owns, and no frozen file changed.
test_support/microcosm_build/transport_nz_toy.pyis unchanged.Hash table
I hashed every kernel the engine-free transport registry installs, 40 at the base, at
d81280bb, B and C. All 40 are byte-identical at all three. The only new rows:transport.quantile_map@2=17ec8fcdc5bf535b…(B and C);measure.combine@1=8636ac082235b979…(C).No kernel digests
compose.pyorregistry.py, so their edits move nothing.@1's hash is unchanged because@2lives in its own module. Thesimulate.*rule kernels need an engine mapping, so the table omits them; neither diff touches a module they digest. Both PR bodies list every hash.Tests
Closed list: 53 files,
-n 4, no--maxfail. Per-file counts are in the PR bodies.a401e22fc29e4871test_aggregate_population_registration_installs_exactly_fourteen_kernelsfailed, because it pins the registry's refs. I updated it on B (fifteen) and then on C (sixteen), and reran the whole closed list at both final heads.test_nz_target_references38,test_nz_reference_aging16,test_nz_family_reconciliation9,test_nz_joint_geography1,test_nz_reference_intervals1 andtest_nz_msd_pre_activation_reconciliation1;test_nz_joint_geography11 andtest_nz_as_area_crosswalk3;rulespec-nzcheckout:test_nz_axiom_input_closure2;test_nz_transport_declarations2;test_transport_donor2.ci_test_plan.py verifyprintsverification=ok;build_nz_transport_graph.py --check,build_nz_entitlement_graph.py --checkandbuild_nz_benefit_gross_up.py --checkmatch. The other NZ builders need a feed or source directory, so these three are the source-free ones;Judgment calls
@2's interpolation is{"method": "pareto", "estimator": {"id": "grouped_pareto_mle@1", "anchor_references": [...], "bracket": ["1.00000095367431640625", "64"], "iterations": 64}}. The anchors must be the band list's top contiguous suffix, ending at the open top. The bracket is exact decimal text. The iterations are capped at 200.@2refuses signed and component band documents, because one α across two distributions isn't decided._surface_bandsreturns shares, so@2calls it for validation and reads the counts fromTargetSpec.valueon the same decoded surface.measure.combine@1imports r2b'sUNIVERSE_STATESfromB/reference_intervals.py, so its hash digests that module. Hunk D's edit toreference_intervals.pywill movemeasure.combine@1's hash in D's re-key window.Int64, so unresolved records stay null. It acceptsint64only when no record is unresolved.nzhub/…branches, not the session's defaultclaude/…branch.Left out
Generated by Claude Code
MaxGhenis commentedon Nov 2, 2026
Ready for hub review
VERDICT: APPROVE_WITH_NITS
Needs Max: no
G8-r2c B+C review: B
a401e22fefb1abfee94592e7c639dc606aef0165, Cc29e4871d9a46560af70574b1d3c8eba58f5ba6eBoth heads match the brief. I re-fetched them before posting and neither had moved. I reviewed at C's head, detached, with Python 3.13.18 and
uv sync --all-packages --locked, and diffed againstd81280bband B.B/,T/and the test directory are as in the brief, and every file:line below is at C's head unless it says otherwise. Labels: Ran means I executed it this session, Read means I read the code, and Inferred means my reasoning. I changed nothing in the repo. Every mutant file was restored withgit checkout --, andgit statusis clean.Summary. I tried to break both kernels and found no wrong output.
@2is@1at a correctly rounded six-decimal α, read from the one decoded surface.measure.combine@1matches the design's arithmetic. The misses are in the tests:Two PR-body claims go further than the design. Both are below.
1. B: the estimator's mathematics (
B/tail_estimators.py)1a. ℓ and ℓ′: HOLDS.
grouped_pareto_score(:311-319) computes exactly that:count * (width / expm1(alpha*width) - offset), then-top_count * top_offset, summed withmath.fsum._terms(:294-308) builds u, d and t withmath.logon exactFractionratios._anchors(:271) enforces d > 0.1b. Belgium α: HOLDS.
decimalat 50 digits (no mpmath installed): ln((1,066,243 + 369,203)/369,203)/ln(300/200) = 3.3489546347441576222607842433469875946621570878728…, which rounds half-even to 3.348955.3348955/10^6. The exact root sits 1.3e-7 from the nearest halfway point, so this one isn't close to a tie.1c. Rounding: HOLDS, with a stated exposure.
math.logat:302-308, thenmath.expm1andmath.fsumat:315-319. Bisection runs in floats (:346-353). Only the final rounding is exact:_round_half_even(Fraction(midpoint)),:160-168,:354.scratchpad/s/rnd.py).decimal.@1's Pareto inverse usesnp.log1p,np.expm1,np.logandnp.exp(packages/microcosm-frame/src/microcosm/frame/transport.py:153-168).@1still declares the defaultNumeric.BITWISE(T/population_kernels.py:492-495; the default is atmicrocosm-graph/.../kernel.py:188).@2reuses@1's capabilities object (T/quantile_kernels.py:119). So@2's α carries the same cross-platform exposure@1's mapped values already carry.1d. Degenerate inputs: HOLDS. Ran
scratchpad/s/degen.py. Each case is either refused by name or handled:@2the kernel then refuses, because@1says "Band shares must be finite and positive" (Ran, item 2). The PR body doesn't mention this case.bracket_without_sign_change(ℓ′ = −10.94 at both ends)@2,@1refuses the zero share.bracket_without_sign_change:230). The fit then runs, and a root ≤ 1 hitsalpha_not_above_one.True,64.0"1e3"," 2","-1","+2","2.","01.5","1.5 ","inf","NaN","٣", or a JSON number_DECIMAL_TEXT,:56, ASCII-only)alpha_not_above_onecan't fire under the declared bracket.bracket_without_sign_change. Ran: counts (1, 1000) gave ℓ′ = −94.4 at the low end.alpha_not_above_oneonly with a bracket starting at 1/2 (test file:149-157).2. B: where the counts come from, and which bands anchor (
T/quantile_kernels.py)2a. Belgium 2, one source: HOLDS.
estimated_alphadecodes the artifact once (:80), runs_surface_bands(document, surface, unit=unit)on that object (:88), and reads the counts fromsurface.registry.specson the same object (:95-103)._surface_bandsbuilds its dict the same way (T/population_kernels.py:418).@1decodes the sameartifact.payloadbytes a second time to get shares (T/population_kernels.py:554-558).scratchpad/b2.py, trying to make the decodes or sources disagree:estimated_alphadecodes exactly once, and the object_surface_bandsvalidates is that decoded object.valueis a Pythonfloat, so_anchors' type check sees what_surface_bands'float()sees.decode_target_surfaceto be nondeterministic on fixed bytes. The kernel has no other count input, so I couldn't build one.@2's own_surface_bandscall (:88) leaves all of B's tests green, because@1re-validates the same payload before any output is written. So it's safe, but no test pins@2's own validation. Style only; not in fixes owed.2b. The top-suffix rule: HOLDS. Ran
scratchpad/suffix.pythrough the kernel on the test's toy surface:[toy_wage_1, top]):90-94)2c. Signed and component band documents: HOLDS. Ran.
{positive, negative}and{components: […]}are both refused: "fits one alpha to a bare band list; signed or component bands are refused" (:72-76).3. B:
@2against@13a. The differential: HOLDS, for the right reason.
test_v2_equals_v1_with_its_estimated_alpha_as_a_literalandtest_property_v2_is_bitwise_v1_at_the_fitted_alphapass.@2writescanonical_json({"method": "pareto", "alpha": fit.alpha_float})(:109-111), wherealpha_float = numerator / denominator, a correctly rounded int/int (tail_estimators.py:95-97).canonical_jsonusesjson.dumps, which gives the shortest round-trip repr (microcosm-graph/.../canonical.py:43-58).@1reads the value back throughcanonical_document_paramandjson.loads(graph_inputs.py:122-141), thenfloat(interpolation["alpha"])(frame/transport.py:89)."alpha": 3.348955isfloat("3.348955"), the same correctly rounded double as 3348955/10⁶. The test's literal isfloat(receipt["alpha_decimal"]), so both sides are the same double, not two floats that merely happen to agree.3b. The receipt by hand: HOLDS. Ran on Belgium's anchors:
alpha = {numerator: 3348955, denominator: 1000000}.top_band_mean = 66979100/156597. By hand, 3.348955·300/2.348955 = 1004686500/2348955, which reduces by 15 to 66979100/156597, ≈ 427.716367. That equalstop_band_mean_decimal.score_at_final_bracket= [5.8e-11, 0.0], which bounds the root as the design asks.4. B: composition (
T/compose.py)4a.
git grep -n 'quantile_map@1'outside the tests at B: HOLDS. It gives 10 hits, all non-test and none in.mdor JSON:T/compose.py:181SKELETON_KERNELS, with@2beside it at:182T/compose.py:217_QUANTILE_MAPS, which holds bothT/compose.py:845T/compose.py:951T/compose.py:1016@2at:1017T/compose.py:1633_SUPPORT_PRODUCERS, with@2at:1634T/population_kernels.py:484and:491@1's own definitionT/quantile_kernels.py:5T/quantile_kernels.py:66@2's refusal "a literal alpha belongs to transport.quantile_map@1", which should name@1aloneEvery other hit lists
@1alongside@2. No hit should accept@2and fails to.4b. A toy graph whose quantile-mapped column comes from
@2: HOLDS.scratchpad/roster.py):person.employment_incomemap node with full params: the test's bands, and either@1with α 2.5 or@2with the estimator. Then I rancompose._support_roster.employment_incomeas a source, and those 307 rows are byte-identical.quantile_mappedunder both, againstpendingwith no map (:1842-1846).test_the_support_bounds_kernel_reports_a_null_expected_max_for_v2runs w4's donor-ranges and support-bounds kernels on@2's params and getsexpected_max: nullandrecords_outside: 0.quantile_supportkeys oninterpolation["method"](T/evidence_contract.py:576-585), which isparetofor@2.scratchpad/money.py): I called_check_single_money_normalizationwith the real NZmoney_normalizationresource on a minimal graph.@1rewritingperson.employment_income: accepted.@2doing the same: accepted.transport.currency@1doing the same: refused, "declared quantile_mapped but transport.currency@1 node 'x' produces it".test_composition_accepts_v2_as_the_quantile_mapped_money_producercovers_money_producer_gapsthe same way.4c. Belgium 3: HOLDS.
@pytest.mark.xfail(strict=True, reason="needs the w4 fix round's E1")sits ontest_v2_support_bounds_both_signs_for_a_bare_band_list(test file:784).git diff d81280bb c29e4871 | grep -n 'xfail\|skip'finds that mark and nothing else, so it's the only xfail or skip either PR adds.5. C:
measure.combine@1(T/measure_kernels.py)5a. Declaration validation: HOLDS. Ran
scratchpad/cdecl.pyon 34 malformed declarations. Every one is refused by a message naming the field::92-103);:124-125);True,1.0or"1"(:107-122);100.0,Trueor"100"(:138-139);0.0,0.5,Trueor"0", and 2⁶³ or −2⁶³−1 (:136-141);:142-153);:126-135).A negative integer floor (−5) is accepted, which the spec doesn't forbid.
5b. Arithmetic: HOLDS. The universe threshold is a build-session reading, not specified.
_component_valuesconverts every cell withint(value)(:164-174). The signed terms, the add-backs and the total are Pythonint(:203-216). The unit ismax(floor, x) / scaleas int/int (:359-362).:217-221), not each partial sum.sum_measureandunweighted_signed_totalare Python ints that can exceed int64 even when every x fits. They are receipt JSON, not columns.:209-212,:225,:359-362).unresolved, a<NA>measure and a NaN unit.excluded.includedwhatever the floor. Ran: with floor 2000, x = 1000 isincludedand its unit is 2.0.includedif x > 0" and "ird_taxable_income_proxy_nzd= max(0, x)/100". It has no floor field at all.5c. Add-backs: HOLDS.
max(0, r): losses not deducted", x = Σ signed components + rental add-back, and "add-back = Σ max(0, −r)".backs = max(0, -value)on the signed add-back component (:213-215) and adds it (:216). So r + max(0, −r) = max(0, r).5d.
measurement_id: HOLDS. Here is the contract hunk A must read."measurement_id", optional (:297-302).str, checked bystring_param(:309-313;T/graph_inputs.py:85-91).params, not inside themeasuredeclaration, someasure_sha256doesn't cover it.receipt["measurement_id"], and isNonewhen absent (:379-383).node.params["measurement_id"]as a string on the node whose outputs are the interval table's income and universe columns.5e. Money classification of a
_centsoutput: HOLDS. It's accepted, and correctly left outside the money checks. Ranscratchpad/money.py, with the NZmoney_normalizationresource present:measure.combine@1writesird_taxable_income_proxy_cents,_nzd,_universe_statusinterest_income_cents, a normalize companionT/compose.py:1009-1026)interest_income,nonfarm_…,farm_…):1088-1094)employment_income_centsemployment_incomeisquantile_mapped, not normalized, so its_centsname isn't a companion. The design reserves that column formoney.minor_units@1, and_check_money_bridge_owners(:969-989) would refuse a second owner in lineage.money_concept_columns()holds onlyUnit.BASE_CURRENCYconcepts (B/money_normalization.py:340-346), andird_taxable_income_proxy_centsand_nzdaren't among them.derived, with its components as sources (:1859-1886)._MONEY_BRIDGESandreporting.*outputs are (:1384).5f. The
UNIVERSE_STATESimport: HOLDS.implementation_hashdigestsreference_intervals_module(:287-293). Ran: the hash is8636ac082235b979….INCLUDED, EXCLUDED, UNRESOLVED = UNIVERSE_STATESunpacks by position (:70; the tuple is atB/reference_intervals.py:79). If D reordered the tuple, the labels would swap silently."included","excluded"and"unresolved"strings (for example, test file:159-174and:189-209), so a reorder goes red.6. The stack
6a. Hashes: HOLDS. Ran
scratchpad/hashes.py.calibrate.ordered_adam@1, gate, terminal, take-up, reporting and evidence (with an empty engine mapping). That's 40 kernels atd81280bb, 41 at B and 42 at C.diffbase → B adds onlytransport.quantile_map@2 17ec8fcdc5bf535b….diffB → C adds onlymeasure.combine@1 8636ac082235b979….simulate.*engine kernels aren't covered, as in the hand-off.6b. Tests: HOLDS. Ran with
-n 4at each head.git grepfor_money_producer_gaps,_check_single_money_normalization,_SUPPORT_PRODUCERS,SKELETON_KERNELSand_MONEY_PRODUCERSfound the composition files._SUPPORT_PRODUCERS' classes.d81280bba401e22fc29e4871test_transport_graph.py's float-literal and country-name guards overT/*.pypass at all three.6c. Mutants. B: the hand-off's mutants HOLD. C: NEEDS CHANGE (tests). Each mutant ran against its PR's test file at C's head.
test_refuses_an_alpha_at_or_below_one_by_nametest_v2_refusals[…top…]×2@1(theirs)tail_estimators.py:166)@2's_surface_bandscall (quantile_kernels.py:88)@1re-validates (2a).test_refuses_malformed_anchors[rows3-contiguous]test_the_receipt_records_the_fittest_the_declaration_is_parsed_exactlytest_an_int64_overflow_is_refused_by_nameweighted_signed_totalignores weights (:245)below_zero.weightedsums the x = 0 set (:264)rewriteoutput (:339):315-316):331-335):140-141)6d. PR bodies: two claims overreach. I found nothing else the code contradicts.
Every file:line range I spot-checked in both bodies is accurate at the heads, as are the test counts for the files I ran and the hash table.
Fixes owed
T/measure_kernels.py:244-247and:264-265.weighted_signed_total,below_zero.weightedandat_zero.weightedagainst hand sums over the frame'slinspace(1, 2, n)weights.:315-316(whole-row slices),:331-335(outputs equal the declaration),:339(norewrite) and:140-141(floor fits int64).test_node_shape_refusalsandtest_malformed_declarations_are_refused.B/tail_estimators.py:160-168._round_half_evenon Fractions (for example 1.0078125, which should give 1.007812, and 1.0078135, which should give 1.007814).alpha_not_above_onecan't fire under the declared bracket [1 + 2^−20, 64].bracket_without_sign_change(1d). It is still refused, so R2C-9 holds, but the receipt names the wrong reason.Left out
--checkand no ruff.make_nz_toy_fixture). 4b and 5e use the real NZ spec, with a minimal graph for the money check.Generated by Claude Code
MaxGhenis commentedon Nov 2, 2026
Hub rulings R2C-13 and R2C-14 (from the B and C review, comment 6585875953)
R2C-13. Where a node declares its measurement. A node declares
measurement_idin its ownparams, as a non-empty string under that key. C'smeasure.combine@1already takes it there (the review's 5d). A's proof readsparams["measurement_id"]on the node that writes the interval table's income and universe columns, and refuses any other place. The A and D continuation brief carries this ruling.R2C-14. C's universe ignores the floor.
measure.combine@1marks a recordincludedat x > 0,excludedat x ≤ 0 andunresolvedon any null component, whatever floor the declaration names. The floor bounds only the unit column, max(floor, x)/scale.MaxGhenis commentedon Nov 2, 2026
Cloud build session picked up G8-r2c shared hunks A and D, continuing from the pushed heads after the earlier session stopped at an account limit.
Generated by Claude Code
MaxGhenis commentedon Nov 2, 2026
Cloud build session picked up the holdout separation guard (B3 claim 8).
Generated by Claude Code
MaxGhenis commentedon Nov 2, 2026
Cloud build session picked up the G8-r2c B and C merge-up.
Generated by Claude Code
70 remaining items
MaxGhenis commentedon Nov 3, 2026
Ready for hub review
VERDICT: APPROVE_WITH_NITS
Needs Max: no
F1 null household pointers and holdout rows review
Brief 210 v2, cloud review session, 19:19Z to 20:25Z. Ran:
gh api …/pulls/1481printedopen 9b49ef303bb4f0a3188f871236591ade3b363a07, and the earlier-report check printed nothing. I set up the head, main (3995d92d) and M (origin/main31c89390merged with the head, clean) on Python 3.13.18 withuv sync --all-packages --locked. I pushed nothing, and every worktree I mutated readsgit statusclean. The interim is comment 6614221923. Its "9 of the 13 forms" should read 10, as item 4 below gives.1. Scope
git diff --statlists only the five allowed files:compose.py+32/−6,concept_mapping.py+13/−4,concept_reference.py+14/−9, and the two test files. These match the body's "17 lines" and "38 lines".F/concept_mapping.py:2096-2098,:2121-2125and:2127-2136are the three fix 1 lines, plus comments and docstrings.T/compose.py,_is_holdout(:1010-1016) is a shared predicate that both guards use.seenmoves up (:1035) because fix 4's dedupe needs it, and the new gap block is:1039-1049. The stage guard filter is:3015-3021, and the.get('entity', '?')is at:3134.holdout_columnsis needed: at main a list entity raisedTypeError: unhashable type: 'list'.2. Fix 1 and I1
Read.
_household_rowshas three readers at the head:household_amount_for_persons(:2125) gives row -1 the appended NaN.reference_rows(:2136) gives row -1 the appended -1._apply'sAllocateToReferencePerson(:2221-2223) compares the reference row with the person's own row. -1 never equals a row, so the person gets 0._role_flags(:2274-2282): theREFERENCE_PERSONtest isreference == rows, so it is False at -1. The partner role readsnp.where(reference >= 0, …, -1), so the person is never flagged as the reference person's partner, married or not._coresident_adult_flags(:2301-2325),_child_countsandHAS_PARTNER/NO_PARTNERdon't read_household_rows, and F1 leaves them as Add a person-level coresident_adult transform kind #1477 left them. A null pointer shares a household with nobody. An unknown id groups with the other persons who carry that same id._Units,:2443-2451,:2485-2495) still converts pointers withto_numpy(dtype=np.int64)and refuses unknown households, as the spec asks.Ran. I used a probe frame: P1 in household 10, P2 and P4 in household 20, and P3 detached, with partners P2↔P3. Each line gives the head first, then main.
pd.NApointer, Int64 and UInt64. Head: rows[0,1,-1,1], reference rows[0,1,-1,1], amount NaN for P3,rent_paid[100,900,0,0], and no partner flag. Main: reference rows[0,1,1,1]and amount 900 for P3. P3 read household 20's row.None, with int64 or object household ids. Head gives -1, NaN and 0. Main gives reference row 1 and amount 900.[10,20,NA](Int64, UInt64 and objectNone). Head: P3 gets -1, NaN,rent_paid0 andreference_personFalse. Main: P3 found the null-id row, with rent 500 andreference_personTrue.rent_paidall 0. Main raisedIndexError: index -1 is out of bounds for axis 0 with size 0.InvalidIndexError: Reindexing only valid with uniquely valued Index objectsfromget_indexer(:2093-2095). They raise even when no person points at the duplicated id. F1 doesn't change this.3. Fix 2
concept_reference.py:57-78,:121-124) gives a person with no householdhousehold_of= None.reference_ofthen gives None, allocation gives 0, and every reference-person and partner role gives False._detached(test_concept_mapping.py:164-182) draws null pointers (st.none()) and unknown ones (st.integers(0, 2**40)outside the known ids) for a random subset of persons. The column is Int64 when any pointer is null and int64 otherwise. It never draws UInt64 or a null id in the household table.test_encode_matches_a_row_by_row_referenceruns for all four packaged mappings (PROPERTY, 100 examples). Ran. These mappings read household rows:policyengine-us(3AllocateToReferencePerson, plus the reference-person, partner and unmarried-partner roles) andaxiom-be(1AllocateToReferencePerson).axiom-nzandpolicyengine-ukread none.encodecan't agree vacuously. Detached rows compare as 0.0 and False, not NaN.assert_encode_matches_referencecompares floats withnp.isclosewithoutequal_nan, so NaN against NaN would fail the test, not pass it.4. Fixes 3 and 4, and I5
Ran. For each form I called
_holdout_separation_gapsdirectly,_precalibration_stageswith a stage output on the measure, andvalidate_transport_activation(head / main):validate_transport_activationTypeErrorKeyError(?, …)/KeyErrorTypeError(?)/ silentTypeErrorTypeErrorfrom the row check at bothTypeErrorTypeError: unhashable type: 'list'at both (compose.py:984)(7)/ silentKeyErrorTypeErrorat both (:984)holdout_columnsalone (:3017-3021).target_referencesdraws a leak gap that names the entity as?or7.validate_transport_activationraisesTypeErrorbefore the guard on a non-mapping row and on a list or dictname, and it did so at main too (Fixes owed 2).5. I4 across versions
Ran. I ran
encodefor all four packaged mappings at main and at the head, in separate processes, on the same pickled inputs:concept_frames()frames, with a share and a rate drawn per frame;test_group_encode.unit_frames()cases through that module's_encode(encode_groups);Every person household id named a household.
assert_frame_equal(check_exact=True)found 0 differences in values, dtypes or deferred bindings, across 174,900 columns. No input raised at either version.6. The properties
Read.
_strays(test_concept_mapping.py:2297-2333) draws:concept_frameshasmin_households=1), an empty person table or an object column. Only the unit testtest_empty_household_table_leaves_everyone_without_onecovers the empty household table, and only for Int64._holdout_rowsdraws list, dict, None, int, empty and missing measures and entities, plus non-mapping rows. It never draws a non-text name or a holdout document in two views.Ran. I raised
max_examplesto 1000 with a fresh database. I1, I2 and I3 passed 1000 each, with 133, 83 and 109 invalid. I5 passed 1000, with 64 invalid. The run took 150 s.7. The changed old test
test_activation_reports_a_malformed_holdout_rowcovers the droppedholdout_referencescase. It covers the list and dict measures that the old test drew, and asserts the new gap both from the guard and throughvalidate_transport_activation.8. Mutants
Each run used the whole test file named, with
-n 4, and I restored the tree after each one.Ran, the hub's mutants:
seencheck at:1047-1049): onlytest_guard_names_a_malformed_holdout_row_once_across_viewsgoes red, out of 51 tests.no entitycase and I5, as the body says.Ran, my own mutants:
_is_holdoutaccepts an empty measure): only I5 goes red.(resource, name), not by gap text): only I5 goes red.holdout_measuresdrops rows with a non-text entity): the stage guard'sno entitycase and I5 go red.9. Kernel hashes
make_composed_fixtureinstalls 45 kernels. Eight move, with the same 12-character prefixes as the body's table. A second run at main repeated every hash.concept_mapping.pyalone moves all 8, andcompose.pyandconcept_reference.pyalone move none. I didn't hash theAxiomEnginekernels.10. Tests at the head and at M
Ran.
-n 4 -rs, withNZ_CHRONICLE_ARTIFACTunset. The head and M gave identical results:test_transport_holdout_separation.py-k "holdout or precalibration"The skips:
test_nz_axiom_input_closure.py:535and:610needaxiom_rules_enginewith its dense extension and a rulespec-nz checkout.test_nz_transport_declarations.py:281×2 reports "Pinned feed not staged", because the feed is unset. The hub ran those two with the feed.No test result differs between the head and M.
11. The body and the hand-off
F/concepts.py:1403andF/transport.py:593/:876;concept_mapping.py:2096-2098,:2121-2125and:2127-2136;compose.py:1010-1016,:1019-1049,:3015-3021and:3134, with:1027on main;concept_reference.py:57-78and:121-124.85efd107plus the new test make 51 at the head, and 359 + 50 + 252 + 50 = 711 matches the hand-off.4a36edcf(on main) and with20507ad8.validate_transport_activation(Fixes owed 2).ci_test_plan.py verify.Left out
ci_test_plan.py verify.AxiomEnginekernel hashes.encode_groupswith a null or unknown pointer (Read only:_Unitsrefuses it at:2443-2451).max_examples.Fixes owed
None blocks the merge. Two body nits:
T/compose.py:1039-1049. The PR body says the guard "reports each malformed row once, even when the document arrives in two views". Ran. That holds when the views are identical. A second view that holds the row at another index gives two gaps,[1]and[0]. Such a view needsreference_profilesto nameholdout_referencesas its reference resource, and no packaged spec does that (Inferred). Smallest fix: write "even when the same document arrives in two views" in the body and the squash message.validate_transport_activationraisesTypeErroron a non-mapping holdout row (its row check), and on a list or dictname(compose.py:984,_reference_activation_gaps). Both behave the same at main, and F1's test pins the first. Smallest fix: write "neither guard raises" in the body.Generated by Claude Code
MaxGhenis commentedon Nov 3, 2026
Interim: H2 NZS co-residence binding review
Head
1b56b916. M is the head merged with G's fix headf43e8fbb, then with origin/main31c89390: main has moved past4a36edcf, so I merged it into M as well. Both merges were clean. F is M merged with F19b49ef30, also clean. The pinned feed built, and both digests match. Dependency sync took about 45 minutes of the time box.Findings so far:
git show 891cb9f1 | git patch-id --stableprintse3f8d1c790817d1c3e6677e68fd1f0a564f68af2. No commit touchesF/concept_mapping.py, anyT/*.py, or another sharedsrcor test-support file beyond the two prepared hunks. Shared test lines change only attest_transport_evidence_kernels.py:810and:2734-2735.nz_super_living_alone_adult_ageis 18 (core.yaml:65-79), and core.yaml names it only at its definition. The input's name, "at least adult age", supports>=. Onlynz_super_living_alone(core.yaml:550-572) reads the input, and only the single branch ofnz_super_ordinary_weekly_rate_before_tax(:664-670) reads that rule. The closure defaultsnz_super_principal_residence_is_qualifying_accommodationto true, so every single superannuitant reaches the rate. The rates 595.57 and 647.37 and the clause 1(b) citation match core.yaml:80-111. core.yaml at6fe181fcand at8dc2507dhashes tod8ea0d15…._Context.__init__raiseInvalidIndexError. F1 changes_household_rows,household_amount_for_personsandreference_rows, and_coresident_adult_flagsreads none of them. So F1 doesn't reach the binding, though the hand-off gives a different reason ("not throughFrame").nz.createframe, it agrees with the binding for all 120 persons. 118 persons read true, and the two who read false are 10006 (20) and 10009 (21). All 20 rows aged 65 or over match the hand-off.git grep '791c1cd4\|06ce7ca0'finds nothing, and the closure's SHA-256 isd2694200…. The closure partition moves as claimed: nz_super goes from 2 encoded and 11 defaulted to 3 and 10, and the whole closure from 93 defaulted to 92. The only entries that change are the sharing input and the exempt reason.Still running: the 42-file runs at the head and at M, the builders'
--check, and the mutants.Generated by Claude Code
MaxGhenis commentedon Nov 3, 2026
Ready for hub review
VERDICT: APPROVE_WITH_NITS
Needs Max: no
G8-r2c G toy review with both fix rounds
All three heads match the brief: toy
85d55af5, fixf43e8fbband G64ef20af, pergit ls-remoteat 19:18Z, and #1183 is open. M is main31c89390with the head merged in. It merged with no conflict as9752cf31and was never pushed. Main has moved since4a36edcf, so M differs from the head. I built the feed from Chronicle8f4cfd57under Python 3.13, and both digests match the pin (03f5ea0f…,1ea2d1ee…). Every run setNZ_CHRONICLE_ARTIFACTunless a line says "without the feed". File:line is atf43e8fbb. Labels: Ran means I ran it, Read means I read the code, Inferred means my reasoning. Every mutant was restored, andgit statuswas clean after each one.No finding blocks the landing. The four design tests, the hub's one-writer test and both fix rounds hold, and every mutant the authors reported reproduces. The nits are five places where a test is weaker than it reads. Three of my mutants survive in the toy file, and one survives fix 3's feed-free test.
1. Scope: HOLDS
Ran.
git diff 64ef20af 85d55af --statchanges 3 files.9ae0914c(toy session) changestest_support/microcosm_build/transport_nz_toy.py(+343) andtest_nz_transport_toy_graph.py.4ae5810e(toy session) changestest_nz_transport_toy_graph.py(+21).85d55af(hub) changestest_nz_transport_declarations.py(+53/−3). Nosrc/,tools/or golden file changes.Ran. The first-parent log over
85d55af..f43e8fbb:f43e8fbbKeep NZ's interval outputs out of the small-frame column tests:test_calibration_columns.pyonly.20507ad8Expect G's nz_super stage in the holdout stage-guard test (R2C-26):test_transport_holdout_separation.pyonly.d1d72353Merge main (4a36edc) into the E-G fix round. Its parents are044440a7and4a36edcf, and its tree52aafc74equalsgit merge-tree --write-tree 044440a7 4a36edcf, so it has no hand resolution.044440a7Name the stand-in in the declared-scenario scaling test:test_transport_composed_graph.pyonly.0506c9d6Test each wage cut against its own committed label:test_nz_transport_declarations.pyonly.06760993Test each coarse row's labels against its committed native rows:test_nz_target_references.pyonly.Each commit matches its row in E-G fix round: feed-free label tests, the stand-in rename and R2C-26 #1486's body.
2. The toy mirrors G: HOLDS, with disclosed differences
Packaged counterparts are at
64ef20af. Read and Ran, by printing both sides.toy_standin_declaration(transport_nz_toy.py:638). Packaged:N/income_measurements.jsonmeasures.central. Both have floor 0, scale 100, the outputs measure/unit/universe_status, andrental_income_centsas the only add-back, with sign +1 everywhere. The difference: the toy sums 7 components (wages, farm, interest, rental and three toy transfers), and the packaged measure sums 10. The toy has no non-farm self-employment, dividends or NZS column. That's harmless to the gates, but see nit 4.nz.income.standin(:725), againstN/transport_graph.json:1845. Both usemeasure.combine@1onnz.central, the samemeasure/measure_sha256selections (json/valueencodings), the samemeasurement_idparam, the same three outputs and dtypes (Int64/float64/string), and annz.earnings.centsmoney.minor_units@1node with identical params (:642packaged). The input is the 7 toy columns, not the 10.:652): person,nil_band: absent,floor_zero, first cut 15,000 above zero, and 3 bands, each with both companions. Packaged: the same policy, first cut 4,000,000, 12 bands.:693): count rows carryvalue_operation: sumwith no operands, amount rows are identity with noaggregate_fact_key, and each row has anexpected_member_countand a list selector fromTOY_STANDIN_MEMBERS. That's the same shape as packaged…people_by_band__b01(sum, 10) and…total_by_band__b01(identity, 10), per R2C-17.toy_people. The problem slices the indicators, cents, universe, the NZD unit (R2C-25) andhousehold.source_household_id, which matchesnz.targets.prepare/nz.targets.problem. The toy's prepare id isnz.calibration.prepare, a naming difference only.R2C3_PREFIT_THRESHOLDS(test_support/microcosm_build/reference_intervals.py:389-395, equal toN/gates.jsonmeasurement_prefit) with support 3/2 andrequired_evidenceequal to the packaged six. Its criticality isdiagnosticwhere the packaged gate's isrelease_blocking, which Test G's income stand-in on NZ's toy graph #1483's body discloses. Coverage requires["income","toy"], against the packaged six families.3. The four tests
Toy file at the head: Ran, 31 passed, 0 skipped.
(1) Rows reach the matrix (
test_nz_transport_toy_graph.py:786): HOLDS. Each of the 6 rows appears once intarget_metadatawith a nonzero matrix row, familyincomeand its measure, and A's proof namesnz.income.standinfornz.calibrate.(2) Coverage (
:825): passes, but it is half the check. Ran: the gate's details list exactly the 6 income rows underincomeand the 4 demography rows undertoy(targets_checked: 10). Ran, with the 6 rows relabelled to familytoyand the gate made diagnostic so the run completes: the gate fails withincome: target profile has 0 match(es), needs 1 for calibration family 'income'.With the gate still release-blocking, the same mutant makesexport.prepare@1refuse, so the fixture errors. The test never asserts the failing case, nor which rows matched (nit 1).(3) Pre-fit (
:839): HOLDS, and the gate really evaluates. Ran:discrepant_bands: []. The failures are exactly the six evidence lines in packaged order.The test pins neither the ratio bounds nor the support numbers (nit 3).
(4) Differential (
:880): HOLDS. For all 6 rows it rebuilds each member fact from the pinned feed, checks the selector equals the declared members, and compares count = Σ members and amount = 11/10 · Σ members at rel 1e-12. I found no vacuous path: the nine member facts are distinct, and the test asserts the feed's member facts equalTOY_STANDIN_FACTS.4. The property: WEAK at zero and the edges
Read (
:949): each of the 7 components isintegers(-150_000, 150_000), with 1–12 records per example. Nothing targets 0 or the aged cuts 16,500, 44,000 and 66,000. Ran, the same strategy at 5,000 examples (about 30,600 records): 13,392 negative, 17,260 positive, x = 0 once (the shrunk all-zero case), and no record on or within one cent of any band edge. Ran: the test itself atmax_examples=3000passes in 86 s. Ran, mutant R1 (B/reference_intervals.py:_position, right closure counted inclusively): all 31 toy tests stay green. 22 shared tests catch it (test_reference_intervals.py,test_nz_reference_intervals.py). Nit 2.5. Changed counts: HOLD
:487:n_targets= 4 demography cells + 2 × 3 bands = 10. The coverage gate'stargets_checked: 10agrees.:240now reads the effective view (4 + 6 active). In the raw resource the 6 rows arerequires_prerequisites(toy_standin_referencessets the prerequisites), so the body's reason holds.6. Mutants
Each ran in place against the toy file and was restored.
members_swapped: red, the differential:880.support_threshold_10: red, prefit:839.table_first_cut_0: red, the property:949._position: every toy test stays green, and the shared interval tests catch it (§4).roster_total_boundsandband_boundsset to[0, 100]andmaximum_discrepant_bands: 99: all 31 toy tests stay green.add_backs: []: all 31 toy tests stay green.status.7. Tests at the head and at M
Ran, feed set,
-n 4.9752cf31: the same three files gave 154 passed, 0 skipped.test_nz_*.pyfile. The 2026-11-04 clock-pin failures don't apply, because the runs finished before 21:05Z on 2026-11-03.8. The bodies: HOLD
Read and Ran.
4ae5810e, and every reference checks::611, :618, :632, :635, :649, :690, :722, :846, :873, :879, :909, and tests:240, :487, :786, :825, :839, :880, :949, :1010. At the head the toy file sits 3 lines lower after:89, because main's Refuse calibration rows that measure a declared holdout #1472 arrives throughd1d72353, which is fine.T/compose.py:610/:716/:4372hold at64ef20af(:4442at the head).T/target_kernels.py:541-551covers_compiled_problemand its refusal at both trees.B/calibration_columns.py:106-107,T/target_kernels.py:679,test_calibration_columns.py:38, :47-60, :101-107, holdout:277at4a36edcf, 257 + 12 = 269 entries, the 106 tests oftest_calibration_columns.py, and 1388 = 41 + 62 + 61 + 4 + 16 + 1203 + 1.test_spec_engine_reemission.pypasses 8/8 here, which supports the body's claim that the hub's one failure came from its venv.4ae5810e(I ran the head instead) and the Belgium patch-ids (Left out).9. The hub's test (
85d55af)Observation: HOLDS (Read).
_check_measurement_proofshas one call,T/compose.py:4442, onskeletonbefore the extension loop.measurement_proofs(:610) reads onlytransport_graph.jsonrows (:633-635). Nothing re-runs either check after the extensions appendnz.v.<V>.calibrate.Definitions (Read). The writer clause is identical to
_check_measurement_proofs(:744-759): an output or an EXPAND cell from_precalibration_expansions, over compiled ancestry. The declarer is equivalent (params.get(measurement_id) == binding). The test differs in three ways:<branch>.income.standin), where the code compares against the skeleton proof's producer, and it never sees variants.calibrate.*kernels, where the code also adds thecalibrationendpoint. Both give nine roots here.The declared-stage
measurement_proofsalso counts an EXPAND with unresolvable cells as a writer of every column (_declared_writer,:596). The compiled check and the test use resolved cells instead.Nine nodes: HOLD (Ran).
N/scenarios.jsonnamesnz.v.{V1,V2,V3,V4,VEX,VHF,VPT,VSI}.calibrate, each with its own.income.standin, plusnz.calibrate.Cache: HOLDS (Read). Only
:879and:901call_nz_composed, and both only read._spec()deep-copies_pristine_spec()(:42-43).Mutants (Ran):
vsi_standin_removed: 1 failed (the declarer assertion,nz.v.VSI.synthetic_measurementagainstnz.v.VSI.income.standin), and the compile test passes.universe_statusoutput (rewrite, nomeasurement_id). The builder's--checkagrees, composition accepts it, the compile test passes, and the hub test fails with('nz.v.VEX.calibrate', 'person.ird_taxable_income_proxy_universe_status'), namingnz.v.VEX.review_rewrite.10. Fix round 1
Fix 3: closes 211's item as specified, with a gap.
test_each_coarse_row_selects_its_native_rows_committed_labels), 1237 passed, 42 skipped, which matches the body.row_59from b01 to b02 in both rows' labels andmetadata.native_rows(counts 9/11): 1238 passed, 42 skipped without the feed. The tiling still reads 50…244 in order. With the feed,test_each_coarse_band_lists_exactly_its_native_rows_labelsandtest_generation_reproduces_committed_resources_with_exact_feed_identitygo red. Nit 5.Fix 4: HOLDS.
row_107upper_cent+100, thenbuild_nz_transport_graph.py, which rewroteearnings_bands.json, and--checkprinted "match". Without the feed: 1 failed (test_each_wage_cut_follows_from_its_committed_label, "row_107"), 63 passed, 2 skipped.--checkagrees): the same test alone goes red,Left contains one more item: 'row_120'.Fix 2 against G7: HOLDS (Ran). I built G7 in
tools/build_nz_entitlement_graph.pyafter the money-normalize loop:nz.v.VSI.reviewis a FILTER with basenz.v.VSI, after the stand-in;nz.v.VSI.review_rewritecopies the stand-in, with every outputrewrite: trueand nomeasurement_id;review, andcalibratekeeps basenz.v.VSI.The builder's
--checkprinted "match". Overtest_nz_transport_declarations.py+test_transport_entitlement_graph.py: 1 failed, 128 passed. Only the hub test fails, with('nz.v.VSI.calibrate', 'person.ird_taxable_income_proxy_cents'),Left contains one more item: 'nz.v.VSI.review_rewrite'.test_nz_packaged_graph_compiles_with_only_pins_and_money_producerspasses, and no other test changes result.R2C-26: HOLDS (Ran). The diff from
4a36edcfchanges only:277, adding"nz_super". The file passes at the head: 41 passed.R2C-27: HOLDS (Read). The recompute lives in
validate_earnings_bands(tools/build_nz_transport_graph.py:384, called from--checkat:1453) and in tests.bands_sha256selectsearnings_bandsatpath: []withencoding: sha256, which is the whole file (N/transport_graph.json:619-623).T/population_kernels.py:11-14saysbands_sha256"is checked for format only: it moves the node key and authenticates nothing". The brief's:13-15is one line off, which is harmless.Rename: HOLDS (Ran).
git diff 64ef20af 044440a7 -- test_transport_composed_graph.pychanges only the function name. The other differences against4a36edcfare G's Belgium-cleared lines.11. Fix round 2: HOLDS
_bindings()filters on"interval" not in entry, which drops exactly the 12 interval entries. At the head,N/calibration_bindings.jsonhas 269 entries, 12 of them interval:ird_taxable_income_standin_band_b01_indicator…_b12_indicator.test_all_active_calibration_bindings_enter_the_small_frame_matrixstill asserts 109 (:268).households,peopleand the fourreceives_*columns. The 24 income rows stayrequires_prerequisites(R2C-18). The test fixes the fixture and hides no fault in G: the prepare kernel passesintervals(T/target_kernels.py:679-681).B/calibration_columns.py:106-107returns{}instead of raising: 2 failed, 104 passed.test_packaged_interval_outputs_need_the_effective_interval_documentgoes red, as required, and the existing fixture testtest_malformed_interval_outputs_refuse[no_document]goes red too._bindingsmutant (Ran). It also dropspopulation_region_01_age_0_14_indicator: 4 failed (test_stale_indicators_are_recomputed_after_inputs_change,…partition_age_and_canonical_tenure…,test_all_active_calibration_bindings_enter_the_small_frame_matrix,test_nonnegative_integer_ages_have_exactly_one_declared_band).test_calibration_columns.py: 9 raiseInterval outputs need the effective interval document., 7 fail with "Regex pattern did not match" (6test_household_roster_refuses_invalid_membership[*]andtest_prepared_columns_cannot_overwrite_a_validated_roster_count), and 1 reads'refused' == 'prepared'(…generated_nz_tables[None]).f43e8fbbfixes all 17: the file passes 106/106 at the head.test_ci_integration_job.py. CI probe: G8-r2c stack's full engine-free failure list (don't merge) #1489's heada0512be9(parent20507ad8) causes them: it setsif: falseon engine-us, engine-uk and integration-uk, and drops--maxfail=1fromtools/run_engine_free_tests.sh. E-G fix round: feed-free label tests, the stand-in rename and R2C-26 #1486 doesn't touch.github, and the file passes 9/9 at the head.12. The closed list and CI
-n 4, over 64 files: the 58 files of G8-r2c E-G: NZ's wage map, coarse income rows, stand-in measure, variants and pre-fit gate #1478's table (frame/frompackages/microcosm-frame, plusuk/test_transport_gate_bindings.py) and the 6 extras. Result: 4866 passed, 50 skipped, 0 failed in 32 m 30 s.test_be_target_references.py32:BE_CHRONICLE_ARTIFACTunset.test_nz_joint_geography.py11:NZ_AS_AREA_SOURCES/NZ_JOINT_GEOGRAPHY_SOURCESunset.test_nz_as_area_crosswalk.py3: pinned ArcGIS bytes.test_nz_axiom_input_closure.py2: Axiom dense engine and the rulespec-nz checkout.frame/test_transport_donor.py2: pinned real donor not cached.ci_test_plan.py verifyprintsverification=ok, andruff checkpasses on the five changed files.f43e8fbb(Ran, check-runs): select-countries, wheels, lint, integration-uk, engine-uk (3.13), engine-uk (3.14), engine-us (3.13), engine-us (3.14), engine-free (3.13) and engine-free (3.14) all report success. Every job had completed.Left out
problem_slices_no_nzd,drop_standin_node,alias_as_problem_artifact,alias_wired_to_problemorone_member_changed, or the hub'svsi_standin_undeclaredandvex_undeclared_rewrite.8e90b9bb380ec92a,3025914a344ef3b3), and I didn't check that G8-r2c E: map NZ wages onto scaled IRD bands with quantile_map@2 #1475's and G8-r2c F: replace NZ's 474 native income rows with 24 coarse stand-in rows #1476's bodies were updated (fix 6).4ae5810eitself.Fixes owed
None block the landing. These are nits, most useful first:
test_nz_target_references.py:1350-1387). Movingrow_59from b01 to b02 consistently in labels andnative_rowspasses every feed-free test, and only the feed audit catches it. Smallest fix: in that test, also assert that each band's first member label starts one dollar above the band's lower cut and its last member label ends at itsupper_cent, read fromird_taxable_income_standin_ty2024.test_nz_transport_toy_graph.py:842-877at the head). Mutant R2 (bounds[0, 100], 99 discrepant bands) leaves it green. Smallest fix: assert that the toy entry'sroster_total_bounds,band_boundsandmaximum_discrepant_bandsequalN/gates.jsonmeasurement_prefit's, as the test already does forrequired_evidence.:952-1010). In 5,000 examples it drew no record on any of the three aged edges, and closure mutant R1 leaves all 31 toy tests green. Smallest fix: add@examples with one component set to 0 and to 16,500, 44,000 and 66,000 (each ±1), all other components 0.transport_nz_toy.py:638-650). Mutant R3 (no rental add-back) leaves every toy test green. Smallest fix: assert that the toy declaration'sfloor,scale,entityandadd_backsequalN/income_measurements.jsonmeasures.central's.:828-839). Smallest fix: assertdetails["matches_by_requirement"]["income"]names exactly the 6 toy rows, so a family relabel turns the test itself red instead of erroring the fixture.Generated by Claude Code
MaxGhenis commentedon Nov 3, 2026
Cloud review session picked up the review of #1501 (G's five test fixes).
Generated by Claude Code
MaxGhenis commentedon Nov 3, 2026
Ready for hub review
VERDICT: APPROVE_WITH_NITS
Needs Max: no
H2 NZS co-residence binding review
I reviewed head
1b56b916daa627715e131cfc22679a1d1bae5dbeon Python 3.13.18. Before I started,pulls/1485printedopenwith this head, andnzhub/g8r2c-g-fix1printedf43e8fbb…. Origin/main had moved to31c89390, so I built M as the head merged withf43e8fbb, then with31c89390. F is M merged with F1 (9b49ef30). All three merges were clean, and I pushed none of them. The pinned feed built:consumer_facts.jsonlhashes to03f5ea0f…andmanifest.jsonto1ea2d1ee…, both as pinned, so I setNZ_CHRONICLE_ARTIFACTfor every run. Dependency sync took about 45 minutes of the time box, and one parallel run hit the container's memory limit (see item 8). The interim is comment 6614519818.1. Scope
git diff --stat df9fce45 1b56b916lists 15 files. By commit:891cb9f1changesconcept_reference.pyandbuild_nz_transport_graph.py:208.9cc37a58changes both mappings, the closure,export_replay_bindings.json,transport_graph.json,reporting_classifiers.json, the method doc, both coverage outputs, the country-spec golden,test_nz_spec_package.py,test_transport_evidence_kernels.py:2734-2735andbuild_nz_transport_graph.py:1271.b78c8ad8changestest_nz_transport_toy_graph.py.1725611cchangestest_transport_evidence_kernels.py:810.1b56b916changes the two mappings' note, the closure's exempt reason, the disclosure, the method doc, the golden,export_replay_bindings.jsonandtest_nz_spec_package.py.git show 891cb9f1 | git patch-id --stableprintse3f8d1c790817d1c3e6677e68fd1f0a564f68af2.F/concept_mapping.py, anyT/*.py, or another sharedsrcor test-support file. The only shared test lines that change aretest_transport_evidence_kernels.py:810and:2734-2735.tools/build_nz_transport_graph.py:1271drops "and the NZS sharing binding (R2C-5)" frompending_outputs. That follows from the binding, because the pending item is the binding this PR adds. The generator writes the same line toN/transport_graph.json:4981, and--checkpasses.2. The law
core.yamlhashes tod8ea0d15…at6fe181fc, at8dc2507dand at today's head710453b3, andN/axiom_input_closure.json:28pins the same hash.nz_super_living_alone_adult_ageis 18 from 2026-04-01, sourced to s 4A(3) and (4) (core.yaml:65-79). The file names it only at its definition, somin_age: 18copies it by hand, as the note says.>=. rulespec'score.test.yaml:24pins the parameter at 18 and doesn't test the boundary.nz_super_living_alone(core.yaml:550-572) reads the input. Only the single branch ofnz_super_ordinary_weekly_rate_before_tax(:664-670) reads that rule.special_rates.yaml:115reads its ownnz_super_living_aloneinput, and NZ's closure holds nospecial_ratesentry, so the binding doesn't reach it.nz_super_principal_residence_is_qualifying_accommodationto true, so every single person reaches the rate's living-alone test, and the binding decides it.nz.json:414-428) holds:core.yaml:567-571;:567requiresnz_super_singleand:664branches on it first;:571makes;:1051-1052isencodedbyconcept_binding, with novalue.:946cites Schedule 1 Part 1 clause 1(b) fornz_super_single_not_living_alone_weekly_rate_before_tax, ascore.yaml:96-111sources it.:111and 647.37 at:95.N/as_rate_bridge.json:462-472addsnz_super_weekly_amount_before_taxwith coefficient 1 to asingle_superannuitantunit.not A or Blets B matter only when A is true.N/reporting_classifiers.json:212-213) and the method doc (docs/nz-reporting-classifiers-method.md:66-71) agree with the closure. Item 7 checks that the disclosure's two evidence pointers resolve.3. The mechanism
Read.
ConceptsEncodeKernel.run(T/column_kernels.py:396-416) callsmapping.encode(:403)._applydispatchesCoresidentAdultto_coresident_adult_flags(F/concept_mapping.py:2231-2232,:2293-2318). That function factorizes the person table's own household ids withuse_na_sentinel=True(:2304-2306)._defaults(:212-232) then callsInputClosure.defaults(F/input_closure.py:520-526), which keeps onlyDEFAULTEDentries, so no default overwrites the bound column.Ran. I ran
ConceptMapping.encodewith the packaged frame mapping on frames I built:InvalidIndexErrorin_Context.__init__The head and F agree in every case. F1 changes
_Context._household_rows,household_amount_for_personsandreference_rows, and_coresident_adult_flagsreads none of them. So the hand-off's conclusion holds: F1's change doesn't reach the small-frame test. Its reason doesn't hold. The test does go through_Context, the class F1 edits, viamapping.encode; "not throughFrame" isn't why. (nit 3)4. The toy table
make_nz_toy_fixtureandrun_entitlement_graph, tookpopulations["nz.create"], and read household composition directly: another row with the sameperson_household_idand age >= 18.test_nz_transport_toy_graph.py:1148-1159). The direct rule gives the same ten values that:1165-1176pins, and the test passes in both runs.5. Closure and pins
git grep -n '791c1cd4\|06ce7ca0'prints nothing. The closure hashes tod2694200…, whichexport_replay_bindings.json:3pins, and the golden's fingerprint is9d0b57a1….--checkpasses with exit 0:build_nz_transport_graph.py,build_nz_benefit_gross_up.pyandbuild_nz_entitlement_graph.py. So doesrefresh_concept_coverage.py --engine axiom-nz --check.git statusstays clean.6. Moved counts
df9fce45and at the head:closure_defaultmoves from 93 to 92 andderivedfrom 322 to 323, because one closure default becomes an encoder output.TestClosurePartitiontests and all 19TestSupportRostertests pass. The roster check includes 158 roots.fact:person.ageand removes it from the uncovered list.7. The tests
test_h2_sharing_binding_turns_the_toy_superannuitants_default_into_true(:1110-1138) is the weakest test.min_agefrom 17 through 65.:1141-1176) holds a single null-id person, so it can't tell "a null id shares with nobody" from "null ids group together".encoded_by, the missingvalue, the exempt default and two phrases. Making it pass vacuously would mean keeping both phrases while changing the meaning.Mutants. I ran each in a separate worktree at the head against the two H2 tests, the builder-parity test, the two disclosure tests,
test_axiom_concept_mapping.py,test_country_spec.py,TestClosurePartitionandTestSupportRoster(201 tests, all green before each mutant). I restored the tree after each one, andgit statuscame back clean.min_age19 in both mappings (Ran). 2 red:test_h2_sharing_binding_on_the_household_shapes_the_toy_lacksandtest_loaded_spec_matches_the_golden_file_byte_for_byte[nz]. The toy test stays green.F/adapters/axiom_concept_mappings/nz.jsonalone (Ran). 5 red:test_packaged_graph_resources_match_their_builders[build_nz_transport_graph.py]and[build_nz_entitlement_graph.py], twoTestClosurePartitiontests (…partitions_as_the_design_tableand…check_equals_input_closure_check_on_the_reduced_surface), andtest_committed_coverage_report_matches_the_surface[nz]. The two new H2 tests stay green, as the hand-off says.min_age65 in both mappings, aimed at the toy test (Ran). 2 red, the small-frame test and the country-spec golden. The toy test stays green, so only the small-frame test pins the age.use_na_sentinel=Falsein_coresident_adult_flags, aimed at the small-frame test's null-id row (Ran). All 201 tests stay green. H1'sTestCoresidentAdult::test_a_null_household_id_shares_a_household_with_nobodyand::test_i7_null_household_ids_leave_the_others_exactgo red. So H1's tests carry this, and H2's tests don't.8. Tests at the head and at M
I ran the hand-off's 42 files with
--dist loadfileand the feed set. The first attempt ran the head and M in parallel at-n 4each, and the container's memory limit killed workers (dmesgshowsoom-kill). The head's run aborted with an xdistINTERNALERROR, and I discarded it. M finished with one crashed worker. I reran the head alone at-n 3.test_calibration_columns.pypasses 106 andtest_transport_holdout_separation.pypasses 41.test_spec_source_citations.py::test_every_country_package_is_scanned, from the crashed worker. Run alone at M, it passes.TestImplementationHashes::test_the_batterys_binding_identity_adds_only_the_leaf_and_its_imports(test_transport_evidence_kernels.py:534, 222 against 197 + 23). It fails the same way at origin/main31c89390alone, and passes at M without that main merge (2d35820c). Main's move since4a36edcfcauses it, not H2. I don't count it against H2, but H2 lands on a main where this test is red.test_calibration_columns.pyandtest_transport_holdout_separation.py::test_packaged_precalibration_stages_pass_the_stage_guard[nz].test_the_batterys_binding_identity_adds_only_the_leaf_and_its_importspasses at the head and fails at M. The cause is main31c89390, as above.test_packaged_interval_outputs_need_the_effective_interval_document,test_each_coarse_row_selects_its_native_rows_committed_labels,test_each_wage_cut_follows_from_its_committed_labelandtest_declared_scenarios_scale_the_standin_cuts_with_their_own_factor.test_declared_scenarios_scale_national_cuts_with_their_own_factor.test_nz_as_area_crosswalk.py:1085,:1096and:1105: "Pinned ArcGIS bytes are distributed separately…"test_nz_joint_geography.py:592,:602,:635×7,:654and:671: "Set NZ_AS_AREA_SOURCES and NZ_JOINT_GEOGRAPHY_SOURCES…"test_nz_axiom_input_closure.py:535and:610: "needs axiom_rules_engine…"test_nz_target_references.py(61),test_nz_reference_aging.py(22) andtest_nz_family_reconciliation.py(16).9. The body and the hand-off
nz.jsonandN/concept_mapping.json:414-428;:946and:1051-1052;:212-213, and the doc at:66-71;:208and:1271, andtransport_graph.json:4981;:3,:46,:53,:59,:83and:94;test_nz_spec_package.py:3238-3261and:3422-3447, andtest_transport_evidence_kernels.py:810and:2734-2735;T/column_kernels.py:212-232,:231and:403,F/input_closure.py:520-526, andF/concept_mapping.py:2232and:2305;as_rate_bridge.json:462-472andprecalibration_stages.json:84-96;core.yaml:65-79,:80-111,:567and:664.core.yaml:550-573, but the rule ends at:572. (trivial)9cc37a58tree, not on the head. My head and M runs cover all 42.Left out
Fixes owed
None.
Nits (none blocks H2):
packages/microcosm-build/tests/engine_free/shared/test_nz_transport_toy_graph.py:1110-1138: the toy test passes withmin_age65 (mutant A). Only the small-frame test pins the adult age. The docstring already says what the toy covers, so this needs no change unless the hub wants the toy to carry the age.test_nz_transport_toy_graph.py:1152: the small-frame test has one null-id person, so mutant B survives H2's tests. H1'sTestCoresidentAdultcatches it. The smallest fix gives person 10 a second null-id co-resident aged 18 or over and still expects False.Frame, so F1's frame change doesn't reach it") give the wrong reason. F1 edits_Context, which the test reaches. The conclusion holds because_coresident_adult_flagsreads the person table's household ids directly (F/concept_mapping.py:2304-2306). The fix is a wording change in the body.31c89390failstest_transport_evidence_kernels.py:534(222 against 220). H2 inherits it once it merges current main.Generated by Claude Code
MaxGhenis commentedon Nov 3, 2026
Ready for hub review
VERDICT: CLEAR_WITH_NITS
Needs Max: no
NZ clearance run on #1488
A cloud session ran NZ's clearance checks on #1488. Base B0 is
20507ad8, head H is3196273c, and the session started at 19:22 UTC. I built the feed under Python 3.13 from Chronicle8f4cfd57. Ran:consumer_facts.jsonl=03f5ea0fe60e…6b18andmanifest.json=1ea2d1eeb122…e053, so both match the pins, and every run setNZ_CHRONICLE_ARTIFACT.T/=packages/microcosm-build/src/microcosm/build/transport/; tests sit inpackages/microcosm-build/tests/engine_free/shared/.The hunk does what #1488 claims on NZ's graphs. NZ's packaged graph composes, NZ's tests pass, and no kernel hash moves. Through NZ's builder, the extension pass refuses G7 (both placements), M2 and M4 at H, and composition accepts each of them at B0. One body claim fails: M1 never reaches the extension pass, because NZ's own test helper inserts a second declarer, and compile then refuses it at B0 and at H alike.
1. Scope
git diff 20507ad8 3196273c --statlists 4 files:changelog.d/transport-extension-measurement-proofs.fixed.md(bdc75bd5),T/compose.py(bdc75bd5),test_transport_measurement_proof.py(bdc75bd5and3196273c) andtest_nz_transport_toy_graph.py(3196273conly).3196273ctouches only the two test files. I found no other file.grep -n 'nz\.' test_transport_measurement_proof.pyfinds nothing. Lines 11–12 and 931 name NZ only in prose, with no node id.2. NZ's tests at H and at B0
-n 4and the feed set. H: 719 passed, 0 skipped, 0 failed. B0: 703 passed, 0 skipped, 0 failed. Neither tree skipped a test.test_composition_proves_each_extension_calibration_producerand the 5…the_declaration_never_seescases). Each exists only at H and passes there. No test that exists in both trees changes result.3. The packaged graph's proofs (H)
_nz_composition_spec_and_config()followed bycompose_transport_graphcomposes. The graph has 9 calibration nodes:nz.calibrateandnz.v.{V1,V2,V3,V4,VEX,VHF,VPT,VSI}.calibrate. The declared proof names onlynz.calibrate. Its proof columns areperson.ird_taxable_income_proxy_centsandperson.ird_taxable_income_proxy_universe_status.ird_taxable_income_ty2027_v1is also the only writer of both columns:nz.calibratenz.income.standin(also the declared producer)nz.v.<V>.calibrate, for each of the 8 variantsnz.v.<V>.income.standin4. NZ's mutants (through
tools/build_nz_entitlement_graph.py)For each mutant I patched the builder, ran it, and ran
--check(which printed "NZ entitlement resources match." every time). Then I composed the graph and rantest_nz_transport_declarations.pyandtest_transport_entitlement_graph.pywith the feed set. The mutants ran in separate worktrees at H and B0. After each one I restored the builder, regenerated the resources, and saw--checkmatch andgit statusprint nothing. The builder is identical at B0 and H, andg7.py's anchor matched unchanged (Ran).test_every_calibration_node_has_its_own_standin_as_the_only_writer: writers['nz.v.VSI.income.standin', 'nz.v.VSI.review_rewrite'])nz.v.VSI.calibratebase →nz.v.VSI.review, which I saw in the generatedscenarios.json)measurement_id)benefits.annual_transfersalso declares)['nz.v.VEX.benefits.annual_transfers', 'nz.v.VEX.income.standin'])benefits.annual_transfers)nz.v.VEX.benefits.annual_transfers); 2 fail (test_every_calibration_branch_has_one_standin_between_transfers_and_prepare, plustest_every_calibration_node_has_its_own_standin_as_the_only_writer: writers of the universe column['nz.v.VEX.benefits.annual_transfers'])The first refusals at H, in full (Ran):
Measurement 'ird_taxable_income_ty2027_v1': the composed graph proves no producer for extension calibration node 'nz.v.VSI.calibrate'; measurement 'ird_taxable_income_ty2027_v1': calibration node 'nz.v.VSI.calibrate' has writers ['nz.v.VSI.income.standin', 'nz.v.VSI.review_rewrite'] of person.ird_taxable_income_proxy_cents upstream; only its producer 'nz.v.VSI.income.standin' may write it.Measurement 'ird_taxable_income_ty2027_v1': the composed graph proves no producer for extension calibration node 'nz.v.VEX.calibrate'; 2 nodes declaring measurement_id 'ird_taxable_income_ty2027_v1' sit upstream of calibration node 'nz.v.VEX.calibrate' (declared by ['nz.income.standin', …, 'nz.v.VEX.benefits.annual_transfers', 'nz.v.VEX.income.standin', …]); exactly one must.Measurement 'ird_taxable_income_ty2027_v1': the composed graph proves no producer for extension calibration node 'nz.v.VEX.calibrate'; its one declarer 'nz.v.VEX.income.standin' does not write ['person.ird_taxable_income_proxy_universe_status'].person.ird_taxable_income_proxy_cents in version 'nz.v.VSI' is owned by both 'nz.v.VSI.income.standin' and 'nz.v.VSI.synthetic_measurement'.(aGraphErrorfromcompile_graph)The tests that fail at H:
test_nz_terminal_filter_keeps_the_skeletons_predecessors_and_ancestry,test_the_s5_guard_refuses_an_undeclared_nz_super_derived_output,test_nz_packaged_graph_compiles_with_only_pins_and_money_producersandtest_every_calibration_node_has_its_own_standin_as_the_only_writer. All four fail on the refusal above.test_every_calibration_branch_has_one_standin_between_transfers_and_prepare, which fails on its own column assertion.GraphError, plustest_every_calibration_branch_has_one_standin_between_transfers_and_prepare(KeyError: 'measurement_id') and bothtest_nz_variant_holdout_selection_refuses_before_calibrationcases (their regex meets theGraphError).Why M1 behaves this way (Read):
add_measurement(test_nz_transport_declarations.py:731-757) checks each calibration scenario. When no row in a branch declares the measurement, it inserts<branch>.synthetic_measurement, which declares the measurement and writes all three stand-in columns. Under M1, VSI's stand-in still writes those columns, socompile_graphrefuses two owners atT/compose.py:4574before_check_measurement_proofs(T/compose.py:4577-4579) ever runs. NZ's packaged graph can't exercise the zero-declarer branch through_nz_composition_spec_and_config. The toy's M1 (test_nz_transport_toy_graph.py:1055) does exercise it.5. The artifact edge
_check_measurement_proofs(T/compose.py:718) to drop the predecessors that each node gets only throughartifact_inputs. I worked these out by compiling the graph again with every node'sartifact_inputsemptied. Under that patch,test_transport_measurement_proof.pyandtest_nz_transport_toy_graph.pygave 79 passed and 1 failed. The one failure is…the_declaration_never_sees[_v3_rewrite_behind_the_problem-…]. This confirms Belgium's claim.nz.v.VSI.targets.problemlets composition refuse NZ's placement. I restored the files, andgit statuscame back clean.6. Kernels
make_composed_fixtureinstalls 45 kernels, and everyimplementation_hashis identical at B0 and H. UnderAxiomEngine, I built the registry withtest_transport_registry.py's_fixtureand the same monkeypatches thattest_native_engines_use_pinned_refs_before_relation_compilationuses. There,simulate.counterfactual@1,simulate.rules_by_ref@1andsimulate.solve_zero@1are identical at both trees. This confirms Belgium's claim. The real Axiom wheel isn't installed here, so theAxiomEnginehashes come from that monkeypatched registry.7. The body
nz.v.V3.calibrate;nz.v.V3.calibrate;compose.pyfail exactly 9 shared tests and NZ's 5 mutant cases (14 failed, 66 passed);_v3_rewrite_behind_the_problemcase;20507ad8and refuses at H, naming the stand-in and the rewrite. NZ's script names the rewritenz.v.VSI.review_rewrite, where the body saysreview.rewrite.test_support/microcosm_build/transport_nz_toy.py:1168-1185).measurement_id| composes | refuses: 0 declarers upstream ofnz.v.VSI.calibrate". The body says it composed through_nz_composition_spec_and_config. Through that path, M1 doesn't compose at20507ad8, and at H the refusal iscompile_graph's ownership error, not 0 declarers (see item 4).test_nz_transport_declarations.py:894-896still says "Composition checks the measurement proof on the skeleton alone … it never seesnz.v.<V>.calibrate". Prove extension calibration nodes' measurement producers in compose #1488 makes that false.8. F1's US worker identity (#1481; doesn't affect the verdict)
3995d92d, at9b49ef30, and at3995d92dwith onlypackages/microcosm-frame/src/microcosm/frame/concept_mapping.pyfrom9b49ef30, and ranuv sync --all-packages --lockedin each. Each ran its own.venv/bin/python -Iwith noPYTHONPATH, andmicrocosm.__path__listed only that worktree's package directories.94429354…75ab, head7b3282ef…db27, mix9448a2cd…77ce. These differ only becauseaudit_aliases(argv_template_0,sys_executable,sys_prefix) embeds each worktree's path.241f9140…dae1e, headbafa81d2…1d55, mixbafa81d2…1d55. The mix output is byte-identical to9b49ef30's.3995d92dto9b49ef30, these fields change:semantic_identity.transitive_environment_code_sha256(27ab80e7…→8d1d4aef…),semantic_identity.worker_module.transitive_imports_sha256(10191fc4…→23701a92…) andsemantic_identity_sha256(48c4fdcd…→59e7064c…). This confirms Belgium's claim thatconcept_mapping.pyaccounts for the whole move.Left out
bdc75bd5.Measurement '…': …; measurement '…': …). B0's skeleton path does the same, so I didn't count it as a finding.Fixes owed
_nz_composition_spec_and_config, M1 doesn't compose at20507ad8. At H,compile_graphrefuses it with… owned by both 'nz.v.VSI.income.standin' and 'nz.v.VSI.synthetic_measurement', not "0 declarers", becauseadd_measurement(test_nz_transport_declarations.py:731-757) inserts a declarer. Smallest fix: Belgium's hub corrects the row, or says how it built M1 so that the shim didn't fire.test_nz_transport_declarations.py:894-896. The docstring says that composition checks the proof on the skeleton alone and never seesnz.v.<V>.calibrate, which is false at H. Smallest fix: the NZ hub (or Prove extension calibration nodes' measurement producers in compose #1488, which already edits NZ's toy file) rewrites that sentence to say that the extension pass now runs the check too.test_nz_transport_declarations.py:731-757(NZ hub).add_measurementsays "until G declares one", but every branch now declares its stand-in, so the shim's only effect is to hide a zero-declarer branch from the extension pass (M1 above). Smallest fix: the NZ hub deletesadd_measurementand its two calls (lines 759 and 763).Generated by Claude Code
Give H2's small-frame test a second null-id adult
MaxGhenis commentedon Nov 3, 2026
Ready for hub review
VERDICT: REQUEST_CHANGES
Needs Max: no
Review of #1501, G's five test fixes
Head reviewed:
92ff931bf57a63a1394f7b3b6eddb80f0410349c(parentf43e8fbb, G's head). The PR's REST state readsopen. I ran Python 3.13.18 afteruv sync --all-packages --locked, withNZ_CHRONICLE_ARTIFACTunset,-p no:cacheprovider, and--basetempin my scratchpad. Paths below are relative topackages/microcosm-build/tests/engine_free/shared/unless they say otherwise.One blocker: the new nit-5 assertion fails at the head (
test_nz_transport_toy_graph.py:842). The gate reports each match with a period suffix (toy_income_standin_amount__b0@2002), and the test compares the matches with bare row names. CI would go red on this file. The other four fixes hold, and each one turns red on its mutant.1. Scope
git diff f43e8fbb 92ff931b --statchanges onlytest_nz_target_references.py(+46) andtest_nz_transport_toy_graph.py(+56/−1). I found no other files.2. Each change matches the PR body
B/nz/income_diagnostic_references.json, the only rows 50–244 whose lower label equals the previous row's upper arerow_60($50,000 - $51,000),row_90($80,000 - $81,000) androw_230($300,000 - $350,000). The test pins them attest_nz_target_references.py:1354and checks them at:1433.B/nz/reference_intervals.json, the tableird_taxable_income_standin_ty2024givesfirst_lower_cut_cents4,000,000. Its finiteupper_cents run from 5,000,000 to 100,000,000, and b12 is open, which matches the tail branch at:1419-1424.Fraction(11, 10)× (15,000, 40,000, 60,000) fromtest_support/microcosm_build/transport_nz_toy.py:610-611. The cuts are exact integers, soint(cut)attest_nz_transport_toy_graph.py:999-1003loses nothing. M3's falsifying example below prints all twelve records.employment_income_cents(transport_nz_toy.py:600). Its add-backs list onlyrental_income_cents(:645).combine_measureadds back only declared add-back columns (B/transport/measure_kernels.py:213-216) and applies no floor. The floor applies only to the unit output (:357-360). So a wage-only record's measure equals its wage, and the edge example reaches the cuts as intended.test_nz_transport_toy_graph.py:870-881,:999-1013,:1041-1061).test_each_toy_coarse_row_enters_the_problem_matrix_once_with_supportalready goes red attest_nz_transport_toy_graph.py:813(assert 'toy' == 'income'). Review 213's "erroring the fixture" doesn't hold either: the fixture builds, and the gate passes.3. Both files at the head
test_nz_target_references.py: 20 passed, 0 failed, 41 skipped. Every skip gives the reason "NZ_CHRONICLE_ARTIFACT is unset; public NZ feed audit unavailable" (lines 356, 464, 584, 836, 847, 866, 901, 918, 961, 989, 1041, 1071, 1100, 1138, 1159, 1179, 1229 and 1312).test_nz_transport_toy_graph.py: 31 passed, 1 failed, 0 skipped.test_target_profile_coverage_passes_on_the_toy_with_income_requiredfails at:842::836). The PR body says the hub didn't run this file, which explains the miss.4. Mutants
I ran each mutant in place and restored it with
git checkout -- <file>.git status --shortcame back empty after each one.row_59from b01 to b02 for people and totals, in the labels and inmetadata.native_rows. Onlytest_each_coarse_row_selects_its_native_rows_committed_labelsgoes red, attest_nz_target_references.py:1431(assert 4900000 == 5000000, b01). All other tests stay green. Caveat: my first attempt leftexpected_member_countat 10, and that alone turned eight other tests red through aValueError. Review 213's mutant passes those tests, so I also moved the counts (b01 to 9, b02 to 11). The figures above come from that run.[0, 100]andmaximum_discrepant_bandsto 99.test_measurement_prefit_fails_the_toy_only_on_its_unwired_evidencegoes red at:879(roster_total_bounds: assert [0, 100] == [0.75, 1.25]). The coverage test stays red at:842, as it is at the head._cuts_at_or_below(..., inclusive=closure == "left")toinclusive=True(B/reference_intervals.py:701). The property test goes red at:1031(assert np.int64(1) == 0) on the explicit example::842, as it is at the head. I also ran M3 against three other interval files andtest_nz_target_references.py:test_nz_reference_intervals.pygoes red in 8 tests:test_property_the_scaled_coarse_table_tiles_the_positive_range, andtest_shared_endpoint_labels_resolve_to_the_ordered_cutsfor 7 cases (age 20,000, 50,000 and 80,000; national 20,000, 50,000, 80,000 and 300,000).test_reference_intervals.pygoes red in 13 tests:test_a_table_starting_above_zero_reports_records_below_its_first_cut[False-right] and [True-right],test_each_exact_cut_and_one_cent_either_side[right-expected0],test_first_positive_band_and_open_tail_under_both_closures[right-below-b1-b2],test_floored_negative_mass_reports_signed_amounts_by_actual_destination(2 cases),test_fractional_cent_aged_cuts_stay_exact_and_are_never_rounded,test_household_entity_table_is_weighted_through_memberships,test_left_closed_nil_zero_first_cut_diagnostics_keep_zero_in_nil, and four property tests.test_reference_interval_support.pyandtest_nz_target_references.pystay green."add_backs": [].test_the_toy_standin_measure_keeps_the_packaged_central_rulesgoes red at:1058(add_backs: assert [] == ['rental_income_cents']). The coverage test stays red at:842, as it is at the head.toy_income_standin_amount__b2to familytoyintoy_standin_references. The coverage test goes red at:842, past:836, so the gate still passes. It also failed before the mutant, though, so at the head the assertion can't tell M5 apart from the suffix bug.test_each_toy_coarse_row_enters_the_problem_matrix_once_with_supportalso goes red at:813.[name.partition("@")[0] for name in …["income"]] == sorted(…)). The coverage test then passes at the head and goes red on M5 at:842(Right contains one more item: 'toy_income_standin_count__b2'). I restored the file afterwards, andgit statusis clean.5. Lint
uv run --no-sync ruff checkprints "All checks passed!" on both files.ruff format --checkprints "2 files already formatted".Left out
NZ_CHRONICLE_ARTIFACTis unset. I ran no package-wide suite, and I ran M1, M2, M4 and M5 against the named file only.92ff931bthrough REST, so I can't confirm what CI reports.Fixes owed
test_nz_transport_toy_graph.py:842). The gate'smatches_by_requirement["income"]names rows with a@2002period suffix, and the test compares them with bare row names, so the toy file goes red. Smallest fix: compare[name.partition("@")[0] for name in row["details"]["matches_by_requirement"]["income"]], or build the expected names with the@2002suffix. I ran the first form: it passes at the head and catches M5.test_each_toy_coarse_row_enters_the_problem_matrix_once_with_support(test_nz_transport_toy_graph.py:813) already catches a family relabel. Smallest fix: reword item 5 to say that the new assertion pins the gate's own match list.Generated by Claude Code
MaxGhenis commentedon Nov 3, 2026
Cloud review session picked up the delta review of #1501 and #1485.
Generated by Claude Code
MaxGhenis commentedon Nov 3, 2026
Ready for hub review
VERDICT: #1501 APPROVE; #1485 APPROVE_WITH_NITS
Needs Max: no
Delta review of #1501 and #1485
Heads reviewed: #1501 at
8edd52a3b62a0ccfbaa4f2655b0f289a28a70bd6(parent92ff931b) and #1485 atd82a2ba5aa4c2a00a38e52a4e84dab64b85bc418(parent1b56b916). Before I started, the REST API printedopenand the expected head for both PRs. I ran Python 3.13.18 afteruv sync --all-packages --locked, withNZ_CHRONICLE_ARTIFACTunset,-p no:cacheprovider,--basetempin my scratchpad, anduv run --no-sync.Tbelow meanspackages/microcosm-build/tests/engine_free/shared/test_nz_transport_toy_graph.py.Both commits do what their reviews asked. Each new assertion passes at its head and goes red on the mutant it targets. #1485's body has one small gap: its small-frame table doesn't list person 11.
A1. #1501 scope
git diff 92ff931b 8edd52a3 --statchanges onlyT(+4/−2). The hunk sits insidetest_target_profile_coverage_passes_on_the_toy_with_income_required. It extends the comment (T:837-840) and rewrites the assertion (T:843-848) to comparesorted(name.partition("@")[0] for name in matched)with the six stand-in row names. Nothing else changes.A2. #1501: the file at the head
Tgives 32 passed, 0 failed, 0 skipped in 151 s. Review 221's failure at:842is gone.ruff checkprints "All checks passed!" andruff format --checkprints "1 file already formatted".A3. #1501: M5
test_support/microcosm_build/transport_nz_toy.py:705, I setfamily="toy"fortoy_income_standin_amount__b2only. The coverage test goes red atT:844, so the gate-status assertion atT:836passed first:git checkout -- test_support/microcosm_build/transport_nz_toy.py, andgit status --shortprinted nothing.A4. #1501: the comment's claim
target_profile_coveragebinding is_target_profile_coverage(packages/microcosm-build/src/microcosm/build/transport/gate_bindings.py:789-823). It takesnames = tuple(problem.problem.names)(:803), builds one{"name": str(name), "family": …}target per row (:807-810), and passes them totarget_profile_coverage_gate(:819). So the gate receives the problem's row names._compiled_problem(…/transport/target_kernels.py:552-554) buildsexpected = tuple(f"{spec.name}@{spec.period}" …)and raises unlessproblem.names == expected. So every row name readsname@period.@recovers the reference name, because the registry appends exactly one@{period}and the stand-in names contain no@. The@2002matches that review 221 printed agree.B1. #1485 scope
git diff 1b56b916 d82a2ba5 --statchanges onlyT(+11/−6). Every hunk sits insidetest_h2_sharing_binding_on_the_household_shapes_the_toy_lacks(T:1141-1181):person_idruns 1–11;person_household_idand age 30;beforeexpectsrange(1, 12)all False (T:1168);afteradds11: False(T:1180).B2. #1485: the file at the head
Tgives 33 passed, 0 failed, 0 skipped in 138 s.ruff checkandruff format --checkboth pass.B3. #1485: mutant B
use_na_sentinel=Falsein_coresident_adult_flags's factorize call (packages/microcosm-frame/src/microcosm/frame/concept_mapping.py:2305). The small-frame test goes red atT:1169. Thebeforeassertion atT:1168passed first:git checkout --, andgit status --shortprinted nothing.B4. #1485: the body
Read: The "Review:" paragraph matches review 215 on these points:
1b56b916and comment 6616135497.Ran: For nit 4,
pulls/1495readsclosed, merged at2026-11-03T21:22:52Z. Its title is "Count the two modules Publish calibration diagnostics as schema 9, each hierarchy label declared once #1342 added to the transport bindings' identity…". That supports the body's "Publish calibration diagnostics as schema 9, each hierarchy label declared once #1342 … Count the two modules #1342 added to the transport bindings' identity, and merge main's US re-pin (#1496) #1495 fixed it on main at 21:22Z 11/03".Read: The corrected reason holds.
_coresident_adult_flags(concept_mapping.py:2293-2318) factorizescontext.person[_PERSON_HOUSEHOLD_ID](:2304-2306) and readscontext.values("fact:person.age")(:2307). It never touches_household_rows,household_amount_for_personsorreference_rows.Ran:
git diff 3995d92d 9b49ef30(F1's head on Give a person with no household no household's row, and read malformed holdout rows alike in both guards #1481, against its merge base) changesconcept_mapping.pyin exactly three places:_Context.__init__now sets_household_rowsto −1 for null ids (:2096-2098at F1);household_amount_for_personsgains an appended-NaN read;reference_rowsgains an appended −1 read.These are the three members the body names. F1's base blob for the file (
13790700) is H2's blob atd82a2ba5.Read (nit): The body's small-frame table, under "The toy, before and after", stops at person 10. The next sentence says "the small-frame test pins every row above". The paragraph above that sentence mentions person 11, but the table has no row for it.
Left out
NZ_CHRONICLE_ARTIFACTis unset. I ran each mutant against its named test only, not against the whole file.Fixes owed
None.
Nit (doesn't block): #1485 body, under "The toy, before and after": add a row
| 11 | none | 30 | none | false | false |to the small-frame table, so the table covers every row thatT:1169-1181pins.Generated by Claude Code
MaxGhenis commentedon Nov 4, 2026
Cloud review session picked up the NZ QES September 2026 re-declaration review.
Generated by Claude Code
MaxGhenis commentedon Nov 4, 2026
Interim: NZ QES September 2026 re-declaration review
Head reviewed:
b44229cd4b8076854196440cd86b0790699c8f42(stateopen, checked through the REST API).Feed (Ran). I built Chronicle
89178aaf's NZ feed under Python 3.13.consumer_facts.jsonlhas 12,370 rows and SHA-2568580143f…35fb5, andmanifest.jsonhasea0cc7ef…24a6. Both matchnz/chronicle_feed.json.Feed identity (Ran).
reference_aging.json(21 identity blocks),reference_inventory.jsonandmsd_pre_activation_reconciliation.jsoneach carry the same seven fields aschronicle_feed.json. The goldennz_country_spec.jsoncarries no identity block. It pins every resource's hash, and all 54 hashes match the files on disk. The generator--checkpasses forgenerate_nz_reference_aging,generate_nz_target_references,generate_nz_msd_pre_activation_reconciliationandbuild_nz_transport_graph, each exiting 0.Operands (Ran). I filtered on
stats_nz/SBSZ9A, the five dimensions andquarter. Each of the 13 operand quarters resolves to exactly one fact. Each fact's value, key and line number equal its_OPERANDSrow, and eachsource_sha256follows the vintage ruling: 25053dcb for 2023-Q2 to 2024-Q1, 1af2e7e3 for 2024-Q2, and d6fb9a94 from 2024-Q3 on.Factors (Ran). I recomputed all ten factors with Decimal at 60 digits. All ten equal
_EXPECTED,reference_aging.jsonand the PR table. The nearest rounding boundary sits 0.094e-6 fromp1_high's unrounded value.Window (Ran).
require_current_factorsrefuses 2026-11-03, accepts 2026-11-04 and 2027-02-02, and refuses 2027-02-03.Mutants (Ran). Each mutant turned
test_nz_reference_aging.pyred:_EXPECTEDp1_central +1e-6: "expected factor mismatch … re-declaration is required"_EXPECTED+1e-6:test_root_enclosure…failsI restored every file afterwards, and the tree is clean.
Schema v5 (Ran/Read). The pinned schema SHA-256
acd33a09…equals Chronicle'sconsumer_fact.v5.schema.json. NZ accepts rows through the exact pin (ledger_artifact.py:286-295), not through the epoch table.The test runs and the diff against the old pin are still in progress.
Generated by Claude Code
MaxGhenis commentedon Nov 4, 2026
Ready for hub review
VERDICT: APPROVE_WITH_NITS
Needs Max: no
NZ QES September 2026 re-declaration review
Head reviewed:
b44229cd4b8076854196440cd86b0790699c8f42.pulls/1507returnedopenand this SHA. I ran everything under Python 3.13.18 withNZ_CHRONICLE_ARTIFACT=$HOME/nz-feed/artifact. The working tree was clean at the end.Factors
I recomputed the factors from my own artifact's values with
decimalat 60 digits, using N = (Jun26 + Sep26·(1+q+q²))/4, q = (1+g)^(1/4) and half-even rounding to six digits. I didn't use the generator. Distance is the gap between the unrounded value and its nearest rounding boundary, in millionths._EXPECTED/reference_aging.json/ PR tablep1_highsits closest to a boundary, at 0.094e-6. Even that is several orders of magnitude above any arithmetic error at 60 digits.1. Feed identity
Ran (
~/chronicleat89178aaf): I built the feed under Python 3.13.consumer_facts.jsonlhas 12,370 rows and SHA-2568580143f…35fb5, andmanifest.jsonhasea0cc7ef…24a6. Both match on the first build, so the 3.14 rerun wasn't needed.Ran (
nz/chronicle_feed.json:6-15): the seven identity fields match my build.reference_aging.jsoncarries 21 identity blocks (top level plus each declaration and receipt).reference_inventory.jsonandmsd_pre_activation_reconciliation.jsoncarry one each. All 23 equal the pin.nz_country_spec.jsoncarries no identity block. It pins resource hashes instead, and all 54 match the files on disk, includingchronicle_feed.json(598f4f50…).Ran: each
--checkexits 0 against my artifact:tools.generate_nz_reference_agingtools.generate_nz_target_referencestools.generate_nz_msd_pre_activation_reconciliationtools.build_nz_transport_graph("NZ transport graph resources match the generator.")Ran (
nz/earnings_bands.json): each profile'soperational_factorequals its P1 entry inreference_aging.json.build_nz_transport_graph --checkandtest_nz_earnings_bands.pyconfirm the receipt hashes:18bb7590…492c93a1…7d4e0a99…2. Operands
Ran (
tools/generate_nz_reference_aging.py:30-44): my filter usedsource_name=stats_nz,source_measure_id=SBSZ9A, the five dimensions andperiod.type=quarter. Each operand quarter matches exactly one fact, and each fact's value, key and line number equal its_OPERANDSrow. Thesource_sha256values follow the vintage ruling:25053dcb1af2e7e3d6fb9a94…26f32025-Q1 (12042, 1637.20) also matches one fact; no factor uses it. 2026-Q3 holds 1714.86 at line 12054.
Read (
tools/generate_nz_reference_aging.py:85-91):_qes_hashimplements the same rule.3. Factors
Ran: see the table above. All ten agree across my recomputation,
_EXPECTED,reference_aging.json, the PR table and the changelog fragment.Ran: the derived pins also agree:
docs/nz-calibration-targets.md:167-169: 40,000 × 1.118491 = 4,473,964 cents, and 125,000 × 1.118491 = 13,981,137.5 cents.test_nz_reference_intervals.py:345: 50,000 × 1.118491 = 5,592,455 cents.test_nz_transport_declarations.py:1137: 50,000 × 1.029853 = 51,492.65 and 51,000 × 1.029853 = 52,522.503.4. Window
Ran (
reference_factors.py:747-766on the packagedreference_aging.json):require_current_factorsrefuses 2026-11-03, accepts 2026-11-04 and 2027-02-02, and refuses 2027-02-03. Each refusal names all ten factors.Read (
transport/cli.py:708-710):_todayuses the UTC date.Read (
test_nz_reference_aging.py:117-133): the test pins the same four dates.Inferred: Stats NZ publishes at 10:45 NZDT on 2027-02-03, which is 21:45 UTC on 2027-02-02. Run entry therefore still accepts the factors for about two hours after the December quarter is public. The ruling chose this, and the previous window (2026-11-03) had the same gap. I didn't verify the release time, and I'm not flagging this as a fix.
5. Re-anchoring and rounding tests
Read (
test_nz_reference_aging.py:262-307):test_december_observation_reanchors_and_requires_reviewed_redeclarationfails for the reason its name claims.pytest.raises("expected factor mismatch.*re-declaration")would not fire.Read/Ran (
test_nz_reference_aging.py:225-237):test_p1_rounds_once_and_the_truncated_numerator_refusesrefuses the truncated numerator 1029852 for the right reason. It does not exercise the "rounds once" half of its name.p1_centralunder three intermediate-rounding schemes: N rounded to cents, the projected quarters rounded to cents, and q rounded to six digits. All three still give 1029853.test_all_nine_factors…catches intermediate rounding for seven of the nine projected factors, so the suite still covers it.p1_lowwould cover both halves: truncation gives 1028903 and N rounded to cents gives 1028901. Nit 1 below.Ran (mutants on
test_nz_reference_aging.py, each restored withgit checkout, tree clean afterwards):_EXPECTED["p1_ty2026_to_ty2027_central"]changed to 1029854: red. The generator raisesValueError: Declared expected factor mismatch: computed {'numerator': 1029853, …}; re-declaration is required., which failstest_generated_aging_resource_reproduces_pinned_feed_and_all_receipts.Reviewed QES operand changed for 2026-Q3, andtest_each_reviewed_operand_resolves_once_with_exact_lexical_evidenceasserts'1714.86' == '1714.87'._EXPECTEDchanged to 1029854: red (test_root_enclosure_certifies_no_rounded_quarters_and_irrational_root).6. Tests
Ran each file on its own with
-p no:cacheprovider,--basetempin the scratchpad and the feed exported:-k nzAll 11 skips have the same reason: "Set NZ_AS_AREA_SOURCES and NZ_JOINT_GEOGRAPHY_SOURCES for pinned-source replay". They come from
test_nz_joint_geography.pylines 592, 602, 635 (×7), 654 and 671. No skip namesNZ_CHRONICLE_ARTIFACT. The counts match the PR body's table.7. Scope
Ran (
git diff origin/main...HEAD --name-only; merge basee2918f4f=origin/main):changelog.d/1507-nz-qes-september-2026-redeclaration.changed.md(new fragment)docs/nz-calibration-targets.mdpackages/microcosm-build/src/microcosm/build/nz/chronicle_feed.jsonpackages/microcosm-build/src/microcosm/build/nz/earnings_bands.jsonpackages/microcosm-build/src/microcosm/build/nz/msd_pre_activation_reconciliation.jsonpackages/microcosm-build/src/microcosm/build/nz/reference_aging.jsonpackages/microcosm-build/src/microcosm/build/nz/reference_inventory.jsonpackages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.pypackages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.pypackages/microcosm-build/tests/engine_free/shared/test_nz_reference_intervals.pypackages/microcosm-build/tests/engine_free/shared/test_nz_target_references.pypackages/microcosm-build/tests/engine_free/shared/test_nz_transport_declarations.pypackages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py: only the three_todaypins move, 2026-10-24 to 2026-11-05 (lines 2804, 2815, 3536).packages/microcosm-build/tests/golden/nz_country_spec.jsontools/generate_nz_reference_aging.pyEvery path falls inside the allowed set. The PR changes no runtime code: the only
src/changes are NZ JSON resources.8. Derived pins
Ran (
git grep -wover the nine old factors in integer and decimal form, 4,468,436, 5,585,545, 13,963,862.5, 51,429.0 and 52,457.58). Every hit stays put:test_money_normalization.py:178,572-587,876-879andtest_reference_intervals.py:38,369-384,534-538,642,807are shared tests that use the old factors as synthetic sample inputs and read no NZ resource. They should stay.test_nz_reference_intervals.py:344andtest_nz_transport_declarations.py:1109are historical comments ("design 80 gave …"). They should stay.No doc, resource or NZ test still pins a retired value.
Ran: I searched the repository for the 22 old receipt and declaration hashes from
main'sreference_aging.json, and none appear anywhere. A 23rd old hash, the v4 schema digestd953306d…, survives only in the BE and UK pins andtest_uk_chronicle_feed.py, which are those countries' own feeds and correct.9. Schema v5
Read (
ledger_artifact.py:464-470): the loader carries each row'sschema_versionand never gates on it.Read (
ledger_artifact.py:257-295):validate_chronicle_feed_pinaccepts the artifact only when its observedconsumer_fact_schema_versionsequal the pin exactly, so NZ accepts v5 through the reviewed pin.Read (
chronicle_epoch.py:216-219, 328-330, 547-549):ACCEPTED_CONSUMER_FACT_SCHEMA_VERSIONSdeclares v1–v4 and not v5.is_accepted_consumer_fact_schema_versionandconsumer_fact_schema_epochhave no caller outsidechronicle_epoch.pyand its tests. No NZ code path accepts or refuses a fact by its rowschema_version. Whether the epoch table should declare v5 is outside NZ's scope. See Nit 2.Ran: the pin's
consumer_fact_schema_sha256(acd33a09…6f970) equals the SHA-256 of Chronicle89178aaf'spolicyengine_chronicle/schemas/consumer_fact.v5.schema.jsonanddocs/schemas/consumer_fact.v5.schema.json. It also equals the manifest's own declaration.Read/Ran (
groupby_value_label):ledger_targets.pyreads it at three places. Two of them strip whitespace (:1524,:1596/:1650)._ledger_metadata(:5905) copies it raw.stats_nz.hnw_2024.table5.01.estimates) appears in any NZ resource,tools/*nz*file ortest_nz_*.py, so the change reaches nothing NZ resolves.Ran (old-pin diff): I built Chronicle
8f4cfd57's NZ feed. It reproduces the old pin: 12,368 rows,03f5ea0f…/1ea2d1ee…. Diffed byaggregate_fact_key:schema_version. 16 differ inschema_versionpluslayout.groupby_value_label('One-person household 'becomes'One-person household'). Those 16 are stats_nz household net worth 2023 facts: median, mean, total and household_count, four of each.stats_nzfrom1af2e7e3(June 2026).stats_nzfromd6fb9a94(September 2026).This matches the hub's account exactly.
Left out
Fixes owed
None blocking. Two nits, the first most relevant:
packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py:225-237): the test's name claims "rounds once", butp1_ty2026_to_ty2027_centralcan't tell single rounding from intermediate rounding. N rounded to cents, the projected quarters rounded to cents and q rounded to six digits all still give 1029853, so the test only refuses truncation. Smallest fix: switch top1_ty2026_to_ty2027_lowand set its numerator to the truncated 1028903 (N rounded to cents gives 1028901). Alternatively, rename the test to drop "rounds once".test_all_nine_factors…already covers intermediate rounding for seven of the nine factors, so nothing is at risk now.packages/microcosm-build/src/microcosm/build/chronicle_epoch.py:206-219): the epoch table doesn't declarechronicle.consumer_fact.v5, though Chronicle has emitted it since chronicle#377. Nothing NZ runs consults the table, so this PR owes nothing. The module's owner can add the row whenever the table should match Chronicle's again.Generated by Claude Code
Fix round for the income evidence producers (WIP, generated files fol…
MaxGhenis commentedon Nov 10, 2026
Hub ruling R2C-29: real producers for
measurement_prefit's six evidence aliases (G8-x1, PR #1555)R2C-18 says to wire an alias only where a graph node really produces that artifact. This ruling names those nodes. It changes no R2C-3 number and adds no threshold. The gate still checks presence by alias, so each producer must refuse, or carry the run's real content, on its own.
scenario_vsi,scenario_vex,scenario_vptnz.v.{VSI,VEX,VPT}.gap, existingtakeup.gap@1, through a newgate_evidencekey in the entitlement graphidentity_receiptsnz.evidence.income_identity(transport.income_identity@1)nz.terminalomission_registernz.evidence.omission_register(transport.omission_register@1)nz.terminalnative_band_reportnz.evidence.native_bands(transport.native_band_report@1)nz.terminalnz.centralwould joinnz.calibrate's proof and its key. Atnz.terminal, their params and kernel sources stay out of that key.measurement. Only themeasure.combine@1stand-ins declaremeasurement_id.identity_receiptschecks four identities on every record of the central measure:component_sum,unit_rule,universe_partitionandint64_range, in Python integers, over the declarationmeasure.combine@1's validator parses. A failure raises and names the record, so the artifact exists only when every identity holds. The add-back totals follow fromcomponent_sum, so the artifact reports them and checks no identity of their own.omission_registerbinds the declared items verbatim, sizes as declared, beside the included totals at pre-fit weights. Those weights come from the calibration problem, each person at their household's, and the register digests them as the native report does.native_band_reportsets the model, at pre-fit and post-fit weights, beside each native row. The native rows must tile the coarse bands, by cuts and by facts, or the kernel refuses. It stays diagnostic (R2C-4).The hub drafted this ruling as R2C-28 on 11/09 and renumbered it, because comment 6601354890 already used that number. #1555 implements it and landed as 5509c92 on 2026-11-10; #1563 tracks three follow-ups from its reviews.