Skip to content

G8-r2c A: summed-band money rows and the measurement proof - #1456

Merged
MaxGhenis merged 9 commits into
mainfrom
nzhub/g8r2c-a-summed-bands
Nov 3, 2026
Merged

MaxGhenis merged 9 commits into
mainfrom
nzhub/g8r2c-a-summed-bands

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

G8-r2c shared hunk A (tracking #1183): money rows that sum several published bands, and composition's proof of a measurement's producer (ruling R2C-1). Stacked on the fixed w4d head fcdb4739. Hunk D (#1457) stacks on this branch, and the two share one re-key window.

B/ is packages/microcosm-build/src/microcosm/build/, T/ is B/transport/, and tests are in packages/microcosm-build/tests/engine_free/shared/. Lines are at 30ae4599, after the fix round. d916ea8d then adds the EXPAND writer fix described under "Fix round", which moves T/compose.py lines after :591 down by up to 17. bf418af1 then adds three @example lines, a comment and one import name to tests/test_transport_measurement_proof.py, and changes no other file.

The change

Summed money rows. scale_money_and_intervals accepts a multi-member identity money row: no aggregate_fact_key, a selector that pins a dimension to a list of members, and expected_member_count. The resolver turns it into one operand {dimension_values, expected_member_count, weight} (B/reference_profiles.py:333 _money_operand). The ledger already sums such an operand, so nothing outside the resolver changes. The count companion keeps value_operation: sum over the same members, because the ledger's identity path resolves one fact. The resolver refuses, by name:

  • a member count that differs from expected_member_count (_summed_members, :217);
  • a money row that isn't identity ("Money scaling requires an untransformed source reference");
  • a count row that doesn't sum its members (_validate_summed_rows, :283);
  • a count row and an amount row over different member sets: the companion key holds the canonical member lists, so such a pair never pairs, and the money row refuses as having no count companion over its members;
  • members shared by two rows of one role in one selector scope;
  • a member repeated inside one list, a non-scalar member, and a row that names both a fact key and a member list.

The weight stays the factor the resolver writes today, numerator / denominator in binary64. The receipt's factor_weight_rounding (:828) states each factor's float, its exact rational and the exact relative rounding as a reduced fraction (R2C-10). Validation recomputes the record and refuses a mismatch (:1073).

The measurement proof (R2C-1, R2C-15) is measurement_proofs in T/compose.py:593. It satisfies measurement:<id> only when, for every calibration node:

  • exactly one node upstream of it declares the id;
  • that node writes the income and universe columns of every interval table the measurement's profiles scale, on that table's entity;
  • and that node is the only node upstream of the calibration node that writes any of those columns on that entity (R2C-15). A second writer refuses whether it runs before or after the producer, and whether or not its output says rewrite: true. An EXPAND's cells count as writes, and an EXPAND whose cells come from a resource counts as writing every column. A writer outside the calibration node's ancestors, or a same-named column on another entity, doesn't count.

Otherwise the rows stay parked and the activation gap names the measurement and the reason (T/compose.py:944-984), so compilation refuses. The writer refusal (_second_writer_refusal, :571) names the measurement, the calibration node, the column and every writer.

  • Where it reads the id: T/compose.py:516, params.get(MEASUREMENT_PARAM), with MEASUREMENT_PARAM = "measurement_id" (:506). A node declares the id as a literal string in its params, which is where measure.combine@1 reads it (context.params["measurement_id"]). A non-string value proves nothing and the refusal names the node. An earlier commit on this branch added a node-level measurement_id field and read it there; a later commit on the branch took the field out again. Against fcdb4739, no schema file changes.
  • How it decides "upstream of calibration": graph ancestry, checked twice.
    1. On the declaration, the proof walks ancestry under the S5 guard's own predecessor rule (_declared_calibration_predecessors, :519, now shared with _validate_precalibration_ancestry at :3343): a node follows its population or base, every artifact producer it names, and every writer _precalibration_declared_reads resolves for its reads. Where a version is unknown that rule counts every writer, so it can only find more producers, which refuses. The roots are every calibrate.* node plus the calibration endpoint (_calibration_roots, :546).
    2. Composition then rechecks each proof against the compiled skeleton's exact predecessors (_check_measurement_proofs, :699, called at :4349 right after compile_graph): each calibration node's compiled ancestors must hold exactly one node declaring the id, it must be the proof's producer, and it must be the only compiled ancestor whose outputs, or EXPAND cells since d916ea8d, hold each of the proof's columns (:735-756). Otherwise composition refuses.
  • What the resolver reads: reference_profile_view passes the proofs to resolve_reference_profiles (T/compose.py:787). A proven binding satisfies measurement:<id> in _satisfy_factor_prerequisite (B/reference_profiles.py:638), the rows become active, and the proofs used enter the selection identity. Validation refuses a factor row that is active without its proof in the receipt, or parked with one (:1155-1162).

Invariants

Invariant How it is tested
A compiled summed amount equals f × Σ members, and its count equals Σ members Hypothesis, test_compiled_coarse_rows_equal_factor_times_member_sums: random partitions of up to 30 synthetic facts, factors p/10^6, through the real ledger
The summed path equals the sum of singleton rows aged by today's path Differential, test_summed_path_equals_the_sum_of_singleton_rows_aged_today, three factors
A summed operand names its members, and the ledger checks their count. Neither pins a record set (R2C-16) test_summed_money_operand_resolves_amount_facts_only covers the one-record-set fixture's case; test_summed_rows_refuse_each_inconsistency covers the member checks
A factor row is active exactly when composition proved its measurement test_an_active_row_without_its_proof_in_the_receipt_refuses; test_without_a_proof_rows_stay_parked_and_compilation_refuses
A measurement is proven exactly when each calibration node has one declaring ancestor, that ancestor writes the table's columns on its entity, it is the only ancestor that writes them (R2C-15), and the compiled graph agrees test_each_proof_mutant_leaves_rows_parked_and_names_the_measurement (no producer, after calibration, two producers, wrong column, income without universe, wrong entity, wrong entity on tax_unit, a later-version rewrite, a writer before the producer, a universe-only rewrite); test_a_chain_proves_exactly_when_one_declarer_is_the_only_writer (Hypothesis); test_a_third_entity_table_passes_the_proof; test_composition_refuses_a_proof_the_compiled_graph_contradicts; test_the_compiled_check_alone_refuses_a_second_writer; test_composition_refuses_a_later_version_rewrite_of_a_measured_column

Mutants

I ran each mutant with a script that edits one line, runs the new test files and restores the file.

Mutant Red
Summed rows fall back to the singleton path (_money_operand) 10 tests, among them test_summed_path_equals_the_sum_of_singleton_rows_aged_today[110]
Drop the member-count check test_summed_rows_refuse_each_inconsistency[money_member_count], [count_member_count]
Drop the shared-member check test_summed_rows_refuse_each_inconsistency[overlapping_members]
Treat every binding as proven 9 tests, among them test_without_a_proof_rows_stay_parked_and_compilation_refuses and every proof mutant
Drop the producer's column check the wrong_column, income_without_universe, wrong_entity and wrong_entity_tax_unit cases
Accept two producers [two_producers]
Ignore ancestry (any declaring node counts) [after_calibration], test_every_calibration_node_needs_its_own_upstream_producer
Drop the compiled recheck both test_composition_refuses_a_proof_the_compiled_graph_contradicts cases
Read a node-level measurement_id instead of the param 10 tests
Fix round: drop the declared writer check (R2C-15) 7 tests: the later_version_rewrite, writer_before_the_producer and universe_rewrite cases, test_a_later_version_rewrite_names_the_measurement_node_column_and_writers, test_a_second_writer_refuses_without_rewrite, test_a_chain_proves_exactly_when_one_declarer_is_the_only_writer, test_composition_refuses_a_later_version_rewrite_of_a_measured_column
Fix round: drop the compiled writer check (R2C-15) test_the_compiled_check_alone_refuses_a_second_writer
Fix round OA1: skip the rounding-record recomputation the 3 cases of test_a_tampered_rounding_record_refuses
Fix round OA2: drop the endpoint clause of _calibration_roots test_a_calibration_endpoint_without_a_calibrate_kernel_is_a_root
Fix round OA3: accept non-scalar members test_summed_rows_refuse_each_inconsistency[mapping_member], [list_member]

At 30ae4599 I reran two of the mutants above: two producers accepted ([two_producers] red) and the member count unchecked ([money_member_count], [count_member_count] red).

One mutant survived the pushed tests: removing the member-set comparison between a money row and its count companion. The comparison couldn't fire (see the change above), so I removed it and documented why (4d2f3b2b).

Merge-up

  • 923b4de1 merges the fixed w4d head fcdb47395293955347ec2697feef1a736a76cc23 into A with --no-ff. Git merged without conflict. git diff fcdb4739 923b4de1 shows A's five files and nothing else, and the lines A adds to and removes from T/compose.py match A's own diff against d81280bb line for line. Every function the fix round owns (quantile_support, inside_quantile_support, the support and closure kernels, _support_roster, support_roster_slices, _check_structural_columns, _requests, _check_unreached_requests, _check_closure_relation_slices, _check_structural_rows_written, _support) keeps the fix round's text.
  • nzhub/g8w4d-nz-wiring is still at fcdb4739.

Shared hunks

Against fcdb4739, at 30ae4599:

  • B/reference_profiles.py: 72-73, 77-109, 181-194, 204-206, 217-371, 396, 434-442, 638-648, 675-678, 680-683, 694, 705-708, 766-776, 828-833, 897-899, a deletion after 906, 908, 1073-1078, 1155-1162, 1165-1171
  • T/compose.py: 463-758, 787, 944, 947, 967-984, 1431, 1531, 1542, 3343, 4349
  • tests/test_reference_summed_bands.py: 1-353
  • tests/test_transport_composed_graph.py: a deletion after 58, 116, 3210-3211, 3214-3224
  • tests/test_transport_measurement_proof.py: 1-732
  • test_support/microcosm_build/reference_factor_transport.py: a deletion after 11, 18-26, 157-160, 162-166, a deletion after 167
  • test_support/microcosm_build/reference_intervals.py: 94-97, 193-379

The fix round changed T/compose.py only inside measurement_proofs, _check_measurement_proofs and the new helper _second_writer_refusal beside them (571-592, 602-604, 672-686, 707-708, 722, 735-756). It left B/reference_profiles.py byte for byte as it was at 4d2f3b2b.

test_support/microcosm_build/reference_factor_transport.py and the CLI replay test in test_transport_composed_graph.py used to mark factor rows active by hand. They now hand the resolver a synthetic proof, so the real resolver activates them. Both are test code, not shared src.

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). A leaves that file alone.

Kernel fcdb4739 A Why it moves: the changed modules it hashes
calibration.prepare@1 c13e76497b8d2dfb 6e214cef0e1455ff reference_profiles
diagnostics.calibration@1 d38f8fad075a9ace 30654dfc7038f560 reference_profiles
gates.battery@1 29f0505b6a0093f8 00216abb7158bf98 reference_profiles
geography.support_from_facts@1 60273ea8388098b8 ad0a8e73d20405be reference_profiles
takeup.assign@1 53caf4ae91fc4acb b2a3a50cc376108b reference_profiles
takeup.compare@1 a318a19b0d147ec9 0231145ac4631652 reference_profiles
takeup.gap@1 3e15ad5e2903c31c 6b200d952fe499c8 reference_profiles
targets.compile@1 f3d372c40418665b 3da9798c5334f495 reference_profiles
targets.problem@1 aed111cadf163873 b949922486c0813b reference_profiles
transport.create@1 82dc0880aefba156 ef23135c5ec9d98a reference_profiles
transport.create@2 82dc0880aefba156 ef23135c5ec9d98a reference_profiles
transport.quantile_map@1 0966a4eb8a492216 4519e4bbfd966f97 reference_profiles

The fix round moves no hash. I recomputed implementation_hash() at 4d2f3b2b and at 30ae4599, 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. uk.full.rotated-holdout@1 and us.post_transfer.derive@1 need an engine to hash, so this run skipped them. No kernel hashes T/compose.py, and the round changed no other src file.

