Skip to content

G8-r2c D: per-band support diagnostics and the measurement_prefit gate - #1457

Merged
MaxGhenis merged 22 commits into
mainfrom
nzhub/g8r2c-d-prefit
Nov 3, 2026
Merged

MaxGhenis merged 22 commits into
mainfrom
nzhub/g8r2c-d-prefit

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

G8-r2c shared hunk D (tracking #1183): per-band support diagnostics and a release-blocking measurement_prefit gate (ruling R2C-3), with ruling R2C-12's line in country_spec.py. Stacked on hunk A (#1456, nzhub/g8r2c-a-summed-bands); the two share one re-key window.

B/ is packages/microcosm-build/src/microcosm/build/, T/ is B/transport/, F/ is packages/microcosm-frame/src/microcosm/frame/, and tests are in packages/microcosm-build/tests/engine_free/shared/. af038d04 merges A's EXPAND writer fix d916ea8d into D, which moves T/compose.py lines after :591 down by up to 17. 91bf4c0f then merges A's bf418af1, test lines only, and changes one number in a test docstring. Other lines are at 4c644db7, after the fix round. The round changed no src line in D, so every src line number below still holds.

The change

Per-band support (B/reference_intervals.py). A table that declares the optional diagnostics.support_household_entity gets, per band, the distinct source households and the source-household Kish ESS, for count and amount contributions separately (band_support, :1027; kish_ess, :1010).

  • A record contributes w to its band's count and w × z to its band's amount. Here w is the record's weight as the diagnostics read it, the frame's weights on the table's entity, which are the weights entering calibration, and z is its floored amount in cents. So an amount contribution's weight is w × z, the record's share of the band's weighted amount.
  • The code sums contributions per source household before the Kish ratio, so donor clones of one household add no support. A household counts only where its summed contribution is nonzero, and an empty band reports 0 households and an ESS of 0.
  • Reaching the household (record_source_households, :1077). The frame declares membership only through the person table: each group entity has a person column EntitySchema.membership_column(group) (F/schema.py:138), which Frame._group_positions reads (F/bundle.py:649). A person reads its household column directly. A unit on another entity, such as a tax_unit, reads its members' households and must nest in exactly one, as Frame._aggregate_group_to_group requires when it places one group onto another (F/bundle.py:1070). So two tax units in one household count as one source household. The code counts source households, not units.
  • It refuses by name an entity with no household membership: a household entity the frame doesn't declare, an entity that isn't declared, or a unit with no members or with members in several households. It also refuses a frame without household.source_household_id.
  • Backward compatibility. A table that declares none of D's fields parses, classifies and reports exactly as before: boundary_diagnostics refuses source households for such a table, and its report carries no support key (:801, :933-945, :995).

The gate (T/gate_bindings.py:1664, _measurement_prefit; binding at :2044). It reads the problem artifact and the battery's reserved graph_evidence map. Every threshold comes from the country's gates.json entry, and none has a default. A country without the entry gets no gate, because the battery evaluates only the entries its manifest lists (B/gate_battery.py:705). The gate refuses on R2C-3's four checks:

  • Roster totals (:1803). The gate sums the model total over every band's count row, and separately over every band's amount row, across all the entry's declared tables together (NZ declares one table, with 12 bands), and compares each sum with the summed reference. The model total is the problem's A @ w0 at the weights entering calibration (problem.estimates(initial_weights), :1726), and the reference is the matrix's right-hand side, the aged reference the matrix uses (target_vector, :1727). It refuses outside roster_total_bounds, inclusive: with R2C-3's [0.75, 1.25], exactly 75% and 125% pass.

  • Concentrated discrepancy (:1809). A band is discrepant if its count or its amount falls outside band_bounds, and it counts once if both fail. The gate refuses when more than maximum_discrepant_bands bands are discrepant. R2C-3's "3 or more refuse" is maximum_discrepant_bands: 2.

  • Support (:1777, :1782). It refuses a band with fewer than minimum_source_households source households, or a Kish ESS below minimum_kish_ess, checked for count and amount separately. Exactly 30 passes.

  • Missing evidence (:1714 onward). Today the gate checks:

    • that each alias in required_evidence is wired into the battery as an artifact edge (graph_evidence);
    • that the problem carries interval diagnostics for each declared table, with per-band support;
    • that each band's count and amount references entered the matrix.

    So of R2C-3's evidence items, the native-band report, the omission register, the VSI, VEX and VPT runs and the identity receipts are checked for presence only, by the alias the country declares. The gate doesn't read their contents.

R2C-12. B/country_spec.py:134 adds "measurement_prefit" to ALLOWED_GATE_FUNCTIONS. That's the only line of that file this PR touches. The test that pinned the loader's refusal is gone, and test_a_gates_json_entry_naming_the_gate_loads loads a gates.json entry naming the gate.

R2C-3's numbers are pinned in test_support/microcosm_build/reference_intervals.py as R2C3_PREFIT_THRESHOLDS, and test_r2c3_thresholds_are_pinned pins them for the NZ session to reuse. NZ's own entry belongs to the NZ session.

Invariants

Invariant How it is tested
Source-household Kish ESS ≤ distinct source households, and a band with none has ESS 0 Hypothesis, test_ess_is_at_most_distinct_households_and_clones_leave_it_unchanged: 7 source households over a nil band and 3 positive bands, so it reaches shared households and empty bands
Cloning records within a source household leaves per-band support unchanged The same property. Each original record carries clones × w and each clone carries w, so the comparison is exact on every example
The diagnostics' weighted band counts equal Σ w × indicator over the prepared columns Differential, test_weighted_band_counts_equal_weighted_indicator_sums, against interval_indicators
Two units in one household count once, with Kish ESS over source households test_two_tax_units_in_one_household_count_once_with_source_household_ess, test_clone_households_share_one_source_household
A household whose summed contribution is zero adds no support test_a_zero_weight_household_is_not_counted
The model side is the matrix at the weights entering calibration, A·w₀, not at unit weights test_the_model_side_is_the_matrix_at_the_initial_weights; the test problem's estimates multiplies a matrix by the weights it receives, with w₀ = (2, 4)
A table without D's fields reports as before test_a_table_without_support_fields_parses_and_reports_as_before
Each threshold refuses just outside and passes at its edge, on each side test_roster_totals_refuse_outside_75_to_125_percent, test_three_discrepant_bands_refuse_and_two_pass (count-only and amount-only failures, 2, 3 and 4 bands), test_band_bounds_are_inclusive_at_50_and_200_percent, test_a_band_failing_count_and_amount_counts_once, test_support_refuses_below_30_households_or_ess_separately, test_missing_evidence_refuses_each_way

Mutants

Mutant Red
Each record is its own household (no grouping) 4 tests, among them the clone property and the two-tax-unit test
Amount contribution weight w instead of w × z test_two_tax_units_in_one_household_count_once_with_source_household_ess
Accept a unit with members in two households test_an_entity_without_household_membership_refuses_by_name[frame0-None-None]
Drop the backward-compatibility check test_a_table_without_support_fields_parses_and_reports_as_before
Household floor 29 instead of 30 4 cases of test_support_refuses_below_30_households_or_ess_separately. This mutant survived the pushed test, whose 29-household case also had ESS 29; the test now checks which check refused.
ESS floor <= instead of < the exact-30 cases
Drop the totals check the 74.99% and 125.01% cases
Concentrated rule >= instead of > the 2-band pass cases and test_a_band_failing_count_and_amount_counts_once
Exclusive band bounds 8 cases, among them test_band_bounds_are_inclusive_at_50_and_200_percent[0.5-True]
Drop the evidence-alias check test_missing_evidence_refuses_each_way
Remove R2C-12's line test_a_gates_json_entry_naming_the_gate_loads
Fix round OD1: count a household whose contribution is zero test_a_zero_weight_household_is_not_counted
Fix round OD2: only the count role marks a band discrepant the 6 amount cases of test_three_discrepant_bands_refuse_and_two_pass
Fix round OD3: evaluate the model side at unit weights test_the_model_side_is_the_matrix_at_the_initial_weights and the edge cases that pass at w₀

At 4c644db7 I reran two of the mutants above: no grouping (5 tests red, among them test_two_tax_units_in_one_household_count_once_with_source_household_ess and the clone property) and concentrated >= (test_a_band_failing_count_and_amount_counts_once and the four 2-band pass cases).

Merge-up

  • 7bc34fdb merges A's merge-up 923b4de1, which carries w4d fcdb47395293955347ec2697feef1a736a76cc23, into D. Git merged without conflict. git diff 923b4de1 7bc34fdb shows D's five files and nothing else, and the lines D adds to T/gate_bindings.py match D's own diff against A's old head.
  • 38cf1bf0 merges A's tested head 4d2f3b2b into D. One conflict, in test_support/microcosm_build/reference_intervals.py: A reformatted the end of its fixtures and D appends there. I kept both sides.
  • 5e4c846a merges the fix round's A head 30ae4599 into D with --no-ff. Git merged without conflict, and the merge brings in only A's fix-round files.
  • af038d04 merges A's EXPAND writer fix d916ea8d into D with --no-ff. Git merged without conflict, and git diff 4c644db7 af038d04 shows only that fix's two files.
  • 40079dfa merges A's bf418af1 into D with --no-ff, and 91bf4c0f corrects the fake's docstring: columns model/4 and model/8 give 3/8 of the model at unit weights, not 5/12. The delta review (Activate the NZ graph against pinned Chronicle facts (NZ graph G8) #1183 comment 6592283997) asked for both. git diff 4c644db7 91bf4c0f shows the EXPAND fix's two files plus that docstring's test file, and the proof and prefit test files give 80 passed at 91bf4c0f.
  • nzhub/g8w4d-nz-wiring is still at fcdb4739.

Shared hunks

Against A's head 30ae4599, at 4c644db7:

  • B/country_spec.py: 134
  • B/reference_intervals.py: 49, 59, 67-68, 134-138, 171, 331-334, 336-338, 446, 782, 784-790, 801-805, 933-943, 945, 995-1134, 1156-1160
  • T/gate_bindings.py: 46-49, 1636-1835, 1900-1905, 2044-2052
  • tests/test_reference_interval_support.py: 1-386
  • tests/test_transport_gate_bindings.py: 605
  • tests/test_transport_measurement_prefit.py: 1-300
  • test_support/microcosm_build/reference_intervals.py: 380-423

The fix round changed only D's two test files. The src ranges are as they were at 38ae4a74.

The pin in test_transport_gate_bindings.py:605 gains measurement_prefit, because the roster test lists every registered gate. No module-hash pin moved.

Implementation hashes

I computed implementation_hash() for 43 transport kernels (every kernel the composed toy registry installs plus every module-level transport kernel), 89 UK runtime kernels (the UK spine registry and every UK kernel object or no-argument kernel class) and 6 US runtime kernels (the post-transfer registry on a stub engine and the module-level kernels), each in its own head's tree. While each hash ran, a trace recorded every source file it read. No UK or US kernel reads reference_profiles.py, reference_intervals.py or country_spec.py, and none moves. That includes the UK target kernels (uk.full.target_compilation@1, uk.full.national_targets@1 and the rest), which leave country_spec out on purpose (uk_runtime/target_compile_modules.py:59, EXCLUDED_DIRECT_IMPORTS). Editing T/compose.py moves no source_hash: no kernel reads it. Editing T/gate_bindings.py moves gates.battery@1, whose implementation_hash folds in its bindings' sources (T/gate_kernels.py:747-775), so D's additions there move the battery too.

Kernel fcdb4739 A D Why it moves: the changed modules it hashes
calibration.prepare@1 c13e76497b8d2dfb 6e214cef0e1455ff 180823901b9a0d99 reference_intervals, reference_profiles
diagnostics.calibration@1 d38f8fad075a9ace 30654dfc7038f560 30654dfc7038f560 (same) reference_profiles
gates.battery@1 29f0505b6a0093f8 00216abb7158bf98 ac050bab09f48dc2 country_spec, gate_bindings, reference_profiles
geography.support_from_facts@1 60273ea8388098b8 ad0a8e73d20405be ad0a8e73d20405be (same) reference_profiles
takeup.assign@1 53caf4ae91fc4acb b2a3a50cc376108b b2a3a50cc376108b (same) reference_profiles
takeup.compare@1 a318a19b0d147ec9 0231145ac4631652 1605d7e8519f4b0f country_spec, reference_profiles
takeup.gap@1 3e15ad5e2903c31c 6b200d952fe499c8 54f79560d896fab4 country_spec, reference_profiles
targets.compile@1 f3d372c40418665b 3da9798c5334f495 dc91d1479f8f84b1 country_spec, reference_profiles
targets.problem@1 aed111cadf163873 b949922486c0813b fe401c736551e351 reference_intervals, reference_profiles
transport.create@1 82dc0880aefba156 ef23135c5ec9d98a 865c21926cee0ac0 country_spec, reference_profiles
transport.create@2 82dc0880aefba156 ef23135c5ec9d98a 865c21926cee0ac0 country_spec, reference_profiles
transport.quantile_map@1 0966a4eb8a492216 4519e4bbfd966f97 b25bb6538b19dd64 country_spec, reference_profiles

The fix round moves no hash. I recomputed implementation_hash() at 38ae4a74 and at 4c644db7, each in its own worktree: 43 transport kernels (the composed toy registry plus module-level transport kernels), 37 UK and 3 US runtime kernels that construct without an engine. All 83 hashes match, as do A's at 4d2f3b2b and 30ae4599. uk.full.rotated-holdout@1 and us.post_transfer.derive@1 need an engine to hash, so this run skipped them.

D adds no kernel to A's set. It moves calibration.prepare@1 and targets.problem@1 again through reference_intervals, and R2C-12's line moves the seven kernels that hash country_spec again: targets.compile@1, takeup.compare@1, takeup.gap@1 (not in R2C-12's list), transport.create@1/@2, transport.quantile_map@1 and gates.battery@1. D's additions to T/gate_bindings.py also move the battery. The battery's binding identity (the SHA-256 of repr(GateBatteryKernel(TRANSPORT_GATE_REGISTRY)._identity[0]), first 16 hex digits) moves at D only, from 715e6202808aade7 at fcdb4739 and A to 55947149ba78ec86, because the registry gains the measurement_prefit binding.

Tests

The closed list at 4c644db7 (one pytest run, -n 4, no --maxfail): 2714 passed, 84 skipped, 0 failed. Per file: country_spec 143, nz_as_area_crosswalk 67 (3 skipped), nz_as_bridge 34, nz_axiom_input_closure 19 (2 skipped), nz_benefit_gross_up 16, nz_family_reconciliation 7 (9 skipped), nz_joint_geography 21 (12 skipped), nz_money_normalization 56, nz_msd_benefit_rates 6, nz_msd_pre_activation_reconciliation 4 (1 skipped), nz_reference_aging 6 (16 skipped), nz_reference_intervals 13 (1 skipped), nz_spec_package 1203, nz_target_references 16 (38 skipped), nz_transport_declarations 29 (2 skipped), nz_transport_toy_graph 23, reference_interval_support 12, reference_intervals 107, reference_profiles 129, reference_summed_bands 23, spec_engine_country_bundles 22, spec_only_country_packages 37, transport_composed_graph 469, transport_gate_bindings 131, transport_graph 15, transport_measurement_prefit 47, transport_measurement_proof 27, transport_population_kernels 32. The repo has no test_be_*.py file.

Every skip needs data or an engine this environment doesn't have: nz_axiom_input_closure 2 (needs axiom_rules_engine with its dense extension and a rulespec-nz checkout); nz_transport_declarations 2 (pinned feed not staged); nz_as_area_crosswalk 3 (needs NZ_AS_AREA_SOURCES); nz_joint_geography 11 (needs NZ_AS_AREA_SOURCES and NZ_JOINT_GEOGRAPHY_SOURCES); nz_target_references 38, nz_reference_aging 16, nz_family_reconciliation 9, nz_joint_geography 1, nz_reference_intervals 1 and nz_msd_pre_activation_reconciliation 1 (need NZ_CHRONICLE_ARTIFACT).

Checks: python3 tools/ci_test_plan.py verify prints verification=ok at 4c644db7, and uv run ruff check packages/microcosm-build passes. I didn't rerun the hash pins outside the closed list or the NZ builders' --check. The round changed no file they read, and the earlier run at 38ae4a74 stands for them.

Fix round

The independent review (#1183, comment 6589309875) drove this round. Lines are at 4c644db7. No fix touched shared src.

Fix Commit Where Red under its mutant Shared hunks touched
Merge-up of A's fix round 5e4c846a A's T/compose.py and tests n/a A's (see #1456)
D1/OD2: three or more amount-only discrepant bands refuse 4c644db7 tests/test_transport_measurement_prefit.py:157-174 (test_three_discrepant_bands_refuse_and_two_pass), against T/gate_bindings.py:1768-1769 the 6 amount cases tests only
D1/OD3: _Problem.estimates uses its weights; w₀ = (2, 4) 4c644db7 tests/test_transport_measurement_prefit.py:40-62, :124-134 (test_the_model_side_is_the_matrix_at_the_initial_weights), against T/gate_bindings.py:1726 test_the_model_side_is_the_matrix_at_the_initial_weights and the edge cases tests only
D1/OD1: a zero-weight household isn't counted 4c644db7 tests/test_reference_interval_support.py:190-206 (test_a_zero_weight_household_is_not_counted), against B/reference_intervals.py:1019 test_a_zero_weight_household_is_not_counted tests only

Not done

  • No NZ wiring. NZ's gates.json entry, its evidence aliases and the household entity its coarse table declares belong to hunk G. The problem frame must carry the household entity and source_household_id for the table's support to run; hunk G needs to wire that.
  • The gate checks R2C-3's evidence items for presence by alias, not by content.
  • The engine-present hash pins weren't run (no engine in this session).

https://claude.ai/code/session_01LwQq6JfFhwRFMYkokanqow


Generated by Claude Code

claude added 13 commits November 2, 2026 16:27
scale_money_and_intervals now accepts a summed money row: an identity
row with no aggregate_fact_key, a selector that pins a dimension to a
list of members, and an expected_member_count equal to the cells those
lists span. The resolver emits one operand {dimension_values,
expected_member_count, weight}; the ledger's multi-member
linear_combination path sums it. The count companion keeps value
operation sum over the same members, because the ledger's identity
path resolves one fact. The resolver refuses a wrong member count, a
non-identity money row, count and money member sets that differ, and
members two rows share. The receipt records each factor's binary64
weight and its exact relative rounding (R2C-10).

compose.measurement_proofs satisfies measurement:<id> only when, for
every calibration node, exactly one upstream node declares
measurement_id: <id> and writes the scaled table's income and universe
columns on its entity (R2C-1). Upstream follows the graph compiler's
predecessor rule over the declared rows. reference_profile_view passes
the proofs to the resolver; a proven row becomes active, any other row
stays parked, and its activation gap names the measurement and the
reason.

Invariants: a compiled summed amount equals the factor times the sum
of its members, and the count equals the sum of its members; a row is
active exactly when its measurement is proven. The tests in
test_reference_summed_bands.py and test_transport_measurement_proof.py
cover them, but this commit is pushed before the environment finished
installing, so they have not run yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N2tyHP1LrUqzw1Kaau8bvP
An interval table that declares diagnostics.support_household_entity
reports, per band, the distinct source households and the
source-household Kish ESS of its count and amount contributions.
Contributions are summed per source household before the Kish ratio,
so donor clones add no support. A unit reaches its household through
the person table's membership columns; a unit with members in several
households, or an undeclared household entity, refuses by name. A
table without the field parses, classifies and reports as before.

The measurement_prefit binding applies R2C-3's four checks to the
problem's matrix at the entering weights: roster totals, concentrated
discrepancy (a band counts once), per-band support for counts and
amounts separately, and missing evidence. Every threshold comes from
the gates.json entry. The frozen country_spec allow-list doesn't yet
admit the gate name; a hub ruling is pending on #1183.

Invariants: ESS never exceeds distinct source households, and merging
clones leaves it unchanged; weighted band counts equal the weighted
indicator sums. This commit is pushed before the environment finished
installing, so its tests have not run yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N2tyHP1LrUqzw1Kaau8bvP
Merge nzhub/g8w4d-nz-wiring at fcdb473 (the w4 fix round's hand-off)
into G8-r2c hunk A. Git merged without conflict: the fix round's edits to
T/compose.py and T/gate_bindings.py sit apart from A's hunks.

git diff fcdb473 HEAD shows A's five files and nothing else, and the
removed and added lines in compose.py match A's own diff against d81280b.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HQomkwKHCkdAgY9cy
Merge nzhub/g8r2c-a-summed-bands at 923b4de, which carries w4d
fcdb473, into G8-r2c hunk D. Git merged without conflict.

git diff 923b4de HEAD shows D's five files and nothing else, and the
lines gate_bindings.py adds match D's own diff against A's old head.

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

The measurement proof (R2C-1) now reads the id a producer declares in its
params, under measurement_id, as a literal string: measure.combine@1 takes
it as context.params["measurement_id"]. The node-level measurement_id
field is gone from the transport node schema, and a non-string value
proves nothing.

Upstream of calibration now means ancestry under the S5 guard's declared
rule (population or base, artifact producers and the writers of each read),
factored out of _validate_precalibration_ancestry and shared. Composition
then rechecks every proof against the compiled skeleton's exact
predecessors and refuses a proof the compiled graph contradicts.

The summed-band fixture now carries the dimension and target labels the
ledger requires for a multi-fact target, so the compile tests run the real
ledger.

Invariant: a measurement is proven exactly when each calibration node has
one declaring ancestor that writes the table's columns on its entity.
Checked by test_transport_measurement_proof.py (16 passed) and
test_reference_summed_bands.py (18 passed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HQomkwKHCkdAgY9cy
Hunk A's resolution validator refuses a factor companion that is active
without its measurement's proof. Two fixtures forged activation after
resolution: factor_transport_fixture(activate=True) and the CLI replay
test's measured view. Both now hand the resolver a synthetic proof, so the
real resolver activates the rows and binds the proof into the selection
identity.

The proof reads the raw transport_graph resource, as the rest of the
reference-profile view does, so a spec whose graph composition would
refuse for another reason still resolves its references.

Invariant: a factor row is active exactly when its receipt carries the
measurement's proof. Checked by test_transport_target_kernels.py,
test_nz_money_normalization.py, test_transport_run_entry_factors.py and the
CLI replay test (200 passed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HQomkwKHCkdAgY9cy
A mutant that removed the comparison between a summed money row's members
and its count companion's members left every test green. The comparison
cannot fire: the companion key includes the canonical member lists, so a
count row pairs with a money row only over the same member set, and a
mismatch already refuses as a missing companion. The refusal now says
so.

Invariant: summed money and count rows pair only over one member set.
Checked by test_summed_rows_refuse_each_inconsistency[mismatched_members]
(18 passed in test_reference_summed_bands.py).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HQomkwKHCkdAgY9cy
Carry A's tested head 4d2f3b2 (params-based measurement proof, compiled
recheck, fixture proofs, member-set simplification) into D.

One conflict, in test_support/microcosm_build/reference_intervals.py: A
reformatted the end of the summed-band fixtures, and D appends the
pre-fit fixtures there. The resolution keeps both sides' lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HQomkwKHCkdAgY9cy
Add "measurement_prefit" to ALLOWED_GATE_FUNCTIONS in country_spec.py,
the one line of that file ruling R2C-12 lets hunk D touch. The pinned
test of the loader's refusal goes, and a test loads a gates.json entry
naming the gate with R2C-3's numbers.

D's pushed tests had not run. They now build the gates.json header the
loader requires (version, country, policy, phases), and the two-household
tax-unit test gives the frame explicit unit weights, so the frame's own
weight derivation does not refuse before the support diagnostics do.

Invariant: a gates.json entry naming measurement_prefit loads with its
parameters intact. Checked by test_transport_measurement_prefit.py (31
passed) and test_reference_interval_support.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HQomkwKHCkdAgY9cy
The pushed clone property split each weight by the clone count and
skipped its assertion whenever the split was inexact in binary64, which
is most draws for 3 clones. It now gives each original record clones x w
and each clone w, so the per-household sums are exact and the assertion
runs on every example. It also checks that a band with no source
household reports an ESS of 0, and an example pins the empty-band report.

Invariants: source-household Kish ESS <= distinct source households, and
cloning records within a source household leaves per-band support
unchanged. Checked by test_reference_interval_support.py (11 passed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HQomkwKHCkdAgY9cy
The docstrings now name what the code does: membership runs through the
person table's group columns, a unit must nest in one household as the
frame's group-to-group placement requires, and an amount contribution's
weight is w x z at the weights entering calibration. No behaviour
changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HQomkwKHCkdAgY9cy
A mutant that lowered the source-household floor to 29 left every test
green: the 29-household case also carried an ESS of 29, so the ESS check
refused it alone. The test now names the expected refusal lines, and a
case with 29 households and an ESS of 30 refuses on the household floor
alone. The mutant now turns four cases red.

Invariant: the gate refuses a band with fewer than 30 source households,
or a Kish ESS below 30, for count and amount separately, and exactly 30
passes. Checked by test_transport_measurement_prefit.py (33 passed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HQomkwKHCkdAgY9cy
The closed list at D found two pins. The registry roster test lists every
bound gate, so it gains measurement_prefit. The gate-bindings module
holds no numeric literal beyond 0 and 1 (test_holds_no_threshold_literal),
and _ratio_bounds compared a length with 2; it now checks for a pair by
unpacking. A new test refuses empty, short, long, string and reversed
bounds.

Invariant: gate_bindings.py carries no threshold literal, and every
registered transport gate is on the roster. Checked by
test_transport_gate_bindings.py, test_transport_measurement_prefit.py,
test_transport_binding_identity.py and test_transport_graph.py (216
passed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6HQomkwKHCkdAgY9cy
Ruling R2C-15: a measurement holds only when its declaring producer is
the only ancestor of each calibration node that writes each of the
interval table's income and universe columns on the table's entity. The
review's item 2c built a graph where an undeclared later-version rewrite
of the income column passed both checks while the receipt named the
declarer. Before this change measurement_proofs proved it, the compiled
check accepted it, and compose_transport_graph composed the composed
toy with the rewrite grafted on.

measurement_proofs now collects every node in the calibration node's
declared closure whose outputs hold each required column, and parks the
rows unless that list is exactly the producer. _check_measurement_proofs
applies the same rule over the compiled ancestors and the composed
nodes' outputs and raises. Both share _second_writer_refusal, which
names the measurement, the calibration node, the column and every
writer. The rule has no ordering logic: a writer before or after the
producer refuses, with or without rewrite.

Checked with new pins in test_transport_measurement_proof.py: item 2c,
a writer before the producer, a universe-only rewrite, a rewrite-free
second writer, the composed toy, a compiled-only case, two controls
that still prove, and a Hypothesis property over declared chains.
Dropping the declared check turns 7 tests red; dropping the compiled
check turns test_the_compiled_check_alone_refuses_a_second_writer red.
measurement_proofs and validate_transport_activation give identical
outcomes on every packaged spec (am, be, nz, uk, us) before and after.

The same commit adds test_a_calibration_endpoint_without_a_calibrate_kernel_is_a_root
for the review's OA2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JfFhwRFMYkokanqow
The review's OA1 and OA3 mutants left every test green. Two new pins in
test_reference_summed_bands.py now kill them.

OA1: test_a_tampered_rounding_record_refuses changes one field of the
receipt's factor_weight_rounding record (weight, exact numerator or
relative rounding), recomputes the receipt hash so that only the
rounding recomputation can see it, and expects
validate_reference_resolution to refuse. Deleting that recomputation
turns all three cases red.

OA3: test_summed_rows_refuse_each_inconsistency gains a mapping member
and a list member inside a summed row's member list. Dropping the
scalar check in _summed_members turns both cases red.

OA2's pin landed with A1 (05bb18c): dropping the endpoint clause of
_calibration_roots turns
test_a_calibration_endpoint_without_a_calibrate_kernel_is_a_root red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JfFhwRFMYkokanqow
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JfFhwRFMYkokanqow
The review's OD1, OD2 and OD3 mutants left every D test green. Each now
has a test that kills it.

OD2: test_three_discrepant_bands_refuse_and_two_pass takes a role, so
two, three and four bands fail on the amount only as well as on the
count only. Three or more refuse under the concentrated rule, and the
failure line names the count. Making only the count role mark a band
discrepant turns the six amount cases red.

OD3: the test _Problem multiplies a two-column matrix by the weights it
receives, with non-unit initial weights (2, 4). The matrix gives the
declared model exactly at w0 and 5/12 of it at unit weights.
test_the_model_side_is_the_matrix_at_the_initial_weights asserts w0 is
not all ones and that the gate's band model is A·w0. Evaluating the
comparator at unit weights turns that test and the edge cases red.

OD1: test_a_zero_weight_household_is_not_counted adds a household of
weight 0 to the hand-computed ESS case; band_support still reports two
source households and the same ESS. Dropping the value != 0 filter in
_support_measure turns it red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JfFhwRFMYkokanqow
@MaxGhenis
MaxGhenis changed the base branch from nzhub/g8r2c-a-summed-bands to main November 3, 2026 00:03
@MaxGhenis MaxGhenis closed this Nov 3, 2026
@MaxGhenis MaxGhenis reopened this Nov 3, 2026
MaxGhenis and others added 5 commits November 2, 2026 19:35
R2C-15 makes the producer the only ancestor of a calibration node that writes
each measured column. Both checks collected writers from node outputs, so an
EXPAND whose `params.expand_cells` names the income passed
`measurement_proofs`, `_check_measurement_proofs` and
`compose_transport_graph`, while the S5 reader in the same file counts those
cells as writes.

The declared check now asks `_declared_writer`. It also counts an EXPAND's
literal cells, and it treats an EXPAND whose cells come from a resource as
writing every column, as `_precalibration_declared_reads` does. The compiled
check reads the cells through `_precalibration_expansions`.

No packaged spec declares an EXPAND, so no packaged outcome changes.

The invariant: a measurement proves only when no ancestor other than its
producer may write a measured column, by output or by EXPAND cell.
`test_transport_measurement_proof.py` gives 33 passed. Dropping the resource
clause fails `[expand_resource_cells]` alone, and
`test_packaged_spec_refuses_with_every_activation_gap_named` still passes.

The patch comes from Belgium's hub, patch-id a48429b46f51315760a66b27bdf60daf2c342bba.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 200 measured the property reaching R2C-15's branch in 16 of 20 seeded runs, about three examples a run, and the proof about once a run. Three explicit examples now cover one declarer alone and a second writer after it and before it. With _second_writer_refusal never refusing, both second-writer examples fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Columns model/4 and model/8 at unit weights give 3/8 of the model, not 5/12. No test asserts the fraction. Review 200 found this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review November 3, 2026 03:48
@MaxGhenis
MaxGhenis merged commit 000243c into main Nov 3, 2026
10 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged by the NZ hub (session local_e407a527) at 91bf4c0 with --merge. Gates: gh pr checks exit 0; MERGEABLE; not draft; no CHANGES_REQUESTED review. Reviewed head: 91bf4c0. Gates: delta review 200 (#1183 comment 6592283997, APPROVE_WITH_NITS) after review 6589309875; its test-only nit fixed at 91bf4c0. Belgium's hub OK on D 91bf4c0 (NZ hub notes/1103/from-belgium-ad-nits-and-1477.md). R2C-11 recheck 2026-11-03 03:5xZ: no reservation in force. Merge commit, after A.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants