Skip to content

Bind NZS co-residence from household composition (H2) - #1485

Merged
MaxGhenis merged 9 commits into
mainfrom
nzhub/h2-nzs-coresident-binding
Nov 4, 2026
Merged

MaxGhenis merged 9 commits into
mainfrom
nzhub/h2-nzs-coresident-binding

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

This is hub ruling R2C-20's H2 (tracking issue #1183). It targets main since G (#1478) landed as f8f543a4. It started on #1483 (nzhub/g8r2c-g-toy at 85d55af), with main 4a36edcf merged in (df9fce45) for #1477's coresident_adult kind. 1ad5bb17 merges main again: main and the branch shared two merge bases (85d55af5 and 4a36edcf), so GitHub reported a conflict that git's recursive merge doesn't have. Against main, 1ad5bb17 changes exactly this PR's own lines in its 15 files.

Before this PR, NZ's input closure defaulted nz_super_shares_residence_with_person_at_least_adult_age to false, so every single superannuitant got the s 4A living-alone rate. Now NZ's concept mapping binds it on Person with {"kind": "coresident_adult", "min_age": 18}: a person reads true when another person with the same household id is aged 18 or over.

Review: brief 215 reviewed 1b56b916 (#1183 comment 6616135497): APPROVE_WITH_NITS, Needs Max: no, no fixes owed. The hub took two of its nits: d82a2ba5 gives the small-frame test a second null-id adult (nit 2), and the Mechanism note under "The toy, before and after" now gives the right reason F1 doesn't reach that test (nit 3). Its nit 4, main's red test_the_batterys_binding_identity_adds_only_the_leaf_and_its_imports, came from #1342, and #1495 fixed it on main at 21:22Z 11/03. Brief 222 (#1183 comment 6616766311) reviewed d82a2ba5: APPROVE_WITH_NITS, no fixes owed. The toy file gave 33 passed, and mutant B turned the small-frame test red, with persons 10 and 11 reading true. For its one nit, the hub added person 11's row to the small-frame table below. G (#1478) landed as f8f543a4 at 00:13Z 11/04. The hub retargeted this PR to main, and CI run 41745582220 tests it against main.

Status: the hub's ruling R2C-28 (#1183) cleared the second shared count move, TestClosurePartition's nz_super partition, and 1725611c applies it. The 18 shared tests that failed in the run under Tests came from G's stack before its fix rounds: fix round 1 (20507ad8, R2C-26) fixes the stage-guard pin, and fix round 2 (f43e8fbb) the 17 calibration-column tests. Both rounds reached main with G, whose CI run 41729696579 passed every engine-free test; this PR's run 41745582220 tests the branch against main.

Commits

  1. 891cb9f1 adds the two hunks Belgium's hub prepared: the CoresidentAdult case in test_support/microcosm_frame/concept_reference.py (23, 160-171) and coresident_adult in the builder's bool set (tools/build_nz_transport_graph.py:208). Its stable patch-id, e3f8d1c790817d1c3e6677e68fd1f0a564f68af2, matches the hub's.
  2. 9cc37a58 adds the binding, the closure move, the regenerated files and NZ's moved pins, and corrects three statements the move made false.
  3. b78c8ad8 adds the before-and-after tests, which run through concepts.encode@1.
  4. 1725611c applies R2C-28: test_transport_evidence_kernels.py:810.
  5. 1b56b916 applies fixes from an independent review. The disclosure's evidence now also cites the exempt-adults default its note blames, and the commit tightens the binding note, the exempt reason and the method doc.

The binding

  • packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json:414-428: the new binding sits between nz_super_person_age and nz_super_single (the file orders by module, then by input name). tools/build_nz_transport_graph.py copies it into N/concept_mapping.json:414-428.
  • The note quotes the consuming rule, nz_super_living_alone (rulespec-nz core.yaml:550-573, s 4A). It says that min_age 18 copies parameter nz_super_living_alone_adult_age (core.yaml:65-79: 18 from 2026-04-01, source s 4A(3) and (4)) by hand, because no formula reads that parameter: the name appears once in core.yaml, at its definition. It says that the frame's household stands in for the statute's residence. It also lists the other differences: the binding counts a partner, but the partner gate (core.yaml:567, :664) means the rate never reads a partnered person's value; it counts adult dependent children and temporary visitors, whom the exempt-adults input carves out; ages are completed years at the survey date; and a person with no household id reads false.
  • I read core.yaml at both pins (6fe181fc and 8dc2507d). Both copies hash to d8ea0d15…, the hash the closure pins.

The closure (N/axiom_input_closure.json)

  • :1051-1052: the sharing entry is now "class": "encoded", "encoded_by": "concept_binding", without value, reason, citation or new_default.
  • :946: nz_super_all_shared_adults_are_exempt_dependent_children_or_temporary_visitors stays defaulted false. Its reason now covers what that default does. Every person who has no partner and shares a home with an adult gets the sharing rate (nz_super_single_not_living_alone_weekly_rate_before_tax, Schedule 1 Part 1 clause 1(b): 595.57 against 647.37 living alone, core.yaml:80-111). That includes a person whose only co-resident adults are exempt dependent children or temporary visitors, whom s 4A counts as living alone, so the default understates NZS for that group. The reason adds that S5's reg 17 superannuitant base rate reads the same weekly amount (as_rate_bridge.json:462-472).

Mechanism (read)

  • Before H2: ConceptsEncodeKernel.run (T/column_kernels.py:396-417) passed the closure to _defaults (:212-232), InputClosure.defaults (F/input_closure.py:520-526) returned this input's false, and _defaults wrote it on every person through apply_defaults (T/column_kernels.py:231).
  • After H2: defaults skips encoded entries, and mapping.encode (:403) dispatches to _coresident_adult_flags (F/concept_mapping.py:2232, :2293-2318). NZ's graph still declares the nz.encode.person output as bool, so the encoder's output dtype doesn't change.
  • Where the value goes: nz_super_ordinary_weekly_rate_before_tax picks a single person's rate from nz_super_living_alone (core.yaml:664-670). The pre-calibration stage nz.precal.nz_super outputs nz_super_weekly_amount_before_tax as a calibration_measure (N/precalibration_stages.json:84-96), and S5's reg 17 base rate adds the same amount for a single-superannuitant unit (N/as_rate_bridge.json:462-472).

Regenerated files

I ran build_nz_transport_graph.py, build_nz_benefit_gross_up.py, build_nz_entitlement_graph.py and refresh_concept_coverage.py --engine axiom-nz, and each --check passes.

  • N/concept_mapping.json:414-428: the binding.
  • N/export_replay_bindings.json:3: axiom_input_closure_sha256 moves from 791c1cd4… to d2694200….
  • N/transport_graph.json:4981: pending_outputs no longer lists the NZS sharing binding. This file holds one literal sha pin, concept_schema_sha256, which doesn't move; the graph reads the closure and mapping through resource selections.
  • packages/microcosm-frame/tests/golden/concept-coverage/axiom-nz.json and docs/concept-coverage/axiom-nz.md: covered inputs move from 89 to 90 and uncovered from 579 to 578.
  • packages/microcosm-build/tests/golden/nz_country_spec.json (3, 46, 53, 59, 83, 94): the fingerprint (06ce7ca0… to 9d0b57a1…) and five resource hashes. I regenerated it by rendering the loaded spec's summary, as G's commits did.

Pins

  • test_nz_spec_package.py:3422-3447: I renamed test_reporting_disclosure_cites_the_sharing_home_default_record to ..._binding_record. It resolves the disclosure's two citations: the encoded sharing record, and the exempt-adults default with its reason. It also checks the new note. test_keyed_citations_survive_a_record_inserted_before_the_target (:3238-3261) now expects both records for sharing_home.
  • test_transport_evidence_kernels.py:2734-2735 (TestSupportRoster.test_the_nz_roster_classes): closure_default moves from 93 to 92 and derived from 322 to 323, the move the brief clears. compose._support_roster classes the column derived, owned by nz.encode.person, and its uncovered sources are the test's pending pair (household.rent, person.liquid_financial_assets).
  • test_transport_evidence_kernels.py:810 (TestClosurePartition, R2C-28): nz_super encoded moves from 2 to 3 and defaulted from 11 to 10. The test still counts 158 roots in total, with check == [].

Beyond the brief

The move made three statements false, and this PR corrects them:

  • N/reporting_classifiers.json:212-213: the sharing disclosure said NZS was overstated because the input defaults false. It now says the model understates NZS for single people whose only co-resident adults are exempt, and that this moves their deciles downward. Its evidence cites both closure records: the sharing input's and the exempt-adults default's.
  • docs/nz-reporting-classifiers-method.md:66-71: the same correction in the method doc.
  • tools/build_nz_transport_graph.py:1271: pending_outputs drops "and the NZS sharing binding (R2C-5)".

The toy, before and after

The toy's adapters are pure Python stand-ins with invented rates, so this records the input only, not NZS amounts. I ran concepts.encode@1 on NZS core.yaml against the toy's nz.create frame: once with NZ's packaged mapping and closure (after), and once with a copy that drops the binding and restores the defaulted entry (before). All 120 persons read false before. After, 118 read true, and the two who read false are both under 65. A scratch script compared the binding with a direct read of household composition over all 120 persons and found no mismatch.

person household age partner (age) other co-resident (age) before after
10090 1030 65 10091 (70) 10092 (16) false true
10091 1030 70 10090 (65) 10092 (16) false true
10093 1031 66 10094 (71) 10095 (16) false true
10094 1031 71 10093 (66) 10095 (16) false true
10096 1032 67 10097 (72) 10098 (16) false true
10097 1032 72 10096 (67) 10098 (16) false true
10099 1033 68 10100 (73) 10101 (16) false true
10100 1033 73 10099 (68) 10101 (16) false true
10102 1034 69 10103 (74) 10104 (16) false true
10103 1034 74 10102 (69) 10104 (16) false true
10105 1035 70 10106 (75) 10107 (16) false true
10106 1035 75 10105 (70) 10107 (16) false true
10108 1036 71 10109 (76) 10110 (16) false true
10109 1036 76 10108 (71) 10110 (16) false true
10111 1037 72 10112 (77) 10113 (16) false true
10112 1037 77 10111 (72) 10113 (16) false true
10114 1038 73 10115 (78) 10116 (16) false true
10115 1038 78 10114 (73) 10116 (16) false true
10117 1039 74 10118 (79) 10119 (16) false true
10118 1039 79 10117 (74) 10119 (16) false true

Every toy superannuitant has a partner, so nz_super_single (core.yaml:567, :664) keeps the rate from reading this input for any of them. The toy has no superannuitant who lives alone or whose only co-resident is under 18, so test_h2_sharing_binding_on_the_household_shapes_the_toy_lacks builds a small frame:

person household age co-resident (age) before after
1 100 70 none false false
2 200 68 3 (40) false true
3 200 40 2 (68) false true
4 300 72 5 (16) false false
5 300 16 4 (72) false true
6 400 66 7 (18) false true
7 400 18 6 (66) false true
8 500 67 9 (17) false false
9 500 17 8 (67) false true
10 none 75 none false false
11 none 30 none false false

Person 10 has no household id (#1481, F1, changes how the frame treats such a person). The coresident_adult kind factorizes household ids with use_na_sentinel=True (F/concept_mapping.py:2305), so that person shares a household with nobody and reads false, as the default did. The test does reach _Context, the class F1 edits, through mapping.encode. F1's change still doesn't reach it, because _coresident_adult_flags reads the person table's own household ids (F/concept_mapping.py:2304-2306) and none of the three members F1 changes (_Context._household_rows, household_amount_for_persons, reference_rows); review 215 ran the cases at H2's head and with F1 merged, and they agree. Since d82a2ba5, person 11 (30) has no household id either, and both read false, so the test also fails if null ids group together (review 215's mutant B, use_na_sentinel=False).

test_h2_sharing_binding_turns_the_toy_superannuitants_default_into_true pins the toy rows, and the small-frame test pins every row above.

Tests

I ran 42 files with -n 4 --dist loadfile: the three the brief names, test_input_closure.py, test_concepts.py, the 16 test_nz_*.py files, test_fetch_nz_as_area_sources.py, and the 20 shared build test files where grep finds a read of NZ's packaged resources.

  • Run1 covered all 42 files on the tree that became 9cc37a58.
  • Run3 reran 12 of them on the tree committed as 1b56b916: every file I edited after run1 started, plus the files that read the resources 1b56b916 edited (2123 passed, 4 skipped).
  • The other 30 files' run1 results stand: they didn't change, and the resources they read changed after run1 only in note, reason and evidence text, and the hashes those edits move.

Ruff check and format --check pass on every changed Python file.

file passed failed skipped run note
test_axiom_concept_mapping.py 15 0 0 run3
test_calibration_columns.py 88 17 0 run1 inherited (R2C-28)
test_concept_mapping.py 245 0 0 run3
test_concepts.py 151 0 0 run1
test_country_spec.py 144 0 0 run3
test_fetch_nz_as_area_sources.py 10 0 0 run1
test_group_encode.py 81 0 0 run1
test_input_closure.py 52 0 0 run3
test_ledger_artifact.py 53 0 2 run1
test_nz_as_area_crosswalk.py 67 0 3 run1
test_nz_as_bridge.py 34 0 0 run1
test_nz_axiom_input_closure.py 19 0 2 run3
test_nz_benefit_gross_up.py 16 0 0 run1
test_nz_earnings_bands.py 4 0 0 run1
test_nz_family_reconciliation.py 7 0 9 run1
test_nz_joint_geography.py 21 0 12 run1
test_nz_money_normalization.py 56 0 0 run1
test_nz_msd_benefit_rates.py 6 0 0 run1
test_nz_msd_pre_activation_reconciliation.py 4 0 1 run1
test_nz_reference_aging.py 6 0 16 run1
test_nz_reference_intervals.py 15 0 1 run1
test_nz_spec_package.py 1203 0 0 run3
test_nz_target_references.py 19 0 41 run1
test_nz_transport_declarations.py 59 0 2 run3
test_nz_transport_toy_graph.py 33 0 0 run3
test_spec_engine_country_bundles.py 22 0 0 run3
test_spec_engine_reemission.py 8 0 0 run1
test_spec_only_country_packages.py 37 0 0 run3
test_spec_source_citations.py 138 0 0 run3
test_transport_column_kernels.py 31 0 0 run1
test_transport_composed_graph.py 469 0 0 run1
test_transport_declared_units.py 18 0 0 run1
test_transport_evidence_kernels.py 156 0 0 run3
test_transport_gate_bindings.py 131 0 0 run1
test_transport_graph.py 15 0 0 run1
test_transport_holdout_separation.py 40 1 0 run1 inherited (R2C-28)
test_transport_money_kernels.py 40 0 0 run1
test_transport_population_kernels.py 38 0 0 run1
test_transport_quantile_map_v2.py 38 0 0 run1
test_transport_takeup_kernels.py 38 0 0 run1
test_transport_target_kernels.py 68 0 0 run1
test_transport_weight_quality.py 30 0 0 run1
total, 42 files 3725 18 89 3832 collected

Inherited (R2C-28), all failing the same way at 891cb9f1, before the binding:

  • 17 in test_calibration_columns.py: G's 714e10d0 adds interval entries to NZ's calibration_bindings.json. The tests call prepare_calibration_columns(tables, _bindings()) (test file :38-44) with no interval document, so calibration_columns.py:106-107 raises. The hub fixes it on G's stack.
  • test_packaged_precalibration_stages_pass_the_stage_guard[nz]: main's pinned set (:277) lacks the nz_super stage that G's 714e10d0 adds. R2C-26 fixes it in E-G fix round: feed-free label tests, the stand-in rename and R2C-26 #1486 (20507ad8).

Skips (89; each names an artifact, source directory or engine this environment lacks):

  • test_ledger_artifact.py (2): :613: "NZ_CHRONICLE_ARTIFACT is unset; real NZ Chronicle artifact unavailable"; :638: "NZ_CHRONICLE_ARTIFACT is unset"
  • test_nz_as_area_crosswalk.py (3): :1085, :1096, :1105: "Pinned ArcGIS bytes are distributed separately; set NZ_AS_AREA_SOURCES to their directory to replay the crosswalk"
  • test_nz_axiom_input_closure.py (2): :535, :610: "needs axiom_rules_engine with its dense extension and a rulespec-nz checkout at POPULACE_RULESPEC_NZ"
  • test_nz_family_reconciliation.py (9): :264 x2, :332 x2, :352, :376 x2, :485, :563: "NZ_CHRONICLE_ARTIFACT is unset; public NZ feed audit unavailable"
  • test_nz_joint_geography.py (12): :394: "Set NZ_CHRONICLE_ARTIFACT to replay the pinned published TA ERP facts"; :592, :602, :635 x7, :654, :671: "Set NZ_AS_AREA_SOURCES and NZ_JOINT_GEOGRAPHY_SOURCES for pinned-source replay"
  • test_nz_msd_pre_activation_reconciliation.py (1): :121: "NZ_CHRONICLE_ARTIFACT supplies the staged pinned feed audit"
  • test_nz_reference_aging.py (16): :88, :137 x10, :167, :210, :225, :261, :307: "NZ_CHRONICLE_ARTIFACT is not set"
  • test_nz_reference_intervals.py (1): :435: "NZ_CHRONICLE_ARTIFACT is unset; public NZ feed audit unavailable"
  • test_nz_target_references.py (41): :355, :463, :583 x2, :835, :846 x2, :865, :900, :917, :960 x17, :988, :1040, :1070, :1099, :1137 x6, :1158, :1178, :1228, :1311: "NZ_CHRONICLE_ARTIFACT is unset; public NZ feed audit unavailable"
  • test_nz_transport_declarations.py (2): :290 x2: "Pinned feed not staged"

https://claude.ai/code/session_01WR8LHbmJT4MutJib6vfq8v

🤖 Generated with Claude Code

Belgium's hub prepared these two hunks for H2 (R2C-20). The shared
row-by-row reference (test_support/microcosm_frame/concept_reference.py)
gains a CoresidentAdult case: a person is true when their household id is
present and another person row with that household id has a present age
at or above min_age. Without the case, reference_values reaches its
"no reference for" AssertionError on any coresident_adult binding, and
test_encode_matches_a_row_by_row_reference runs over every committed
mapping. tools/build_nz_transport_graph.py adds coresident_adult to the
transforms whose closure dtype is bool, so closure_entry_dtype returns
"bool" for the binding instead of raising "unsupported transform".

The diff applies to this base with the builder hunk two lines lower, and
`git diff | git patch-id --stable` printed
e3f8d1c790817d1c3e6677e68fd1f0a564f68af2 before this commit, the id the
hub gave. No other file changes here.

Claude-Session: https://claude.ai/code/session_01WR8LHbmJT4MutJib6vfq8v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NZ's concept mapping now binds
nz_super_shares_residence_with_person_at_least_adult_age on Person with
{"kind": "coresident_adult", "min_age": 18} (R2C-20 H2). The edit is in
the frame adapter's nz.json; tools/build_nz_transport_graph.py copies that
file into N/concept_mapping.json. The 18 copies rulespec-nz core.yaml's
parameter nz_super_living_alone_adult_age (s 4A(3) and (4)) by hand,
because no RuleSpec formula reads it. The binding's note says so, and says
that the frame's household stands in for the statute's residence.

The closure entry moves from defaulted false to encoded by the concept
binding. concepts.encode@1 therefore stops writing the default
(column_kernels.py:407, input_closure.py:520-526) and writes the binding's
values (concept_mapping.py:2293-2318). The exempt-adults entry stays
defaulted false. Its reason now says that every single person who shares
a home with an adult gets the sharing rate, including one whose only
co-resident adults are exempt dependent children or temporary visitors.

The move made three statements false, and this commit corrects them:
reporting_classifiers.json's sharing disclosure and its method doc now
describe the understatement for that exempt group, and the graph's
pending_outputs no longer lists the NZS sharing binding (R2C-5).

The three NZ builders and refresh_concept_coverage.py regenerated
N/concept_mapping.json, export_replay_bindings.json's closure hash,
transport_graph.json's pending_outputs, and the axiom-nz coverage golden
and report (89 to 90 covered inputs). A load of the spec regenerated
tests/golden/nz_country_spec.json. Each builder's --check passes.

Pins: test_reporting_disclosure_cites_the_sharing_home_default_record
becomes ..._binding_record and asserts the encoded record, the exempt
reason and the new note. test_the_nz_roster_classes moves one input from
closure_default to derived (93 to 92, 322 to 323), as the H2 brief
allows; compose._support_roster classes the column derived, owned by
nz.encode.person, and its uncovered sources are the test's pending pair.

TestClosurePartition.test_the_committed_nz_surface_partitions_as_the_design_table
(a shared test) now fails on nz_super's partition (defaulted 11 to 10,
encoded 2 to 3). This commit leaves it unedited pending a hub ruling. 18
other shared tests (17 in test_calibration_columns.py and
test_packaged_precalibration_stages_pass_the_stage_guard[nz]) fail
identically at 891cb9f, before this binding.

Claude-Session: https://claude.ai/code/session_01WR8LHbmJT4MutJib6vfq8v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two tests run the real concepts.encode@1 kernel on NZS core.yaml with
NZ's packaged concept_mapping.json and axiom_input_closure.json. Each
test runs the kernel twice: once with the packaged resources (after H2),
and once with a copy that drops the binding and restores the defaulted
false entry (before H2).

On the toy's nz.create frame, the twenty persons aged 65 or over each
live with a partner aged 65 to 79 and a 16-year-old. Every one reads
false before and true after. The toy has no superannuitant who lives
alone or whose only co-resident is under 18, so a small frame covers
those: alone (70) false, with a 40-year-old true, only with a 16- or
17-year-old false, with an 18-year-old true, and a person with no
household id false. Before H2 every person in both frames reads false.

Checks: both tests pass. A scratch script compared the binding with a
direct read of household composition over all 120 toy persons and
found no mismatch.

Claude-Session: https://claude.ai/code/session_01WR8LHbmJT4MutJib6vfq8v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude and others added 3 commits November 3, 2026 10:53
9cc37a5 moves nz_super_shares_residence_with_person_at_least_adult_age
from defaulted to encoded in N/axiom_input_closure.json, so the nz_super
binding's partition in
TestClosurePartition.test_the_committed_nz_surface_partitions_as_the_design_table
moves one root from defaulted to encoded: defaulted 11 to 10, encoded 2
to 3. The hub's ruling R2C-28 (#1183) clears this one line, at :810. The
test's other assertions stand unchanged: 158 roots, and check == [].

Checks: test_transport_evidence_kernels.py passes in full (156 tests).
Ruff check and format --check pass on the file.

Claude-Session: https://claude.ai/code/session_01WR8LHbmJT4MutJib6vfq8v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An independent review of H2 found that the sharing disclosure's evidence
pointed only at the shares-residence record. 9cc37a5 left that record
encoded, with no value or reason, while the disclosure's note blames the
exempt-adults default. The evidence now cites both closure records, and
the two NZ citation tests resolve both.

The same review tightened the prose. The exempt-adults reason now names
the sharing rate's source (Schedule 1 Part 1 clause 1(b)) instead of
s 4A, says which person s 4A counts as living alone, and adds that S5's
reg 17 superannuitant base rate reads the same weekly amount
(as_rate_bridge.json:462-472). The binding note no longer says the
mapping keeps min_age in step with the parameter, because nothing checks
the two. The method doc's paragraph now opens with a sentence that stands
alone.

The transport builder copied the note into concept_mapping.json and wrote
export_replay_bindings.json's new closure hash. Rendering the loaded
spec's summary rewrote nz_country_spec.json. Each builder's --check
passes.

Checks: 12 test files that read the edited resources pass (2123 passed,
4 skipped for a missing feed or engine): test_nz_spec_package.py,
test_country_spec.py, test_axiom_concept_mapping.py,
test_nz_axiom_input_closure.py, test_spec_source_citations.py,
test_concept_mapping.py, test_input_closure.py,
test_transport_evidence_kernels.py, test_nz_transport_declarations.py,
test_spec_engine_country_bundles.py, test_spec_only_country_packages.py
and test_nz_transport_toy_graph.py. Ruff passes on the edited test file.

Claude-Session: https://claude.ai/code/session_01WR8LHbmJT4MutJib6vfq8v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 215 (#1183 comment 6616135497, nit 2) found that the small-frame
test held one person with no household id, so it couldn't tell "a null id
shares with nobody" from "null ids group together": mutant B
(use_na_sentinel=False in _coresident_adult_flags) passed every H2 test.
Person 11, aged 30, now has no household id either, and both 10 and 11
must still read false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis changed the base branch from nzhub/g8r2c-g-toy to main November 4, 2026 00:14
MaxGhenis and others added 2 commits November 3, 2026 19:16
GitHub reported H2 as conflicting with main once G landed. main and
H2 share two merge bases, G's toy head 85d55af and main's 4a36edc,
and a merge over either one alone conflicts in NZ files. Git's
recursive merge over both is clean. This commit records that result:
against main it changes exactly H2's own lines in H2's 15 files and
nothing else, so the PR's diff doesn't change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This PR's CI passed at 01:5xZ against main f8f543a. Main has since
taken six PRs (#1345, #1493, #1467, #1426, #1465, #1391), 178 files,
none of them this PR's. #1493 changes how the target-compilation
kernels hash their code, so CI reruns here on today's main before the
PR lands. The merge is clean over the single base f8f543a, and
against main it changes only this PR's own lines, file for file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review November 4, 2026 18:21
@MaxGhenis
MaxGhenis merged commit 358b1c1 into main Nov 4, 2026
11 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged by the NZ hub (session local_e407a527) at 10bcfd1 with --merge. Gates: gh pr checks exit 0; MERGEABLE; not draft; no CHANGES_REQUESTED review. Reviewed head: d82a2ba; the merged head differs only by a base update, and the PR's own changed lines are identical to the reviewed head's. Review 222 (#1183 comment 6616766311): APPROVE_WITH_NITS at d82a2ba, no fixes owed. Belgium's OK on H2's two shared places (1b56b91) holds: every later commit changes only NZ's toy file or merges main. CI run 41851097624 tested 10bcfd1 merged into main 875506e. Main since then (0f0e777 and ad67d0b: #1473, #1506) changes 23 files, none of them NZ's or H2's: US feed pins, Modal telemetry, docs, US target parity, and three telemetry env names in the root conftest's fixture.

MaxGhenis added a commit that referenced this pull request Nov 4, 2026
H2 and #1501 landed on main, and both change test_nz_transport_toy_graph.py, as this PR does. git merges every other file without a conflict. The one conflict sits at the toy file's end, where H2's section and this PR's section both append. The resolution keeps main's file whole, #1501's edits included, then appends this PR's section unchanged: blob 5cb8c87. This PR's diff against main still adds the same 158 toy-file lines in the same order, and its other three files keep their patch-ids. No top-level name in the resolved file repeats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Nov 4, 2026
Three PRs reached main after this PR's last CI run, which tested 17e2643 on main 0f0e777: #1506, #1485 (H2) and #1501. None of them changes a file this PR changes, and git merge-tree writes tree 79ee630 with no conflict. H2 adds an NZ coresident binding and teaches test_support's reference concepts the coresident_adult transform, which the concept-mapping differential beside this PR's encode change reads, so this merge has CI test the two together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis deleted the nzhub/h2-nzs-coresident-binding branch November 4, 2026 18:39
MaxGhenis added a commit that referenced this pull request Nov 4, 2026
…1488)

* Prove extension calibration nodes' measurement producers in compose

The extension pass of compose_transport_graph compiled the extended graph
and reran the ancestry guard, but never ran _check_measurement_proofs. So a
calibration-tier scenario's calibrate.* node could calibrate against a
money-scaled measurement that no proof covered: measurement_proofs reads
only the declared graph's rows and never names those nodes.

The extension pass now runs the check after the ancestry guard, as the
skeleton pass does. For a calibration node that an extension adds, the
compiled graph proves the producer under R2C-1's rule: exactly one compiled
ancestor declares the measurement, and that ancestor outputs every proof
column. R2C-15's only-writer rule applies as before. A skeleton calibration
node keeps the declared comparison, which the pass now also runs against
the extended graph.

Tests: unit cases on the compiled toy; a differential property that the
declared proof and the compiled extension proof agree, refusal for refusal,
on chains of declarers and writers; and NZ's synthetic graph with four V3
mutants (M1, M2, M4 and the NZ hub's G7), each of which composes without
this change.

* Move the NZ-toy measurement-proof tests into NZ's toy-graph file

The five tests that run the extension proof on NZ's synthetic graph name the
toy's V3 nodes, and the NZ hub owns the toy and its node ids. They move, with
their helpers, from test_transport_measurement_proof.py into
test_nz_transport_toy_graph.py and use that file's module `toy` fixture, so a
change to the toy edits only NZ's files. The shared file keeps the unit tests
on the compiled toy and the differential property.

The move adds G7 in the NZ hub's placement. V3's calibration keeps its base on
V3, while preparation, the compiled targets and the problem move into the
review version that rewrites both measured columns. The problem reads the
rewritten columns, so only the calibration's artifact edge from the problem
puts the rewrite among its compiled ancestors. The earlier G7 opens the
calibration from the review version, where the rewrite joins its base, and
doesn't need that edge.

* Refuse a malformed measurement_id in the compiled proof check

Review r2 found that the compiled check skipped a node whose
measurement_id was neither None nor a nonempty string, while the declared
check refuses it. With the string producer still in place, an extra
upstream node declaring measurement_id 7 composed, though the same param on
a declared row parks every measurement. r2 showed it on NZ's toy graph,
where V3's earnings.cents with id 7 composed.

_check_measurement_proofs now refuses such a node with the declared
check's reason before it proves any producer, and reads an explicit None as
no declaration, as measurement_proofs does.

The new test gives a node upstream of calibration a measurement_id of 7, ""
or None, and runs the declared check and the compiled check on a
calibration node that an extension adds and on a skeleton one. Both checks
refuse 7 and "" with the same reason, and both ignore None. Without the
fix, the compiled check accepts the four 7 and "" cases, and those cases
fail.

The commit also fixes three docstrings that r2 flagged: the test module's
account of the compiled tests, which patch in a proof; _chain's, whose
nodes chain by step columns; and compose's module docstring, which now
names gate_inputs, the other explicit way an extension may add a skeleton
predecessor.

* Say the compiled proof check scans malformed ids only under a proof

_check_measurement_proofs returns before its malformed-id scan when the declaration proves no measurement, and its docstring now says so. The independent review of b64eb6c noted that the sentence read as unconditional.

Two merges of main bring G (#1478) and then H2 (#1485) and #1501. The second
keeps main's test_nz_transport_toy_graph.py whole and appends this PR's
section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Nov 4, 2026
Since the merge base 875506e, main gained #1473, #1506, H2 (#1485),
#1501, #1480 and #1488. The merge is clean. Two files changed on both
sides, in separate hunks: compose.py (#1488 on main's side) and
test_transport_evidence_kernels.py (H2 on main's side).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Nov 5, 2026
…losure (S2b PR 1) (#1482)

* Add binding-local views to the Axiom adapter

AxiomEngine takes an optional views sequence of AxiomView declarations:
a logical engine entity no frame entity maps to (rulespec-be's Child), one
bool person selector column, and an outside value per output. The adapter
gathers the selected person rows in table order, runs the module's program
on the logical entity, checks the engine's array against the selected count,
and scatters it back through the same mask; every other row holds the
outside value, cast through the output's graph dtype. An empty selection
makes no engine call, and a null or non-bool selector refuses.

View outputs resolve to the person entity, so callers that key on
(binding entity, variable) read them unedited. Each view's program compiles
under its own cache key, re-checks the pinned RuleSpec tree first, refuses
dense relations, and contributes its derived metadata, so a Child-only
module has metadata. The selector joins the person entity's
relation_dependencies; view_root_inputs() reports each view's roots.
axiom_engine_ref writes views only when present, so every existing
reference is unchanged. The view code stays in axiom.py, because
source_hash hashes defining files and does not follow imports.

* Partition binding-local view roots in the input closure

closure_partition no longer refuses a binding that declares views, and
BINDING_LOCAL_VIEW_MESSAGE is gone. Per binding, each view's logical entity
(Child) joins the live entities and maps to the person table, so a view's
roots partition against the closure's Child entries and the existing frame
check reads them on the person slice. Roots on that entity stay unmapped on
every other binding, and unreached_roots still reads only the binding's
engine entity, so it cannot excuse a view root.

engine_root_inputs adds each view's roots under its logical entity through
AxiomEngine.view_root_inputs(), and refuses a view binding on an injected
adapter. binding_view_entities reads a binding's logical entities for the
closure and compose.

* Parse binding-local views in the registry and check them in compose

The registry parses a binding's views block into AxiomView declarations
(logical_entity, selector {column}, outside {name: value}) and builds the
adapter with them. It refuses an empty list, views beside relations, a
binding off the person entity, a logical entity that names a mapped engine
entity, a repeated logical entity, a selector with any field but column, a
structural selector, a selector that names a binding output, and an outside
key that names no binding variable. Once the adapter is pinned, it checks
each view's program for relations in place of the binding entity's program,
requires each view's outside keys to equal the binding's variables on its
logical entity, and refuses a view input named like a root input of the
binding entity's program. _check_engine compares an injected adapter's views
with the binding's and refuses views on an injected Python adapter.

binding_view_dependencies reports each selector as a whole-table person
slice, the same columns AxiomEngine.relation_dependencies reports to the
rules kernels. Compose unions them with the relation dependencies for every
rules node, refuses a view node whose person slice omits an input the
closure encodes (other than through a graph node) or defaults on the view's
logical entity, and requires each selector on the closure producer's person
slice.

* Pin NZ-shaped and relation engine references to goldens from the base

No NZ CI test builds an axiom_engine_ref for NZ's rows: NZ's engine-free
graph injects Python toy engines. The new golden holds references generated
at main 4a36edc for NZ's six binding rows (unreached_roots included) over a
synthetic tree, and for the registry and adapter relation fixtures, all
built engine-free through build_transport_registry and axiom_engine_ref.
The test compares them byte for byte, so a reference that gains
"views": [] or "views": null without a declared view fails.

A second test, gated on POPULACE_RULESPEC_NZ, builds NZ's six real
references from the tree alone and compares them with a frozen copy the NZ
hub writes at the base.

* Add the changelog fragment for binding-local views

* Test that every rules kernel requires a view's whole selector slice

SimulateRulesKernel._require_relation_inputs reads the adapter's
relation_dependencies, which now carry each view's selector, so a node
whose person slice omits or masks the selector refuses before the engine
runs. The test pins that path for a view adapter.

* Pin view code to axiom.py and the no-view outputs to the base path

The placement lint now walks every import node in axiom.py, relative and
function-body imports included, and a second test requires AxiomView and
every view helper to carry axiom.py's module name. A helper moved into a
module axiom.py already imports (materialize, concept_mapping) would leave
the simulate.* hashes unmoved by an edit to it; the module-name test
refuses that move, which the import pin alone passes.

P4 gains an output clause: with views=None, materialize returns the same
names, order, dtypes and values, through the same engine calls, as a
reference copy of main 4a36edc's no-view path kept in the test. NZ's CI
graph injects toy engines and never runs AxiomEngine.materialize, so this
is the executable guard on view-free outputs.

* Class a view's defaulted roots as person columns in the support roster

_support_roster mapped each defaulted closure entry to a frame entity
through entity_names alone, so a defaulted Child root, which PR 2 writes on
the person table, mapped to no entity and missed the closure_default class.
_closure_defaults now maps a binding-local view's logical entity to the
person entity for entries of that binding's module. Without views the
mapping is unchanged.

* Freeze NZ's real-tree engine references at the base

test_nz_real_refs_match_their_frozen_copy compares NZ's six real
references with a frozen copy, and wrote none without a rulespec-nz
tree. This adds the copy, generated at PR 1's base (main 4a36edc) from
a git archive of rulespec-nz 8dc2507d, the commit NZ's bindings pin.

The base run froze the file, and a second base run passed against it.
At this branch's head the test passes too, so the six references keep
their bytes with views in place. The CI golden test passed at the base
as well, which checks that axiom_engine_refs.json came from the base.

* Document binding-local views in the declared-units guide

The guide said binding-local views remained separate work. It now
describes the declaration, the gather and scatter, the outside values,
the registry's refusals, the selector slice, the closure's treatment of
view roots, and the row-locality assumption that the skip-gated
real-engine differential tests.

* Union shared-path closure roots to remove binding-order dependence

* Cover both logical views to catch dropped input roots

* Validate complete outside declarations before pinning bindings

* Preserve outside value types in view equality and hashing

* Exercise view property edges across every graph dtype

* Type and stabilize real Child differential inputs

* Freeze CI reference bindings to isolate NZ edits

* Clarify view evidence, serialization and selector refusals

* Accept overrides of a view selector that a program reads

The first review of the fix round (P2) found that 0d6201a's selector
refusal also refused an override of a selector that a Person program
reads as a root input. The registry accepts that shape, variables()
lists the selector, and before 0d6201a the counterfactual bridge
applied such an override. validate_input_override now names a selector
only when no program reads it, which is exactly when the bridge refuses
the override as outside the engine's inputs, so the check changes no
override the bridge applies.

test_a_selector_that_a_program_reads_stays_overridable drives the
counterfactual bridge with a Person program that reads is_child: the
override reaches that program's input and the rows the view selects. It
fails on 0d6201a's check. The refusal test now also runs through the
bridge, which calls no program. The guide states the rule.

* Correct the two docstrings that review B flagged

`_real_inputs` said the pinned engine refuses a wrong-typed input by name.
At axiom-rules-engine 753ce7d, `src/dense.rs` reports a bool, text or date
column as a type mismatch when a program needs a number from it ("expected
decimal-compatible dense column" for an arithmetic operand, "{side} side of
comparison is not numeric" for a comparison), and neither message names the
input. The docstring now says that.

`axiom_ref_goldens.py`'s module docstring wrote `"\n"` in a non-raw string,
so it rendered a line break where the regeneration recipe needs the two
characters. It now escapes the backslash.

Two merges of main bring this branch up to date: `02265bb5` merges main `875506e8`, and `36800b14` merges main `3e7a7045` (#1473, #1506, H2 #1485, #1501, #1480 and #1488). git merged both without conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants