From d901f0db0f8a79bc7c6285a11f36def2c1e98214 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 23:21:17 -0500 Subject: [PATCH 1/3] Fix the NZ bridge doc and tests the #1440 review flagged The bridge paragraph predated H29 and never named nz.as.bridge. The childless-family tests cast the column themselves and claimed the adapter raises, but the masked override returns float64, so _batch_from_table takes its float branch. Both tests now read that function. A new test runs a mixed couple through S5's base rate in both person orders, because a `first` aggregation passes one order and fails the other. Co-Authored-By: Claude Opus 5.5 --- .../nz-as-bridge-review-follow-up.fixed.md | 1 + docs/transport-entitlement.md | 15 ++++--- .../engine_free/shared/test_nz_as_bridge.py | 45 ++++++++++++++----- 3 files changed, 45 insertions(+), 16 deletions(-) create mode 100644 changelog.d/nz-as-bridge-review-follow-up.fixed.md diff --git a/changelog.d/nz-as-bridge-review-follow-up.fixed.md b/changelog.d/nz-as-bridge-review-follow-up.fixed.md new file mode 100644 index 0000000000..962f244984 --- /dev/null +++ b/changelog.d/nz-as-bridge-review-follow-up.fixed.md @@ -0,0 +1 @@ +Correct the NZ Accommodation Supplement bridge's documentation and tests after the review of #1440. `docs/transport-entitlement.md` now names `nz.as.bridge` as the shared population that runs the bridge for S0, S1, S2, S3 and S5. The childless-family tests read the Axiom adapter's own batch builder, and a new test runs a mixed couple through S5's base rate. diff --git a/docs/transport-entitlement.md b/docs/transport-entitlement.md index 0fbf6315b7..4f67cb24a4 100644 --- a/docs/transport-entitlement.md +++ b/docs/transport-entitlement.md @@ -178,15 +178,18 @@ does this with its invented family-credit engine. It supplies no real WFF inputs or statutory claim. `build/nz/as_rate_bridge.json` declares both NZ bridge outputs as kernel -calls, with no new kernel. On each population that runs the Accommodation -Supplement engine, a `calibration.prepare@1` node writes the family masks the -equality-only bridge masks compare, a `simulate.counterfactual@1` node writes -the reg 18(3)(b) sole-parent Jobseeker Support rate, and a +calls, with no new kernel. A `calibration.prepare@1` node writes the family +masks the equality-only bridge masks compare, a `simulate.counterfactual@1` +node writes the reg 18(3)(b) sole-parent Jobseeker Support rate, and a `simulate.solve_zero@1` node writes the reg 18 cutout: the zero of each unit's Jobseeker Support residual over weekly income on [0, 5000]. A sole parent's residual is the sole-parent rate minus the Income Test 3 reduction; -every other unit's residual is its own net payment. The resource keeps the -harness expressions as the statement of equivalence. Each branch runs its own +every other unit's residual is its own net payment. The shared `nz.as.bridge` +population, a FILTER branch of `nz.as`, runs these three nodes once for S0, +S1, S2, S3 and S5, and each variant repeats them on its own housing +population. A node on `nz.as` itself would re-key the skeleton's +`nz.terminal`, which composition refuses. The resource keeps the harness +expressions as the statement of equivalence. Each branch runs its own reg 17 base-rate counterfactual: zero-income Jobseeker Support once for a single unit and twice for a partnered one, plus the eldest-child family tax credit over 52 for a unit with a dependent child. The tests in diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_as_bridge.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_as_bridge.py index 5673c99b77..5e505e450a 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_as_bridge.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_as_bridge.py @@ -31,7 +31,7 @@ from microcosm.build.transport.entitlement import _parameter, _rows from microcosm.build.transport.target_kernels import CalibrationPrepareKernel from microcosm.frame import ExportContract, Frame, VariableMetadata, WeightKind, Weights -from microcosm.frame.adapters.axiom import NZ_SCHEMA +from microcosm.frame.adapters.axiom import NZ_SCHEMA, _batch_from_table from microcosm.frame.rules_kernels import ( SimulateCounterfactualKernel, SimulateSolveZeroKernel, @@ -373,6 +373,7 @@ class Unit: adult_age: int = 30 cash_assets: float = 0.0 rent: float = 400.0 + partner_age: int | None = None _RECEIPT_FLAGS = { @@ -414,7 +415,12 @@ def _frame(units): adults = 2 if unit.partnered else 1 for member in range(adults + len(unit.children)): adult = member < adults - age = unit.adult_age if adult else unit.children[member - adults] + if not adult: + age = unit.children[member - adults] + elif member == 1 and unit.partner_age is not None: + age = unit.partner_age + else: + age = unit.adult_age row = { "person_id": next_person, "person_household_id": family_id, @@ -724,28 +730,28 @@ def _youngest_received(units, *, masked=True): _run(frame, row, scenario, engines) finally: received = engines[_REFS["main_benefits_rates"]].received - return received[-1][_YOUNGEST] + table = received[-1][[_YOUNGEST]] + return _batch_from_table(table, [_YOUNGEST])[_YOUNGEST] def test_a_childless_family_keeps_the_closure_default_youngest_child_age(): units = [Unit(), Unit(children=(4, 7))] received = _youngest_received(units) # 0 for the childless family's adult, 4 for the parent and both children. - assert pd.Series(received).to_numpy(dtype=np.int64).tolist() == [0, 4, 4, 4] + assert received.dtype == np.float64 + assert received.tolist() == [0.0, 4.0, 4.0, 4.0] def test_unmasked_youngest_child_override_carries_a_missing_age_to_the_engine(): """Without the mask the childless unit's null age reaches the engine input. - pandas 3 reads the nullable Int64 source as float64 with NaN, so the - adapter's int64 conversion raises under numpy's invalid-value check and - otherwise yields int64's minimum. + pandas 3 reads the nullable Int64 source as float64 with NaN, so the adapter + takes its float branch and hands the engine NaN for that adult. """ units = [Unit(), Unit(children=(4, 7))] received = _youngest_received(units, masked=False) - assert received.isna().tolist() == [True, False, False, False] - with np.errstate(invalid="raise"), pytest.raises(FloatingPointError): - pd.Series(received).to_numpy(dtype=np.int64) + assert received.dtype == np.float64 + assert np.isnan(received).tolist() == [True, False, False, False] def test_bridge_scratch_columns_stay_out_of_the_export_contract(): @@ -926,6 +932,25 @@ def test_a_partnered_superannuitant_unit_takes_coefficient_two_in_s5(): assert abs(base[101] - (SuperannuationEngine.weekly + credit)) <= 1e-9 +@pytest.mark.parametrize(("first_age", "second_age"), [(70, 50), (50, 70)]) +def test_a_mixed_couple_in_s5_takes_the_superannuitants_amount_for_both_partners( + first_age, second_age +): + # The fixture pays the partner under 65 nothing, so ``max`` over members + # gives the superannuitant's amount in either person order. + base = _s5_base_rate( + [ + Unit( + receipt_group="superannuitant", + adult_age=first_age, + partner_age=second_age, + partnered=True, + ) + ] + ) + assert base[100] == 2 * SuperannuationEngine.weekly + + def test_the_central_base_rate_keeps_jobseeker_for_superannuitant_units(): _, _, base, _ = _bridge( [Unit(receipt_group="superannuitant", adult_age=70, partnered=True)] From 1a5882691621218c37328ca7a99b35b21054b5ab Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 23:43:56 -0500 Subject: [PATCH 2/3] Correct the other stale bridge claims the review named The generator's docstring and one test name repeated the wording the doc paragraph lost, and the graph never matched it. Two as_rate_bridge.json reasons gave the adapter's int64 read as why the youngest-child override takes a mask, which the childless-family tests now show doesn't happen on this path: a null age would reach the engine input as NaN. The golden moves only for that resource's hash. Co-Authored-By: Claude Opus 5.5 --- .../nz-as-bridge-review-follow-up.fixed.md | 2 +- .../microcosm/build/nz/as_rate_bridge.json | 4 ++-- .../engine_free/shared/test_nz_as_bridge.py | 2 +- .../tests/golden/nz_country_spec.json | 4 ++-- tools/build_nz_entitlement_graph.py | 20 +++++++++---------- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/changelog.d/nz-as-bridge-review-follow-up.fixed.md b/changelog.d/nz-as-bridge-review-follow-up.fixed.md index 962f244984..2b4bebabca 100644 --- a/changelog.d/nz-as-bridge-review-follow-up.fixed.md +++ b/changelog.d/nz-as-bridge-review-follow-up.fixed.md @@ -1 +1 @@ -Correct the NZ Accommodation Supplement bridge's documentation and tests after the review of #1440. `docs/transport-entitlement.md` now names `nz.as.bridge` as the shared population that runs the bridge for S0, S1, S2, S3 and S5. The childless-family tests read the Axiom adapter's own batch builder, and a new test runs a mixed couple through S5's base rate. +Correct the NZ Accommodation Supplement bridge's documentation and tests after the review of #1440. `docs/transport-entitlement.md` and the entitlement generator's docstring now name `nz.as.bridge` as the shared population that runs the masks, gross rate and cutout for S0, S1, S2, S3 and S5. `as_rate_bridge.json` now states why the youngest-child override takes a mask: a null age would reach the engine as NaN. The childless-family tests read the Axiom adapter's own batch builder, and a new test runs a mixed couple through S5's base rate. diff --git a/packages/microcosm-build/src/microcosm/build/nz/as_rate_bridge.json b/packages/microcosm-build/src/microcosm/build/nz/as_rate_bridge.json index f278805131..57e543c382 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/as_rate_bridge.json +++ b/packages/microcosm-build/src/microcosm/build/nz/as_rate_bridge.json @@ -111,7 +111,7 @@ "variable": "jobseeker_support_youngest_dependent_child_age", "unit_attribute": "family.youngest_dependent_child_age", "value_source": "unit", - "reason": "harness: family_shape youngest_child_age (run.py:1938,1951). The unit column is null for a childless family, and the engine adapter reads integer inputs as int64, so the override applies only where the unit has a dependent child; other rows keep the closure default 0.", + "reason": "harness: family_shape youngest_child_age (run.py:1938,1951). The unit column is null for a childless family, and that null would reach the engine input as NaN, so the override applies only where the unit has a dependent child; other rows keep the closure default 0.", "mask": { "unit_attribute": "family.accommodation_supplement_bridge_has_dependent_child", "equals": true @@ -285,7 +285,7 @@ "variable": "jobseeker_support_youngest_dependent_child_age", "unit_attribute": "family.youngest_dependent_child_age", "value_source": "unit", - "reason": "harness: family_shape youngest_child_age (run.py:1938,1951). The unit column is null for a childless family, and the engine adapter reads integer inputs as int64, so the override applies only where the unit has a dependent child; other rows keep the closure default 0.", + "reason": "harness: family_shape youngest_child_age (run.py:1938,1951). The unit column is null for a childless family, and that null would reach the engine input as NaN, so the override applies only where the unit has a dependent child; other rows keep the closure default 0.", "mask": { "unit_attribute": "family.accommodation_supplement_bridge_has_dependent_child", "equals": true diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_as_bridge.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_as_bridge.py index 5e505e450a..37127e3369 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_as_bridge.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_as_bridge.py @@ -767,7 +767,7 @@ def test_bridge_scratch_columns_stay_out_of_the_export_contract(): assert not persisted & (scratch | {_GROSS}) -def test_every_as_population_runs_its_own_bridge_and_every_branch_its_base_rate(): +def test_each_bridge_population_runs_the_bridge_and_every_branch_its_base_rate(): rows = {row["id"]: row for row, _ in _rows(_SPEC)[0]} scenarios = _SPEC["resources"]["scenarios"]["scenarios"] for node_id in ("masks", "sole_parent_gross_rate", "cutout"): diff --git a/packages/microcosm-build/tests/golden/nz_country_spec.json b/packages/microcosm-build/tests/golden/nz_country_spec.json index 8318fd8570..a49614f1b5 100644 --- a/packages/microcosm-build/tests/golden/nz_country_spec.json +++ b/packages/microcosm-build/tests/golden/nz_country_spec.json @@ -1,6 +1,6 @@ { "country": "nz", - "fingerprint": "fc9e3f65841862016207a1baa368e8ecf0476d194675ad8ec771811a5ea54f4f", + "fingerprint": "0f80d7e36814b8e68e651275223559cfd284049b4bb8e56e3581c71644285338", "gate_ids": [ "calibration_per_family_fit", "demography_per_family_fit", @@ -40,7 +40,7 @@ "resource_hashes": { "as_area_crosswalk.json": "56eeb638d77c73fd92ca541d9d2f249fd3062bece0503470e0e85dde70c4c05b", "as_encoding.json": "91e0e9972abfaaa202a13ede6e2dbc815a4c6c9514726c970928a47adfef0e32", - "as_rate_bridge.json": "25fa43210fd91655988933fe23ac6411bc30ddeb73c480c24da19999ffa2da22", + "as_rate_bridge.json": "1399321cc7ef44486f7ab407968b036930ff6ae8fbab4c30ce710ea0fff7301c", "atomic_geography_support.json": "6efd09d5cde2f16d4d01e522a721191a829a754d2ce019d75bb0407085ecb4b7", "axiom_input_closure.json": "1c237a316eecffd8d8eb9258a451b992373b804d72b411895337647c272731aa", "axiom_rules_bindings.json": "fb163e4842c8a910d63bd0ecf0fc81e5a55a2dddaa9cde66a52d9f4ff2c791f3", diff --git a/tools/build_nz_entitlement_graph.py b/tools/build_nz_entitlement_graph.py index e371567160..bde440adc7 100644 --- a/tools/build_nz_entitlement_graph.py +++ b/tools/build_nz_entitlement_graph.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 """Generate NZ's post-calibration entitlement branch declarations. -The Accommodation Supplement bridge follows ``as_rate_bridge.json``. On each -population that runs the AS engine, a ``calibration.prepare@1`` node writes -the family masks, a ``simulate.counterfactual@1`` node writes the reg 18(3)(b) -sole-parent gross rate and a ``simulate.solve_zero@1`` node writes the reg 18 -cutout. The shared ``nz.as.bridge`` population, a FILTER branch of ``nz.as``, -runs these once for every entitlement scenario, and each variant repeats them -on its own housing population. An extension node on ``nz.as`` itself would -re-key the skeleton's ``nz.terminal``, which composition refuses. Each -branch runs its own reg 17 base-rate counterfactual. Every coefficient, mask -and override comes from the resource; this generator adds no schedule. +The Accommodation Supplement bridge follows ``as_rate_bridge.json``. A +``calibration.prepare@1`` node writes the family masks, a +``simulate.counterfactual@1`` node writes the reg 18(3)(b) sole-parent gross +rate and a ``simulate.solve_zero@1`` node writes the reg 18 cutout. The shared +``nz.as.bridge`` population, a FILTER branch of ``nz.as``, runs these once for +every entitlement scenario, and each variant repeats them on its own housing +population. An extension node on ``nz.as`` itself would re-key the skeleton's +``nz.terminal``, which composition refuses. Each branch runs its own reg 17 +base-rate counterfactual. Every coefficient, mask and override comes from the +resource; this generator adds no schedule. """ from __future__ import annotations From 082f60fe420c27a7e7e671bfbc7b9378ea21161f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 23:58:27 -0500 Subject: [PATCH 3/3] Fix the last wording findings on the NZ bridge review The changelog credited the generator docstring with a change this PR did not make: the docstring already named the shared population, and the PR only dropped the sentence that contradicted it. The docstring's "every coefficient" holds for the bridge alone, so it now says so. benefit_unit_rule.json said the youngest-child age column holds 0 for a childless family. unit_construction.py writes a null there, which is why the bridge override takes a mask, so the description now says null. The golden picks up that resource's new hash and the fingerprint. Co-Authored-By: Claude Opus 5.5 --- changelog.d/nz-as-bridge-review-follow-up.fixed.md | 2 +- .../src/microcosm/build/nz/benefit_unit_rule.json | 2 +- packages/microcosm-build/tests/golden/nz_country_spec.json | 4 ++-- tools/build_nz_entitlement_graph.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/changelog.d/nz-as-bridge-review-follow-up.fixed.md b/changelog.d/nz-as-bridge-review-follow-up.fixed.md index 2b4bebabca..3b3a693992 100644 --- a/changelog.d/nz-as-bridge-review-follow-up.fixed.md +++ b/changelog.d/nz-as-bridge-review-follow-up.fixed.md @@ -1 +1 @@ -Correct the NZ Accommodation Supplement bridge's documentation and tests after the review of #1440. `docs/transport-entitlement.md` and the entitlement generator's docstring now name `nz.as.bridge` as the shared population that runs the masks, gross rate and cutout for S0, S1, S2, S3 and S5. `as_rate_bridge.json` now states why the youngest-child override takes a mask: a null age would reach the engine as NaN. The childless-family tests read the Axiom adapter's own batch builder, and a new test runs a mixed couple through S5's base rate. +Correct the NZ Accommodation Supplement bridge's documentation and tests after the review of #1440. `docs/transport-entitlement.md` now names `nz.as.bridge` as the shared population that runs the masks, gross rate and cutout for S0, S1, S2, S3 and S5, and the entitlement generator's docstring drops the sentence that contradicted it. `as_rate_bridge.json` now states why the youngest-child override takes a mask: a null age would reach the engine as NaN. `benefit_unit_rule.json` now says the youngest-child age column is null for a childless family, as `unit_construction.py` writes it. The childless-family tests read the Axiom adapter's own batch builder, and a new test runs a mixed couple through S5's base rate. diff --git a/packages/microcosm-build/src/microcosm/build/nz/benefit_unit_rule.json b/packages/microcosm-build/src/microcosm/build/nz/benefit_unit_rule.json index bf3be694eb..6476c0ea08 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/benefit_unit_rule.json +++ b/packages/microcosm-build/src/microcosm/build/nz/benefit_unit_rule.json @@ -91,7 +91,7 @@ "family.is_sole_parent": "single adult with at least one dependent child", "family.is_partnered": "the unit contains a partnered couple", "family.is_single": "the unit has one adult (not family.is_partnered)", - "family.youngest_dependent_child_age": "age of the youngest dependent child; 0 when there is none" + "family.youngest_dependent_child_age": "age of the youngest dependent child; null when there is none" } }, "settled_date": "2026-10-12", diff --git a/packages/microcosm-build/tests/golden/nz_country_spec.json b/packages/microcosm-build/tests/golden/nz_country_spec.json index a49614f1b5..23a3bc995c 100644 --- a/packages/microcosm-build/tests/golden/nz_country_spec.json +++ b/packages/microcosm-build/tests/golden/nz_country_spec.json @@ -1,6 +1,6 @@ { "country": "nz", - "fingerprint": "0f80d7e36814b8e68e651275223559cfd284049b4bb8e56e3581c71644285338", + "fingerprint": "4d6be0d9d5adc1ecc1c65a04c3fa97baa646665eeb9439b6a8100642a7723f79", "gate_ids": [ "calibration_per_family_fit", "demography_per_family_fit", @@ -45,7 +45,7 @@ "axiom_input_closure.json": "1c237a316eecffd8d8eb9258a451b992373b804d72b411895337647c272731aa", "axiom_rules_bindings.json": "fb163e4842c8a910d63bd0ecf0fc81e5a55a2dddaa9cde66a52d9f4ff2c791f3", "benefit_gross_up.json": "bb0100927c3d666f497b24694109463de247d53d6e7ee0cfa092255e33c5514e", - "benefit_unit_rule.json": "a9e8cbfd38a64d2f8db0002fe8c31bb5a18fb34dbe59fa943336487ce95e2ca8", + "benefit_unit_rule.json": "c84efba8353c770131256a8c17b82c7d98f738e6dd8ef217e19d30014cb14604", "calibration_bindings.json": "abf5278b51a13530e2c589a932942a185f5dda095c1eb3e4bc703999c24852b3", "calibration_settings.json": "56e4d1da8468245da6bd41daf263425cc65e4e9c9f11ff45eee459d3aed1989e", "chronicle_feed.json": "60a80dd0042838b0412baf9dd0985c961b4253a6876d16bcdbb8a2c6c03de0c7", diff --git a/tools/build_nz_entitlement_graph.py b/tools/build_nz_entitlement_graph.py index bde440adc7..3161833993 100644 --- a/tools/build_nz_entitlement_graph.py +++ b/tools/build_nz_entitlement_graph.py @@ -9,8 +9,8 @@ every entitlement scenario, and each variant repeats them on its own housing population. An extension node on ``nz.as`` itself would re-key the skeleton's ``nz.terminal``, which composition refuses. Each branch runs its own reg 17 -base-rate counterfactual. Every coefficient, mask and override comes from the -resource; this generator adds no schedule. +base-rate counterfactual. Every bridge coefficient, mask and override comes +from the resource; this generator adds no schedule. """ from __future__ import annotations