The design's list missed two kernels: takeup.compare@1 (R2C-12 named it) and diagnostics.calibration@1, which hashes reference_profiles (T/terminal_kernels.py:206). The battery's binding identity doesn't move at A. Every other kernel keeps its fcdb4739 hash.

Tests

The closed list at 30ae4599 (one pytest run, -n 4, no --maxfail): 2524 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_intervals 107, reference_profiles 129, reference_summed_bands 23, spec_engine_country_bundles 22, spec_only_country_packages 37, transport_composed_graph 469, transport_graph 15, 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).

Packaged specs. measurement_proofs and validate_transport_activation give byte-identical results on every packaged spec that load_transport_spec loads (am, be, nz, uk, us) at 4d2f3b2b and at 30ae4599. NZ's ird_taxable_income_ty2027_v1 stays parked with "0 nodes declaring", because no NZ node declares it yet.

C's producer still proves. In a scratch merge of origin/nzhub/g8r2c-c-measure into 30ae4599 (never pushed), a toy whose measure.combine@1 node carries C's real params (measure, measure_sha256, measurement_id) and C's three outputs proves, with that node as the only writer of both measured columns, and _check_measurement_proofs accepts the compiled graph. In that tree, test_transport_measurement_proof.py plus C's test_the_measurement_id_is_carried_not_used give 28 passed.

Checks: python3 tools/ci_test_plan.py verify prints verification=ok at 30ae4599, 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 4d2f3b2b stands for them.

Fix round

