From e277e6f7ef2e01a73e3ed8e0940eb4764a6f179e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 9 Nov 2026 05:10:35 -0500 Subject: [PATCH 1/4] Test the support roster's pending class on a toy spec _support_roster classes a money column that no node produces as pending. Today only test_the_nz_roster_classes reaches that branch, through NZ's packaged spec, and #1547 gives NZ's last such columns real producers. These tests drive the branch from the toy graph's money routes, and check that a derived row names a pending source. Co-Authored-By: Claude Opus 5.5 --- .../shared/test_transport_evidence_kernels.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py index 9e9709f7e3..f579810fa3 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py @@ -87,6 +87,7 @@ Slice, ) from microcosm.graph.canonical import canonical_json +from test_support.microcosm_build.money_normalization import graph_declaration from test_support.microcosm_build.transport_composed import make_composed_fixture from test_support.paths import paths_for @@ -2983,6 +2984,83 @@ def test_a_rulespec_name_with_another_codec_is_uncovered(self) -> None: ), outcome.result.failures +def _money_battery_spec(*inputs, nodes=()): + """CREATE and a battery over the toy graph's money routes; no node maps money.""" + + return { + "country": "xx", + "resources": { + "money_normalization": graph_declaration(), + "transport_graph": { + "schema_version": 1, + "country": "xx", + "sources": [], + "nodes": [ + { + "id": "x.open", + "kernel": "transport.create@1", + "structural": "create", + }, + *nodes, + { + "id": "x.gates", + "kernel": "gates.battery@1", + "inputs": [ + {"entity": entity, "columns": [column]} + for entity, column in inputs + ], + }, + ], + }, + }, + } + + +class TestMoneyAwaitingItsProducer: + @pytest.mark.parametrize( + ("entity", "column", "reason"), + ( + ("person", "interest_income", "awaits its declared producer"), + ("household", "rent", "awaits toy rent bands"), + ("person", "dividend_income", "awaits a donor that carries it"), + ), + ids=("normalized", "quantile_mapped", "pending"), + ) + def test_a_money_column_no_node_produces_is_pending( + self, entity, column, reason + ) -> None: + # With no owner after CREATE, the money route decides the class. + # Without it, a money concept would read as copied from the donor. + spec = _money_battery_spec((entity, column)) + (row,) = compose._support_roster(spec)["columns"] + assert row["class"] == "pending" + assert row["reason"] == reason + + def test_a_donor_concept_without_a_money_route_is_copied(self) -> None: + spec = _money_battery_spec(("person", "age")) + (row,) = compose._support_roster(spec)["columns"] + assert row["class"] == "copied" + + def test_a_derived_row_names_the_pending_source_it_reads(self) -> None: + # The support gate fails a derived row on any source outside + # SUPPORT_COVERED, so the roster must report the pending source. + derive = { + "id": "x.derive", + "kernel": "toy.derive@1", + "inputs": [{"entity": "household", "columns": ["rent"]}], + "outputs": [ + {"entity": "person", "column": "rent_share", "dtype": "float64"} + ], + } + spec = _money_battery_spec(("person", "rent_share"), nodes=(derive,)) + (row,) = compose._support_roster(spec)["columns"] + assert row["class"] == "derived" + (source,) = row["sources"] + assert (source["entity"], source["column"]) == ("household", "rent") + assert source["class"] == "pending" + assert source["class"] not in compose.SUPPORT_COVERED + + class TestDeclaredStructuralRows: def test_a_row_no_node_writes_is_refused(self, nz_composed) -> None: # H46: household.employment_income is a stale, misspelled row. From 7d739f12d93013210cd70d2aa2aa837885414fa5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 9 Nov 2026 06:32:59 -0500 Subject: [PATCH 2/4] Run the toy derived rows through the support gate The review of e277e6f7 found that the last assertion pinned compose.SUPPORT_COVERED, which only tests read, while the gate reads gate_bindings._SUPPORT_COVERED. The derived-row test now runs the toy roster through the support gate and checks that the failure names the pending source. It also covers a derived row that reads another derived row, since nothing else guards that path once #1547 lands, and it pins the source's reason. A differential test ties the two covered-class sets together. Co-Authored-By: Claude Opus 5.5 --- .../shared/test_transport_evidence_kernels.py | 78 ++++++++++++++----- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py index f579810fa3..b1fdeab8b5 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py @@ -2985,7 +2985,11 @@ def test_a_rulespec_name_with_another_codec_is_uncovered(self) -> None: def _money_battery_spec(*inputs, nodes=()): - """CREATE and a battery over the toy graph's money routes; no node maps money.""" + """CREATE, ``nodes`` and a battery over ``inputs``, with the toy money routes. + + CREATE declares no outputs, so a money column has an owner only if ``nodes`` + adds one. + """ return { "country": "xx", @@ -3016,6 +3020,21 @@ def _money_battery_spec(*inputs, nodes=()): } +def _derived_person_column(column, entity, read): + """A node that owns ``person.`` and reads ``.``.""" + + return { + "id": f"x.{column}", + "kernel": "toy.derive@1", + "inputs": [{"entity": entity, "columns": [read]}], + "outputs": [{"entity": "person", "column": column, "dtype": "float64"}], + } + + +_RENT_SHARE = _derived_person_column("rent_share", "household", "rent") +_RENT_SHARE_HALF = _derived_person_column("rent_share_half", "person", "rent_share") + + class TestMoneyAwaitingItsProducer: @pytest.mark.parametrize( ("entity", "column", "reason"), @@ -3041,24 +3060,47 @@ def test_a_donor_concept_without_a_money_route_is_copied(self) -> None: (row,) = compose._support_roster(spec)["columns"] assert row["class"] == "copied" - def test_a_derived_row_names_the_pending_source_it_reads(self) -> None: - # The support gate fails a derived row on any source outside - # SUPPORT_COVERED, so the roster must report the pending source. - derive = { - "id": "x.derive", - "kernel": "toy.derive@1", - "inputs": [{"entity": "household", "columns": ["rent"]}], - "outputs": [ - {"entity": "person", "column": "rent_share", "dtype": "float64"} - ], - } - spec = _money_battery_spec(("person", "rent_share"), nodes=(derive,)) - (row,) = compose._support_roster(spec)["columns"] + @pytest.mark.parametrize( + ("column", "nodes"), + ( + ("rent_share", (_RENT_SHARE,)), + ("rent_share_half", (_RENT_SHARE, _RENT_SHARE_HALF)), + ), + ids=("direct", "through-a-derived-row"), + ) + def test_a_derived_row_fails_the_gate_on_the_pending_source_it_reads( + self, column, nodes + ) -> None: + # The walk flattens a derived input into that input's own sources, + # so the pending source reaches the row at any depth. + spec = _money_battery_spec(("person", column), nodes=nodes) + roster = compose._support_roster(spec) + (row,) = roster["columns"] assert row["class"] == "derived" - (source,) = row["sources"] - assert (source["entity"], source["column"]) == ("household", "rent") - assert source["class"] == "pending" - assert source["class"] not in compose.SUPPORT_COVERED + assert row["sources"] == [ + { + "entity": "household", + "column": "rent", + "class": "pending", + "reason": "awaits toy rent bands", + } + ] + donor, _ = _donor_and_terminal() + terminal = _support_frame({("person", column): [1e9] * 6}) + ranges, bounds, rn, bn = _run_support(roster, donor, terminal) + outcome = _support_outcome(ranges, bounds, rn, bn, terminal) + assert outcome.status is GateStatus.FAILED + assert any( + f"person.{column} (derived): sources without coverage: " + "household.rent (pending)" in line + for line in outcome.result.failures + ) + + def test_the_roster_and_the_gate_agree_on_the_covered_classes(self) -> None: + # compose and gate_bindings each hold this set; the gate reads its own. + from microcosm.build.transport.gate_bindings import _SUPPORT_COVERED + + assert compose.SUPPORT_COVERED == _SUPPORT_COVERED class TestDeclaredStructuralRows: From 77ab4564f7bbc86088c1dd8bb34c0f34efde30b5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 9 Nov 2026 14:02:19 -0500 Subject: [PATCH 3/4] Pin the support gate's derived-row line and the walk's breadth The second review found two gaps in TestMoneyAwaitingItsProducer. The toy frame fails the support gate on four columns that the one-row roster never names, so the derived test's status assertion held whatever the row read. And every case read one input with one column, so after #1547 no test would catch a walk that reads only a node's first input or an input's first column. The derived cases now run the gate with the toy roster's rows beside their own, so the derived line is the only failure, and a control whose row reads a copied source passes the same gate. A new case reads two columns of one input and a second input; it checks that the row lists all three sources and that the gate names both pending ones. The battery spec's docstring now gives the reason CREATE owns no column. Co-Authored-By: Claude Opus 5.5 --- .../shared/test_transport_evidence_kernels.py | 88 +++++++++++++++++-- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py index b1fdeab8b5..0cee087e7a 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py @@ -2987,8 +2987,8 @@ def test_a_rulespec_name_with_another_codec_is_uncovered(self) -> None: def _money_battery_spec(*inputs, nodes=()): """CREATE, ``nodes`` and a battery over ``inputs``, with the toy money routes. - CREATE declares no outputs, so a money column has an owner only if ``nodes`` - adds one. + CREATE is structural and owns no column, so a money column has an owner only + if ``nodes`` adds one. """ return { @@ -3035,6 +3035,26 @@ def _derived_person_column(column, entity, read): _RENT_SHARE_HALF = _derived_person_column("rent_share_half", "person", "rent_share") +def _support_gate_on(roster): + """The support gate on the toy frames, with ``roster``'s rows added to the toy roster. + + The toy roster alone passes this gate, so each failure comes from ``roster``. + """ + + donor, terminal = _donor_and_terminal() + ranges, bounds, rn, bn = _run_support( + _toy_roster(more=roster["columns"]), donor, terminal + ) + return _support_outcome( + ranges, + bounds, + rn, + bn, + terminal, + structural=[("household", "source_household_id")], + ) + + class TestMoneyAwaitingItsProducer: @pytest.mark.parametrize( ("entity", "column", "reason"), @@ -3085,15 +3105,65 @@ def test_a_derived_row_fails_the_gate_on_the_pending_source_it_reads( "reason": "awaits toy rent bands", } ] - donor, _ = _donor_and_terminal() - terminal = _support_frame({("person", column): [1e9] * 6}) - ranges, bounds, rn, bn = _run_support(roster, donor, terminal) - outcome = _support_outcome(ranges, bounds, rn, bn, terminal) + outcome = _support_gate_on(roster) assert outcome.status is GateStatus.FAILED - assert any( + (line,) = outcome.result.failures + assert line.startswith( f"person.{column} (derived): sources without coverage: " - "household.rent (pending)" in line - for line in outcome.result.failures + "household.rent (pending). " + ) + + def test_a_derived_row_on_a_copied_source_passes_the_gate(self) -> None: + # The control for the test above: the same gate passes a derived row + # whose one source the donor covers. + node = _derived_person_column("age_share", "person", "age") + roster = compose._support_roster( + _money_battery_spec(("person", "age_share"), nodes=(node,)) + ) + (row,) = roster["columns"] + assert row["sources"] == [ + {"entity": "person", "column": "age", "class": "copied"} + ] + outcome = _support_gate_on(roster) + assert outcome.status is GateStatus.PASSED, outcome.result.failures + + def test_a_derived_row_lists_each_column_of_each_input_its_owner_reads( + self, + ) -> None: + # Every input of the owner, and every column of each, reaches the row, + # and the gate names each pending source. + node = { + "id": "x.mix", + "kernel": "toy.derive@1", + "inputs": [ + {"entity": "person", "columns": ["age", "dividend_income"]}, + {"entity": "household", "columns": ["rent"]}, + ], + "outputs": [{"entity": "person", "column": "mix", "dtype": "float64"}], + } + roster = compose._support_roster( + _money_battery_spec(("person", "mix"), nodes=(node,)) + ) + (row,) = roster["columns"] + assert row["sources"] == [ + { + "entity": "household", + "column": "rent", + "class": "pending", + "reason": "awaits toy rent bands", + }, + {"entity": "person", "column": "age", "class": "copied"}, + { + "entity": "person", + "column": "dividend_income", + "class": "pending", + "reason": "awaits a donor that carries it", + }, + ] + (line,) = _support_gate_on(roster).result.failures + assert line.startswith( + "person.mix (derived): sources without coverage: " + "household.rent (pending), person.dividend_income (pending). " ) def test_the_roster_and_the_gate_agree_on_the_covered_classes(self) -> None: From 8f050b76c9231f3e257e79098c6ebf4df5390fb8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 9 Nov 2026 17:06:56 -0500 Subject: [PATCH 4/4] Follow the breadth case through a derived input The third review found that a flatten keeping only a derived input's first source survives every file it ran after #1547. The derived cases read one column through their derived input, and the breadth case reads no derived input. Before #1547, test_the_nz_roster_classes caught that mutant on an NZ row with two pending sources. The breadth case now also runs on a row that reads the mix column through a derived input. That row must list the same three sources, and the gate must name both pending ones. Co-Authored-By: Claude Opus 5.5 --- .../shared/test_transport_evidence_kernels.py | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py index 0cee087e7a..7977e3ea70 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_evidence_kernels.py @@ -3033,6 +3033,16 @@ def _derived_person_column(column, entity, read): _RENT_SHARE = _derived_person_column("rent_share", "household", "rent") _RENT_SHARE_HALF = _derived_person_column("rent_share_half", "person", "rent_share") +_MIX = { + "id": "x.mix", + "kernel": "toy.derive@1", + "inputs": [ + {"entity": "person", "columns": ["age", "dividend_income"]}, + {"entity": "household", "columns": ["rent"]}, + ], + "outputs": [{"entity": "person", "column": "mix", "dtype": "float64"}], +} +_MIX_HALF = _derived_person_column("mix_half", "person", "mix") def _support_gate_on(roster): @@ -3127,22 +3137,22 @@ def test_a_derived_row_on_a_copied_source_passes_the_gate(self) -> None: outcome = _support_gate_on(roster) assert outcome.status is GateStatus.PASSED, outcome.result.failures + @pytest.mark.parametrize( + ("column", "nodes"), + ( + ("mix", (_MIX,)), + ("mix_half", (_MIX, _MIX_HALF)), + ), + ids=("direct", "through-a-derived-row"), + ) def test_a_derived_row_lists_each_column_of_each_input_its_owner_reads( - self, + self, column, nodes ) -> None: - # Every input of the owner, and every column of each, reaches the row, + # Every input of the owner, and every column of each, reaches the row. + # Through a derived input, every one of that input's own sources does, # and the gate names each pending source. - node = { - "id": "x.mix", - "kernel": "toy.derive@1", - "inputs": [ - {"entity": "person", "columns": ["age", "dividend_income"]}, - {"entity": "household", "columns": ["rent"]}, - ], - "outputs": [{"entity": "person", "column": "mix", "dtype": "float64"}], - } roster = compose._support_roster( - _money_battery_spec(("person", "mix"), nodes=(node,)) + _money_battery_spec(("person", column), nodes=nodes) ) (row,) = roster["columns"] assert row["sources"] == [ @@ -3162,7 +3172,7 @@ def test_a_derived_row_lists_each_column_of_each_input_its_owner_reads( ] (line,) = _support_gate_on(roster).result.failures assert line.startswith( - "person.mix (derived): sources without coverage: " + f"person.{column} (derived): sources without coverage: " "household.rent (pending), person.dividend_income (pending). " )