diff --git a/docs/concept-coverage/axiom-nz.md b/docs/concept-coverage/axiom-nz.md index 1b213f1607..a2564100d0 100644 --- a/docs/concept-coverage/axiom-nz.md +++ b/docs/concept-coverage/axiom-nz.md @@ -3,16 +3,16 @@ Engine version `6fe181fc4c65763150fe97501ef3443774e64869`; concept schema `5a55669d0447`. Generated by `tools/refresh_concept_coverage.py`; do not edit by hand. - Engine inputs: 668 -- Fed by a concept: 89 +- Fed by a concept: 90 - Structural: 0 -- Uncovered: 579 +- Uncovered: 578 - Mapped but missing from the engine: 0 ## Inputs each concept feeds | Concept | Engine inputs | |---|---| -| `fact:person.age` | `accommodation_supplement_has_dependent_children` (Family)
`accommodation_supplement_has_two_or_more_dependent_children` (Family)
`accommodation_supplement_sole_parent` (Family)
`caregiver_age_years` (Person)
`childcare_assistance_dependent_children_count` (Person)
`community_services_card_dependent_children_count` (Person)
`community_services_card_person_age_years` (Person)
`disability_allowance_dependent_children_count` (Person)
`disability_allowance_person_age_years` (Person)
`disability_allowance_person_is_sole_parent` (Person)
`in_work_tax_credit_allowed_children_count` (Family)
`in_work_tax_credit_person_age` (Family)
`jobseeker_applicant_age` (Person)
`jobseeker_has_dependent_child` (Person)
`jobseeker_support_applicant_age` (Person)
`jobseeker_support_dependent_children_count` (Person)
`nz_super_has_dependent_children` (Person)
`nz_super_person_age` (Person)
`sole_parent_applicant_age` (Person)
`sole_parent_caring_for_dependent_children_under_age_limit` (Person)
`sole_parent_is_mother_or_father_or_child_regarded_as_child` (Person)
`student_allowance_has_children` (Person)
`student_allowance_student_age` (Person)
`supported_living_applicant_age` (Person)
`supported_living_caring_applicant_age` (Person)
`supported_living_caring_applicant_has_dependent_child` (Person)
`supported_living_payment_applicant_age` (Person)
`supported_living_payment_dependent_children_count` (Person)
`wff_dependent_children_principal_caregiver_count` (Family)
`wff_person_age` (Family)
`winter_energy_payment_has_dependent_children` (Person)
`winter_energy_payment_person_age` (Person)
`young_person_age_years` (Person)
`young_person_has_dependent_child` (Person)
`young_person_has_dependent_child_for_part6_rates` (Person)
`young_person_is_parent_or_step_parent_of_dependent_child` (Person) | +| `fact:person.age` | `accommodation_supplement_has_dependent_children` (Family)
`accommodation_supplement_has_two_or_more_dependent_children` (Family)
`accommodation_supplement_sole_parent` (Family)
`caregiver_age_years` (Person)
`childcare_assistance_dependent_children_count` (Person)
`community_services_card_dependent_children_count` (Person)
`community_services_card_person_age_years` (Person)
`disability_allowance_dependent_children_count` (Person)
`disability_allowance_person_age_years` (Person)
`disability_allowance_person_is_sole_parent` (Person)
`in_work_tax_credit_allowed_children_count` (Family)
`in_work_tax_credit_person_age` (Family)
`jobseeker_applicant_age` (Person)
`jobseeker_has_dependent_child` (Person)
`jobseeker_support_applicant_age` (Person)
`jobseeker_support_dependent_children_count` (Person)
`nz_super_has_dependent_children` (Person)
`nz_super_person_age` (Person)
`nz_super_shares_residence_with_person_at_least_adult_age` (Person)
`sole_parent_applicant_age` (Person)
`sole_parent_caring_for_dependent_children_under_age_limit` (Person)
`sole_parent_is_mother_or_father_or_child_regarded_as_child` (Person)
`student_allowance_has_children` (Person)
`student_allowance_student_age` (Person)
`supported_living_applicant_age` (Person)
`supported_living_caring_applicant_age` (Person)
`supported_living_caring_applicant_has_dependent_child` (Person)
`supported_living_payment_applicant_age` (Person)
`supported_living_payment_dependent_children_count` (Person)
`wff_dependent_children_principal_caregiver_count` (Family)
`wff_person_age` (Family)
`winter_energy_payment_has_dependent_children` (Person)
`winter_energy_payment_person_age` (Person)
`young_person_age_years` (Person)
`young_person_has_dependent_child` (Person)
`young_person_has_dependent_child_for_part6_rates` (Person)
`young_person_is_parent_or_step_parent_of_dependent_child` (Person) | | `fact:person.legal_marital_status` | `sole_parent_marriage_or_civil_union_dissolved` (Person)
`sole_parent_spouse_or_partner_died` (Person) | | `fact:person.partner_person_id` | `accommodation_supplement_has_dependent_children` (Family)
`accommodation_supplement_has_two_or_more_dependent_children` (Family)
`accommodation_supplement_in_relationship` (Family)
`accommodation_supplement_sole_parent` (Family)
`community_services_card_person_married_or_partnered` (Person)
`disability_allowance_person_in_relationship` (Person)
`has_relationship_partner` (Person)
`jobseeker_support_single` (Person)
`nz_super_in_relationship` (Person)
`nz_super_single` (Person)
`nz_super_single` (Person)
`sole_parent_is_single` (Person)
`student_allowance_has_partner` (Person)
`student_allowance_lives_with_partner` (Person)
`supported_living_payment_single` (Person)
`winter_energy_payment_single` (Person)
`young_person_is_in_relationship` (Person) | | `fact:person.parent_1_person_id` | `accommodation_supplement_has_dependent_children` (Family)
`accommodation_supplement_has_two_or_more_dependent_children` (Family)
`accommodation_supplement_sole_parent` (Family)
`childcare_assistance_dependent_children_count` (Person)
`community_services_card_dependent_children_count` (Person)
`disability_allowance_dependent_children_count` (Person)
`disability_allowance_person_is_sole_parent` (Person)
`in_work_tax_credit_allowed_children_count` (Family)
`jobseeker_has_dependent_child` (Person)
`jobseeker_support_dependent_children_count` (Person)
`nz_super_has_dependent_children` (Person)
`sole_parent_caring_for_dependent_children_under_age_limit` (Person)
`sole_parent_is_mother_or_father_or_child_regarded_as_child` (Person)
`student_allowance_has_children` (Person)
`supported_living_caring_applicant_has_dependent_child` (Person)
`supported_living_payment_dependent_children_count` (Person)
`wff_dependent_children_principal_caregiver_count` (Family)
`winter_energy_payment_has_dependent_children` (Person)
`young_person_has_dependent_child` (Person)
`young_person_has_dependent_child_for_part6_rates` (Person)
`young_person_is_parent_or_step_parent_of_dependent_child` (Person) | @@ -138,9 +138,9 @@ Engine version `6fe181fc4c65763150fe97501ef3443774e64869`; concept schema `5a556 `kiwisaver_selected_employee_contribution_rate (Person)` -### nz/statutes/new_zealand_superannuation/core.yaml (11) +### nz/statutes/new_zealand_superannuation/core.yaml (10) -`nz_super_all_shared_adults_are_exempt_dependent_children_or_temporary_visitors (Person)`, `nz_super_birth_year (Person)`, `nz_super_born_on_or_before_30_june_of_birth_year (Person)`, `nz_super_elected_weekly_compensation_instead_of_superannuation (Person)`, `nz_super_ordinarily_resident_in_new_zealand_on_application (Person)`, `nz_super_overseas_application_residence_exception_applies (Person)`, `nz_super_principal_residence_is_qualifying_accommodation (Person)`, `nz_super_resident_present_since_age_20_new_zealand_years (Person)`, `nz_super_resident_present_since_age_20_total_years (Person)`, `nz_super_resident_present_since_age_50_total_years (Person)`, `nz_super_shares_residence_with_person_at_least_adult_age (Person)` +`nz_super_all_shared_adults_are_exempt_dependent_children_or_temporary_visitors (Person)`, `nz_super_birth_year (Person)`, `nz_super_born_on_or_before_30_june_of_birth_year (Person)`, `nz_super_elected_weekly_compensation_instead_of_superannuation (Person)`, `nz_super_ordinarily_resident_in_new_zealand_on_application (Person)`, `nz_super_overseas_application_residence_exception_applies (Person)`, `nz_super_principal_residence_is_qualifying_accommodation (Person)`, `nz_super_resident_present_since_age_20_new_zealand_years (Person)`, `nz_super_resident_present_since_age_20_total_years (Person)`, `nz_super_resident_present_since_age_50_total_years (Person)` ### nz/statutes/new_zealand_superannuation/special_rates.yaml (13) diff --git a/docs/nz-reporting-classifiers-method.md b/docs/nz-reporting-classifiers-method.md index 83934bed9a..3b68f08790 100644 --- a/docs/nz-reporting-classifiers-method.md +++ b/docs/nz-reporting-classifiers-method.md @@ -63,9 +63,12 @@ weighted share of every fixed cell must be reported for every scenario and variant. These fixed memberships preserve comparisons even when variant cells are no longer weighted tenths. -The disclosure on New Zealand Superannuation remains material: the current -shared-residence input defaults false, overstating payments to single people -who share a home and moving those units upward in the income ranking. +The disclosure on New Zealand Superannuation records an understatement. The +shared-residence input reads household composition, and the input for exempt +co-resident adults defaults false, so a single person whose only co-resident +adults are exempt dependent children or temporary visitors gets the sharing +rate. That understates their payments and moves those units downward in the +income ranking. ## A2 Receipt group diff --git a/packages/microcosm-build/src/microcosm/build/nz/axiom_input_closure.json b/packages/microcosm-build/src/microcosm/build/nz/axiom_input_closure.json index acde1e2d59..a9bc863d83 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/axiom_input_closure.json +++ b/packages/microcosm-build/src/microcosm/build/nz/axiom_input_closure.json @@ -943,7 +943,7 @@ "input": "nz_super_all_shared_adults_are_exempt_dependent_children_or_temporary_visitors", "class": "defaulted", "value": false, - "reason": "Read only when the residence is shared, which is defaulted false.", + "reason": "nz_super_living_alone (s 4A) reads this input only when nz_super_shares_residence_with_person_at_least_adult_age is true, and the axiom:nz concept binding now derives that input from household composition: another person in the same household is aged 18 or over. With this input false, every person who has no partner (nz_super_single) 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)). That includes a person whose only co-resident adults are exempt dependent children or temporary visitors. Section 4A counts that person as living alone, so this default understates NZS for that group. S5's reg 17 superannuitant base rate (as_rate_bridge.json) adds the same weekly amount for a single superannuitant unit, so the default lowers that base rate for the group too.", "citation": "New Zealand Superannuation and Retirement Income Act 2001 s 4A (nz_super_living_alone)", "new_default": true }, @@ -1048,11 +1048,8 @@ "rulespec_path": "nz/statutes/new_zealand_superannuation/core.yaml", "entity": "Person", "input": "nz_super_shares_residence_with_person_at_least_adult_age", - "class": "defaulted", - "value": false, - "reason": "Household composition is not read into this person-level input in v0 (no mapping transform flags another adult in the household), so every single person gets the s 4A living-alone rate; this overstates NZS for singles who share a home. The receipt layer reads NZS eligibility, not the amount; if the reg 17 superannuitant bridge reads the amount (method card MC11), this default matters there too.", - "citation": "New Zealand Superannuation and Retirement Income Act 2001 s 4A (nz_super_living_alone)", - "new_default": true + "class": "encoded", + "encoded_by": "concept_binding" }, { "rulespec_path": "nz/statutes/new_zealand_superannuation/core.yaml", diff --git a/packages/microcosm-build/src/microcosm/build/nz/concept_mapping.json b/packages/microcosm-build/src/microcosm/build/nz/concept_mapping.json index 5ae43f6954..b8c29d586f 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/concept_mapping.json +++ b/packages/microcosm-build/src/microcosm/build/nz/concept_mapping.json @@ -411,6 +411,21 @@ "kind": "identity" } }, + { + "canonical_input": "nz:statutes/new_zealand_superannuation/core#input.nz_super_shares_residence_with_person_at_least_adult_age", + "concepts": [ + "fact:person.age" + ], + "engine_entity": "Person", + "engine_input": "nz_super_shares_residence_with_person_at_least_adult_age", + "module": "nz/statutes/new_zealand_superannuation/core.yaml", + "note": "nz/statutes/new_zealand_superannuation/core.yaml rule nz_super_living_alone (New Zealand Superannuation and Retirement Income Act 2001 s 4A): 'nz_super_single and nz_super_principal_residence_is_qualifying_accommodation and (not nz_super_shares_residence_with_person_at_least_adult_age or nz_super_all_shared_adults_are_exempt_dependent_children_or_temporary_visitors)'. The binding returns true when at least one other person with the same frame household id is aged min_age or over. min_age 18 copies parameter nz_super_living_alone_adult_age in the same module (18 from 2026-04-01, source s 4A(3) and (4)) by hand: no RuleSpec formula reads that parameter, so nothing checks min_age against it, and a change to the parameter needs a matching edit here. Differences: the frame's household stands in for the statute's residence, so the binding counts every other person aged 18 or over who carries the person's household id, and misses an adult who shares the residence under another household id; it counts a partner, but nz_super_living_alone requires nz_super_single and nz_super_ordinary_weekly_rate_before_tax branches on nz_super_single first, so the rate never reads a partnered person's value; it counts adult dependent children and temporary visitors, whom nz_super_all_shared_adults_are_exempt_dependent_children_or_temporary_visitors carves out, and the input closure defaults that input to false; ages are completed years at the survey reference date, so a co-resident who turns 18 between that date and the assessment reads differently; a person with no household id reads false.", + "relation": "approximate", + "transform": { + "kind": "coresident_adult", + "min_age": 18 + } + }, { "canonical_input": "nz:statutes/new_zealand_superannuation/core#input.nz_super_single", "concepts": [ diff --git a/packages/microcosm-build/src/microcosm/build/nz/export_replay_bindings.json b/packages/microcosm-build/src/microcosm/build/nz/export_replay_bindings.json index ae9d57b9ef..94e66babd1 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/export_replay_bindings.json +++ b/packages/microcosm-build/src/microcosm/build/nz/export_replay_bindings.json @@ -1,4 +1,4 @@ { "export_contract_sha256": "0000fbc98cec95bc8216dc968ffc09fd9a3a07455697275ba7cf67dec54f17b2", - "axiom_input_closure_sha256": "791c1cd420de8eddde8af6f9bc8a9aa5627d517f90ffed9b0012a36734071058" + "axiom_input_closure_sha256": "d2694200e0191803f1fdfa231106ed35fb9de3bbb0594e87bff87fed19849ce6" } diff --git a/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json b/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json index 6106713a1e..4c4658f720 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json +++ b/packages/microcosm-build/src/microcosm/build/nz/reporting_classifiers.json @@ -209,8 +209,8 @@ ], "disclosures": [ { - "note": "NZ Super is overstated for single people who share a home because the shares-residence input defaults false; this moves their deciles upward.", - "evidence": "axiom_input_closure.json#/entries[input=nz_super_shares_residence_with_person_at_least_adult_age]", + "note": "The model understates NZ Super for single people whose only co-resident adults are exempt dependent children or temporary visitors: the shares-residence input reads household composition, but the exempt-adults input defaults false, so they get the sharing rate. This moves their deciles downward.", + "evidence": "axiom_input_closure.json#/entries[input=nz_super_shares_residence_with_person_at_least_adult_age]; axiom_input_closure.json#/entries[input=nz_super_all_shared_adults_are_exempt_dependent_children_or_temporary_visitors]", "citation": { "item": "A1", "source": "docs/nz-reporting-classifiers-method.md#a1-income-decile-provisional" diff --git a/packages/microcosm-build/src/microcosm/build/nz/transport_graph.json b/packages/microcosm-build/src/microcosm/build/nz/transport_graph.json index f936c4ca6a..0943fbb605 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/transport_graph.json +++ b/packages/microcosm-build/src/microcosm/build/nz/transport_graph.json @@ -4978,7 +4978,7 @@ ] }, "pending_outputs": [ - "G8-x income reconciliation evidence: the native-band report, omission-register and identity-receipt artifacts and the VSI, VEX and VPT run evidence that measurement_prefit requires (R2C-18), and the NZS sharing binding (R2C-5)", + "G8-x income reconciliation evidence: the native-band report, omission-register and identity-receipt artifacts and the VSI, VEX and VPT run evidence that measurement_prefit requires (R2C-18)", "Census rent and liquid-assets quantile producers" ] } diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py index d5af14f373..ef42424669 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py @@ -3235,24 +3235,30 @@ def test_keyed_citations_survive_a_record_inserted_before_the_target( (_BUILD.repository / file).read_text(encoding="utf-8") ) array, field = "bindings", "canonical_input" - expected_key = "nz:statutes/social_security/accommodation_supplement/core#input.accommodation_supplement_relevant_weekly_income" + expected_keys = [ + "nz:statutes/social_security/accommodation_supplement/core#input.accommodation_supplement_relevant_weekly_income" + ] evidence = reporting["disclosures"][1]["evidence"] else: file, array, field = "axiom_input_closure.json", "entries", "input" original = _load(file) if disclosure == "sharing_home": - expected_key = ( - "nz_super_shares_residence_with_person_at_least_adult_age" - ) + expected_keys = [ + "nz_super_shares_residence_with_person_at_least_adult_age", + "nz_super_all_shared_adults_are_exempt_dependent_children_or_temporary_visitors", + ] evidence = reporting["disclosures"][0]["evidence"] else: - expected_key = "accommodation_supplement_non_beneficiary" + expected_keys = ["accommodation_supplement_non_beneficiary"] evidence = reporting["as_non_beneficiary_audit"]["evidence"] - (expected,) = [row for row in original[array] if row[field] == expected_key] + expected = [] + for key in expected_keys: + (row,) = [row for row in original[array] if row[field] == key] + expected.append(row) original[array].insert(0, {field: "dummy_record"}) citation_checkout(file, original) - (citation,) = json_pointer_citations(evidence) - assert resolve_json_pointer(*citation) == expected + citations = json_pointer_citations(evidence) + assert [resolve_json_pointer(*citation) for citation in citations] == expected def test_local_file_pins_cannot_hide_absolute_or_parent_path_aliases(self) -> None: relative = "packages/microcosm-frame/src/microcosm/frame/concepts.py" @@ -3413,18 +3419,32 @@ def test_financial_independence_citations_still_bind_carried_usual_hours( ] assert f"`{rule['donor_variable']}`" in row - def test_reporting_disclosure_cites_the_sharing_home_default_record(self) -> None: + def test_reporting_disclosure_cites_the_sharing_home_binding_record(self) -> None: disclosure = _load("reporting_classifiers.json")["disclosures"][0] - (citation,) = json_pointer_citations(disclosure["evidence"]) - record = resolve_json_pointer(*citation) + record, exempt = ( + resolve_json_pointer(*citation) + for citation in json_pointer_citations(disclosure["evidence"]) + ) assert ( record["input"] == "nz_super_shares_residence_with_person_at_least_adult_age" ) - assert record["class"] == "defaulted" - assert record["value"] is False - assert "overstates NZS for singles who share a home" in record["reason"] - assert "overstated for single people who share a home" in disclosure["note"] + # H2 (R2C-20): household composition now encodes the input. + assert record["class"] == "encoded" + assert record["encoded_by"] == "concept_binding" + assert "value" not in record + # The exempt-adults default the note names is the second citation. + assert ( + exempt["input"] + == "nz_super_all_shared_adults_are_exempt_dependent_children_or_temporary_visitors" + ) + assert exempt["class"] == "defaulted" + assert exempt["value"] is False + assert "default understates NZS for that group" in exempt["reason"] + assert ( + "understates NZ Super for single people whose only co-resident adults" + in disclosure["note"] + ) def test_reporting_non_beneficiary_audit_cites_its_exact_state_binding( self, diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_toy_graph.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_toy_graph.py index 5d07658f56..d1a7205e64 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_toy_graph.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_toy_graph.py @@ -1026,3 +1026,156 @@ def test_without_the_toy_standin_its_six_rows_refuse_activation_by_name(toy): for role in ("count_reference", "amount_reference"): assert f"target_references.{band[role]}; awaits" in message assert f"measurement:{TOY_MEASUREMENT}" in message + + +# --------------------------------------------------------------------------- +# H2 (R2C-20): NZS co-residence from household composition +# --------------------------------------------------------------------------- + +_SHARING = "nz_super_shares_residence_with_person_at_least_adult_age" +_NZS_CORE = "nz/statutes/new_zealand_superannuation/core.yaml" + + +def _packaged_nz(name): + """One of NZ's packaged JSON resources, the bytes its graph selects.""" + from importlib.resources import files + + path = files("microcosm.build").joinpath("nz", name) + return json.loads(path.read_text(encoding="utf-8")) + + +def _before_h2(mapping, closure): + """NZ's mapping and closure as they stood before H2 bound the input. + + The mapping had no binding for it, and the closure defaulted it to false + with the citation the defaulted entry carried. + """ + mapping = copy.deepcopy(mapping) + mapping["bindings"] = [ + row for row in mapping["bindings"] if row["engine_input"] != _SHARING + ] + closure = copy.deepcopy(closure) + (entry,) = [row for row in closure["entries"] if row["input"] == _SHARING] + del entry["encoded_by"] + entry.update( + { + "class": "defaulted", + "value": False, + "reason": "Before H2, no mapping transform read household composition.", + "citation": ( + "New Zealand Superannuation and Retirement Income Act 2001 s 4A " + "(nz_super_living_alone)" + ), + "new_default": True, + } + ) + return mapping, closure + + +def _encode_sharing(person, household, mapping, closure): + """Run concepts.encode@1 on NZS core.yaml and return the sharing column.""" + from microcosm.build.transport.column_kernels import CONCEPTS_ENCODE + + def text(document): + return canonical_json(document).decode("utf-8") + + node = Node( + "nz.encode.person", + CONCEPTS_ENCODE.ref, + population="nz.central", + params={ + "mapping": text(mapping), + "mapping_sha256": sha256(canonical_json(mapping)).hexdigest(), + "closure": text(closure), + "closure_sha256": sha256(canonical_json(closure)).hexdigest(), + "rulespec_paths": (_NZS_CORE,), + }, + outputs=(Owned("person", _SHARING, "bool"),), + ) + result = CONCEPTS_ENCODE.run( + KernelContext( + node=node, + tables={"person": person.copy(), "household": household.copy()}, + weights={}, + strata=pd.Series(["toy"] * len(person), name="stratum"), + params=node.params, + rng=np.random.default_rng(1), + ) + ) + values = result.columns["person", _SHARING] + assert values.dtype == np.bool_ + return values.to_dict() + + +def test_h2_sharing_binding_turns_the_toy_superannuitants_default_into_true(toy): + """R2C-20 H2 on the toy's created frame, through NZ's packaged resources. + + The toy's adapters invent their rates, so this records the input only. + Each of the twenty persons aged 65 or over lives with a partner aged 65 + to 79 and a 16-year-old: before H2 the closure default gave each false, + and the coresident_adult binding now gives each true. + """ + frame = toy[1][0].populations["nz.create"] + person = frame.table("person") + household = frame.table("household") + mapping = _packaged_nz("concept_mapping.json") + closure = _packaged_nz("axiom_input_closure.json") + before = _encode_sharing(person, household, *_before_h2(mapping, closure)) + after = _encode_sharing(person, household, mapping, closure) + superannuitants = [ + 10000 + 3 * h + member for h in range(30, 40) for member in (0, 1) + ] + assert sorted(person.loc[person["age"] >= 65, "person_id"]) == superannuitants + for identity in superannuitants: + row = person.set_index("person_id").loc[identity] + others = person[ + (person["person_household_id"] == row["person_household_id"]) + & (person["person_id"] != identity) + ] + assert sorted(others["age"].tolist())[0] == 16 + assert sorted(others["age"].tolist())[1] >= 65 + assert before[identity] is False + assert after[identity] is True + + +def test_h2_sharing_binding_on_the_household_shapes_the_toy_lacks(): + """A superannuitant alone, with an adult, or only with a minor. + + The toy has no superannuitant who lives alone or whose only co-resident + is under 18. Persons 10 and 11 have no household id, and the + coresident_adult kind places each in a household with nobody, so both + read false although each is an adult: null ids don't group together. + """ + person = pd.DataFrame( + { + "person_id": np.arange(1, 12, dtype=np.int64), + "person_household_id": pd.array( + [100, 200, 200, 300, 300, 400, 400, 500, 500, None, None], + dtype="Int64", + ), + "age": np.array( + [70, 68, 40, 72, 16, 66, 18, 67, 17, 75, 30], dtype=np.int64 + ), + } + ) + household = pd.DataFrame( + {"household_id": np.array([100, 200, 300, 400, 500], dtype=np.int64)} + ) + mapping = _packaged_nz("concept_mapping.json") + closure = _packaged_nz("axiom_input_closure.json") + before = _encode_sharing(person, household, *_before_h2(mapping, closure)) + after = _encode_sharing(person, household, mapping, closure) + assert before == dict.fromkeys(range(1, 12), False) + assert after == { + 1: False, # 70, lives alone + 2: True, # 68, with a 40-year-old + 3: True, + 4: False, # 72, whose only co-resident is 16 + 5: True, + 6: True, # 66, with an 18-year-old: the adult age is inclusive + 7: True, + 8: False, # 67, whose only co-resident is 17 + 9: True, + 10: False, # 75, no household id + 11: False, # 30, no household id either + } 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 cb5ab98995..610fe829a6 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 @@ -824,7 +824,7 @@ def test_the_committed_nz_surface_partitions_as_the_design_table(self) -> None: assert counts == { "main_benefits_entitlement": {**zero, "encoded": 13, "defaulted": 47}, "main_benefits_rates": {**zero, "encoded": 13, "defaulted": 15}, - "nz_super": {**zero, "encoded": 2, "defaulted": 11}, + "nz_super": {**zero, "encoded": 3, "defaulted": 10}, "accommodation_supplement": { **zero, "encoded": 17, @@ -2748,8 +2748,8 @@ def test_the_nz_roster_classes(self, nz_spec) -> None: for row in roster: counts[row["class"]] = counts.get(row["class"], 0) + 1 assert counts == { - "closure_default": 93, - "derived": 322, + "closure_default": 92, + "derived": 323, "normalized": 10, "copied": 3, "pending": 1, diff --git a/packages/microcosm-build/tests/golden/nz_country_spec.json b/packages/microcosm-build/tests/golden/nz_country_spec.json index 904e42286c..88360280a1 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": "06ce7ca0820bee4ff34067377b3c654b7d733a7afc69deba8abfa51ac7a783d3", + "fingerprint": "9d0b57a16ddb6c4985707d148c597469315bc0244ba2e54555d6cd078ffd5c93", "gate_ids": [ "calibration_per_family_fit", "demography_per_family_fit", @@ -43,20 +43,20 @@ "as_encoding.json": "91e0e9972abfaaa202a13ede6e2dbc815a4c6c9514726c970928a47adfef0e32", "as_rate_bridge.json": "1399321cc7ef44486f7ab407968b036930ff6ae8fbab4c30ce710ea0fff7301c", "atomic_geography_support.json": "6efd09d5cde2f16d4d01e522a721191a829a754d2ce019d75bb0407085ecb4b7", - "axiom_input_closure.json": "791c1cd420de8eddde8af6f9bc8a9aa5627d517f90ffed9b0012a36734071058", + "axiom_input_closure.json": "d2694200e0191803f1fdfa231106ed35fb9de3bbb0594e87bff87fed19849ce6", "axiom_rules_bindings.json": "2ec91292e3231a4524c372ba78f1e9fedc62a34f8a4f6e38a274c9f608d138a6", "benefit_gross_up.json": "5518e7ad23b4053d36df842a3335358f8ff89c494ed065e540eb83585d8c6137", "benefit_unit_rule.json": "c84efba8353c770131256a8c17b82c7d98f738e6dd8ef217e19d30014cb14604", "calibration_bindings.json": "26983da893770a7ea915d9eee70c47f310654ffe0fdffcc0327ac5a34c01bf50", "calibration_settings.json": "56e4d1da8468245da6bd41daf263425cc65e4e9c9f11ff45eee459d3aed1989e", "chronicle_feed.json": "60a80dd0042838b0412baf9dd0985c961b4253a6876d16bcdbb8a2c6c03de0c7", - "concept_mapping.json": "25171d7599a9d565e158af9ca010477db87ff0876bd374374329c0c58bf9f8bf", + "concept_mapping.json": "1645d1974fd24e754361bb585cff2bff8905c1f742014c126cb4ed818227774b", "country_package.json": "7f8a7eee604c843c32bf1f6462706dc39a6d32df93276d4fe92aaf3b53941a97", "currency_bridge.json": "a23fcf09d364d22f346d3faf2ed59ddb6d2b2da88c9757e46f1d624b175ad21b", "earnings_bands.json": "362d094ca03ec3f636053466f33ec079758f17b8a9d66da2c928f59bfa0cac07", "entitlement_graph.json": "75be6d21bae116691711a4c2191b2120603a3e048758d37e21361166ab120b5a", "export_contract.json": "0000fbc98cec95bc8216dc968ffc09fd9a3a07455697275ba7cf67dec54f17b2", - "export_replay_bindings.json": "b503d012466ef51364ff5ae635191a01c41b89b96292b6131ee50cf28edc121d", + "export_replay_bindings.json": "ca0172fc5306ed316c74a24ccfa28feceb4dbeab926f6460dbfdd328287cf700", "family_reconciliation.json": "20e3f9f3ce6266d57828d721c99d15bd3642e6c6314a7076ec38972de02bc69e", "gap_annualisation.json": "54762c18c549d2f1f35a630358080e3d581abab901b52a62b4f279830f3f8536", "gates.json": "9dcd748ddc22e99c9422a62c09b3fc22b6402c9968376dfaa6dad5b4d76f8a8e", @@ -80,7 +80,7 @@ "reference_profiles.json": "dc3d81e6fdeae12b93176872de690e21424695209390b253a3490e65cfd5740c", "reference_selection.json": "d7d1fbbd1c240c1943a3daa2e8c509cc33363dbd8d8bf6f405e4a7048ea86d6a", "release_contract.json": "6cd5e3a4a2bf30e4025e04e964c393b24040937d13db7e6a07b3966a55f3b114", - "reporting_classifiers.json": "d50c1a24ee0959def0d3f95bf7f965f7acacfa2d9e2d84afce4567173e66d2dc", + "reporting_classifiers.json": "60f0936a9cab3776affda81e2612174dbc2192947630e9cc037b9b11277ca4d2", "scenarios.json": "b0faa24cc01adee6cf67a0691b0f8115784140ac4b1c3c8866b53f056777634c", "source_stages.json": "cc82c68878ac0fc07ecbd796c1a66de6f34786ef8785125046425c37f26aff31", "spec/bundle.yaml": "e9716ce827e9006d4efdc392e7aaf9cbc33c16112a8fbc423e8f3e74ec506d8e", @@ -91,7 +91,7 @@ "spec/vintages.yaml": "1ecc1a3f952bd25b834c394314e8c933ddb5b095fd64ba492dddb03e3cdd803e", "target_references.json": "6af5258a4e3451d7742ab9f3b454fec9e0bd7cf320fab6e784ff002a8be211c8", "transport_atomic_geography_support.json": "53465827f67c87aaf5a06c305c082cbf1e6cdf28f3843e853d1e012635517d10", - "transport_graph.json": "1e5b482248fd814b63c9db8a9149d3b0773aa8539fa9c7bf1567a893a7c1dac9", + "transport_graph.json": "91e0dfa2f8cdc70dda4450a1d44db37a4bf4e7b46e8f4636bf3969c66718ec8b", "transport_inputs.json": "16d3957214d6305e3d3f41fe6343143a411d6055c75c43d3f171db3ee659ec2d" }, "resources": [ diff --git a/packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json b/packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json index 49f6e3cd45..613f3e1e21 100644 --- a/packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json +++ b/packages/microcosm-frame/src/microcosm/frame/adapters/axiom_concept_mappings/nz.json @@ -411,6 +411,21 @@ "kind": "identity" } }, + { + "canonical_input": "nz:statutes/new_zealand_superannuation/core#input.nz_super_shares_residence_with_person_at_least_adult_age", + "concepts": [ + "fact:person.age" + ], + "engine_entity": "Person", + "engine_input": "nz_super_shares_residence_with_person_at_least_adult_age", + "module": "nz/statutes/new_zealand_superannuation/core.yaml", + "note": "nz/statutes/new_zealand_superannuation/core.yaml rule nz_super_living_alone (New Zealand Superannuation and Retirement Income Act 2001 s 4A): 'nz_super_single and nz_super_principal_residence_is_qualifying_accommodation and (not nz_super_shares_residence_with_person_at_least_adult_age or nz_super_all_shared_adults_are_exempt_dependent_children_or_temporary_visitors)'. The binding returns true when at least one other person with the same frame household id is aged min_age or over. min_age 18 copies parameter nz_super_living_alone_adult_age in the same module (18 from 2026-04-01, source s 4A(3) and (4)) by hand: no RuleSpec formula reads that parameter, so nothing checks min_age against it, and a change to the parameter needs a matching edit here. Differences: the frame's household stands in for the statute's residence, so the binding counts every other person aged 18 or over who carries the person's household id, and misses an adult who shares the residence under another household id; it counts a partner, but nz_super_living_alone requires nz_super_single and nz_super_ordinary_weekly_rate_before_tax branches on nz_super_single first, so the rate never reads a partnered person's value; it counts adult dependent children and temporary visitors, whom nz_super_all_shared_adults_are_exempt_dependent_children_or_temporary_visitors carves out, and the input closure defaults that input to false; ages are completed years at the survey reference date, so a co-resident who turns 18 between that date and the assessment reads differently; a person with no household id reads false.", + "relation": "approximate", + "transform": { + "kind": "coresident_adult", + "min_age": 18 + } + }, { "canonical_input": "nz:statutes/new_zealand_superannuation/core#input.nz_super_single", "concepts": [ diff --git a/packages/microcosm-frame/tests/golden/concept-coverage/axiom-nz.json b/packages/microcosm-frame/tests/golden/concept-coverage/axiom-nz.json index 0660e15e34..c609adf45d 100644 --- a/packages/microcosm-frame/tests/golden/concept-coverage/axiom-nz.json +++ b/packages/microcosm-frame/tests/golden/concept-coverage/axiom-nz.json @@ -166,6 +166,11 @@ "module": "nz/statutes/new_zealand_superannuation/core.yaml", "name": "nz_super_person_age" }, + { + "entity": "Person", + "module": "nz/statutes/new_zealand_superannuation/core.yaml", + "name": "nz_super_shares_residence_with_person_at_least_adult_age" + }, { "entity": "Person", "module": "nz/statutes/social_security/main_benefits/entitlement.yaml", @@ -932,7 +937,7 @@ } ] }, - "covered_count": 89, + "covered_count": 90, "covered_inputs": [ { "entity": "Person", @@ -1179,6 +1184,11 @@ "module": "nz/statutes/new_zealand_superannuation/special_rates.yaml", "name": "nz_super_receives_or_becomes_entitled" }, + { + "entity": "Person", + "module": "nz/statutes/new_zealand_superannuation/core.yaml", + "name": "nz_super_shares_residence_with_person_at_least_adult_age" + }, { "entity": "Person", "module": "nz/statutes/new_zealand_superannuation/core.yaml", @@ -3506,11 +3516,6 @@ "module": "nz/statutes/new_zealand_superannuation/special_rates.yaml", "name": "nz_super_saved_non_qualifying_spouse_rate_eligible_under_schedule_1aa_clause_7" }, - { - "entity": "Person", - "module": "nz/statutes/new_zealand_superannuation/core.yaml", - "name": "nz_super_shares_residence_with_person_at_least_adult_age" - }, { "entity": "Person", "module": "nz/statutes/new_zealand_superannuation/special_rates.yaml", diff --git a/test_support/microcosm_frame/concept_reference.py b/test_support/microcosm_frame/concept_reference.py index 3080fea506..f8c0e2124c 100644 --- a/test_support/microcosm_frame/concept_reference.py +++ b/test_support/microcosm_frame/concept_reference.py @@ -20,6 +20,7 @@ from microcosm.frame.concept_mapping import ( AllocateToReferencePerson, ConceptMapping, + CoresidentAdult, CoresidentChildCount, Fraction, Identity, @@ -161,6 +162,18 @@ def is_parent_of(row: dict, child: dict, max_age: int | None) -> bool: and (transform.max_age is None or child["age"] <= transform.max_age) ) ) + elif isinstance(transform, CoresidentAdult): + home = row["person_household_id"] + out.append( + _present(home) + and any( + other is not row + and other["person_household_id"] == home + and _present(other["age"]) + and other["age"] >= transform.min_age + for other in persons + ) + ) elif isinstance(transform, TakeUpThreshold): seed = np.asarray([value(row, concepts[0])], dtype=np.float64) draw = derive_take_up_draws(seed, transform.program)[0] diff --git a/tools/build_nz_transport_graph.py b/tools/build_nz_transport_graph.py index dcb81f39eb..b52f1f6fb2 100644 --- a/tools/build_nz_transport_graph.py +++ b/tools/build_nz_transport_graph.py @@ -205,6 +205,7 @@ def closure_entry_dtype(entry, mapping): "str": "string", }[concept(binding["concepts"][0]).dtype] elif transform in { + "coresident_adult", "predicate", "relationship_role", "positive", @@ -1267,7 +1268,7 @@ def documents(): "terminal": ["nz.export.prepare", "nz.package"], }, "pending_outputs": [ - "G8-x income reconciliation evidence: the native-band report, omission-register and identity-receipt artifacts and the VSI, VEX and VPT run evidence that measurement_prefit requires (R2C-18), and the NZS sharing binding (R2C-5)", + "G8-x income reconciliation evidence: the native-band report, omission-register and identity-receipt artifacts and the VSI, VEX and VPT run evidence that measurement_prefit requires (R2C-18)", "Census rent and liquid-assets quantile producers", ], }