After the round: EXPAND cells count as writers (d916ea8d). Belgium's hub and the delta review (#1183 comment 6592283997) each found that R2C-15's two checks read only node outputs, so an EXPAND naming the income in params.expand_cells passed measurement_proofs, _check_measurement_proofs and compose_transport_graph, while the S5 reader in the same file counts those cells as writes. No packaged spec declares an EXPAND, so nothing reached this.

  • The fix: _declared_writer in T/compose.py also counts an EXPAND's literal cells, and 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. test_transport_measurement_proof.py gains six tests.
  • The hub's runs at d916ea8d: the proof test file gives 33 passed, test_packaged_spec_refuses_with_every_activation_gap_named passes, and dropping the resource clause fails [expand_resource_cells] alone. ruff check and format --check pass on both files.
  • Belgium's hub's runs, on the same patch: five mutants, each red. None of the 40 engine-free kernels' hashes moves, and measurement_proofs and validate_transport_activation give byte-identical results on am, be, nz, uk and us.
  • The delta review's runs at af038d04: its EXPAND probes refuse, and three mutants of the patch each turn a test red.
  • One stricter case, kept: the delta review built a producer inside an EXPAND's version while that EXPAND also lists the measured column. compile_graph accepts that graph and both checks refuse it, though the review reads the executor as taking the column from the producer alone. R2C-15 refuses any other writer among the ancestors, before or after the producer, so the refusal stands. A spec with that shape must leave the column out of expand_cells.

The delta review's nit (bf418af1). The review measured the chain property reaching R2C-15's branch in 16 of 20 seeded runs, about three examples 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. The proof file still gives 33 passed, and ruff passes.

The independent review (#1183, comment 6589309875) and rulings R2C-15 and R2C-16 (comment 6590041678) drove this round. Lines are at 30ae4599.

Fix Commit Where Red under its mutant Shared hunks touched
A1: a measurement has one writer (R2C-15) 05bb18ca T/compose.py:571-592 _second_writer_refusal; declared check :672-686 in measurement_proofs; compiled check :735-756 in _check_measurement_proofs Declared check dropped: 7 tests (see Mutants). Compiled check dropped: test_the_compiled_check_alone_refuses_a_second_writer T/compose.py (lines listed), tests/test_transport_measurement_proof.py
A2: the record-set claim (R2C-16) body only The invariant row, the schema wording and the proof's rule in this body n/a none
A3/OA1: tampered rounding record 30ae4599 tests/test_reference_summed_bands.py:229-248, against B/reference_profiles.py:1073 test_a_tampered_rounding_record_refuses (3 cases) tests only
A3/OA2: non-calibrate.* calibration endpoint 05bb18ca tests/test_transport_measurement_proof.py, test_a_calibration_endpoint_without_a_calibrate_kernel_is_a_root, against T/compose.py:546 that test tests only
A3/OA3: non-scalar member 30ae4599 tests/test_reference_summed_bands.py:187-191, :208-209, against B/reference_profiles.py:247-255 [mapping_member], [list_member] tests only

Item 2c's graph, before and after. toy.measure declares the id and writes both columns, the filter version toy.filter follows, then the undeclared toy.overwrite with rewrite: true on person.toy_income_cents, then preparation and calibration on toy.filter.

Call 4d2f3b2b 30ae4599
measurement_proofs proves, producer toy.measure refuses: "calibration node 'toy.calibrate' has writers ['toy.measure', 'toy.overwrite'] of person.toy_income_cents upstream; only its producer 'toy.measure' may write it"
compile_graph accepts; preparation's predecessors are toy.filter and toy.overwrite accepts (unchanged)
_check_measurement_proofs accepts with no declared proof it has nothing to check; with a proof forced as in test_the_compiled_check_alone_refuses_a_second_writer, it raises the same writer refusal
compose_transport_graph the bare toy stops at activation for resources it lacks. With item 2c grafted onto the composed toy, it composes 29 nodes, so no other guard catches it refuses at activation: "placeholder reference target_references.toy_count_b0; awaits measurement:reviewed_measurement (… writers ['toy.measure', 'toy.overwrite'] …)"

Not done

https://claude.ai/code/session_01LwQq6JfFhwRFMYkokanqow


Generated by Claude Code

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
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
…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
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
@MaxGhenis
MaxGhenis changed the base branch from nzhub/g8w4d-nz-wiring 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 2 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>
@MaxGhenis
MaxGhenis marked this pull request as ready for review November 3, 2026 03:48
@MaxGhenis
MaxGhenis merged commit ab913e5 into main Nov 3, 2026
10 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged by the NZ hub (session local_e407a527) at bf418af with --merge. Gates: gh pr checks exit 0; MERGEABLE; not draft; no CHANGES_REQUESTED review. Reviewed head: bf418af. Gates: delta review 200 (#1183 comment 6592283997, APPROVE_WITH_NITS) after review 6589309875; its test-only nits fixed at bf418af (A) and 91bf4c0 (D). Belgium's hub OK on A bf418af and 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 and no Fleet ops hold on shared hashes. Merge commit; D lands next.

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