From a6c44418afccac0270dcb9ea155338dac0c9a05d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 29 Sep 2026 12:09:47 -0400 Subject: [PATCH 01/10] Count Missouri TANF parents claimed as tax dependents as unit members 13 CSR 40-2.300(5)(C) and the Combined IM Policy Manual 4.2.2 (formerly DSS Manual 0210.005.05) make the biological or adoptive parents of the eligible children mandatory members, whoever claims them for taxes. mo_tanf_is_assistance_unit_member admitted only tax-unit heads and spouses, so a 20-year-old mother claimed by her own mother was left out and the unit was her child alone. A tax-unit member marked mo_tanf_is_parent_of_dependent_child is now a member unless they receive SSI, are marked a non-parent caretaker, or are a dependent child (then they are a member as a child, and the three-generation family files as one group). The default for that input now also requires being 12 to 50 years older than one of the tax unit's dependent children, which rules out, for example, the head's elderly mother. Co-Authored-By: Claude Opus 5.5 --- .../mo-tanf-dependent-parent-member.fixed.md | 1 + ...est_mo_tanf_dependent_parent_properties.py | 448 +++++++++++++++++ ...mo_tanf_non_parent_caretaker_properties.py | 12 +- .../mo_tanf_is_assistance_unit_member.yaml | 472 ++++++++++++++++++ .../non_parent_caretaker/integration.yaml | 11 +- .../mo_tanf_non_parent_caretaker.yaml | 15 +- .../mo_tanf_is_assistance_unit_member.py | 28 +- .../mo_tanf_is_parent_of_dependent_child.py | 44 +- .../income/is_mo_tanf_earned_income_exempt.py | 4 +- 9 files changed, 1004 insertions(+), 31 deletions(-) create mode 100644 changelog.d/mo-tanf-dependent-parent-member.fixed.md create mode 100644 policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml diff --git a/changelog.d/mo-tanf-dependent-parent-member.fixed.md b/changelog.d/mo-tanf-dependent-parent-member.fixed.md new file mode 100644 index 00000000000..f5b2bb2dde7 --- /dev/null +++ b/changelog.d/mo-tanf-dependent-parent-member.fixed.md @@ -0,0 +1 @@ +Missouri TANF now counts a parent claimed as someone else's tax dependent, such as a young mother claimed by her own parent, as a member of the assistance unit with their child. diff --git a/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py b/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py new file mode 100644 index 00000000000..4d62542a7be --- /dev/null +++ b/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py @@ -0,0 +1,448 @@ +"""Invariants of Missouri TANF membership for parents claimed as tax dependents. + +13 CSR 40-2.300(5)(C) and DSS Manual 0210.005.05 put the "biological or +adoptive parents of one or more of the eligible children" in the assistance +unit, whoever claims them for taxes. SSI recipients are excluded (13 CSR +40-2.310(1)(F)). A parent who is also a cash-eligible child is a member as a +child, and DSS Manual 0210.005.30 lets that three-generation family file as +one group. A non-parent caretaker relative is excluded while a parent is in +the home (0210.005.10). + +Every combination in a grid of households shares one simulation and is +checked against those rules, restated here independently of the formulas: + +1. Membership: children not on SSI; unmarked heads and spouses not on SSI; + a tax-dependent adult when they are a dependent child, or when they are a + parent of the children and not on SSI; a marked head or spouse only as an + included non-parent caretaker, which needs no parent in the home. +2. A dependent parent who is a member always excludes the non-parent + caretaker. +3. Marking the dependent adult as a parent adds exactly that adult to the + unit and removes, at most, an included non-parent caretaker. +4. Unit income is the sum of members' income, less the student exemption. +5. With no income anywhere, the grant is the payment standard for the unit + size, and marking a parent never lowers it, unless the parent receives + SSI: then they stay out of the unit but still exclude the non-parent + caretaker (intended). +6. The default input (own children in the household) gives the same answer + as marking the parent directly, and each household gets the same answer + alone as in the shared simulation. +7. The default marks a person with own children in the household only when + some dependent child in the tax unit is 12 to 50 years younger, checked + pair by pair against the formula's youngest-and-oldest shortcut. +""" + +import itertools + +import numpy as np +import pytest + +from policyengine_us import Simulation + +PERIOD = "2026-01" +YEAR = 2026 +# 13 CSR 40-2.310(13) standard of need and (12) payment percentage. +STANDARD_OF_NEED = {1: 393, 2: 678, 3: 846, 4: 990, 5: 1_123, 6: 1_247} +PAYMENT_PERCENTAGE = 0.34526 + +# How the tax-dependent adult is described to the model. +# parent: marked a parent of the children +# non_parent: own_children_in_household = 1 but marked not a parent of the +# children (for example the head's own mother) +# ssi_parent: marked a parent, and receives SSI +# own_children: only own_children_in_household = 1, leaving +# mo_tanf_is_parent_of_dependent_child to its default. A situation that +# sets an input for anyone sets it for everyone (unset people get the +# default value, not the formula), so these households get a +# simulation of their own. +ADULT_KINDS = ["parent", "non_parent", "ssi_parent"] +PARENT_KINDS = {"parent", "own_children", "ssi_parent"} + + +def _adult_configs(): + configs = [dict(adult=None)] + for kind, age, school, earned, unearned in itertools.product( + ADULT_KINDS, (18, 22), (False, True), (0, 300), (0, 150) + ): + configs.append( + dict( + adult=kind, + adult_age=age, + adult_school=school, + adult_earned=earned, + adult_unearned=unearned, + ) + ) + return configs + + +def _households(): + households = [] + for marked, married, head_ssi, minor, child_ssi, adult in itertools.product( + (False, True), + (False, True), + (False, True), + (False, True), + (False, True), + _adult_configs(), + ): + households.append( + dict( + marked=marked, + married=married, + head_ssi=head_ssi, + minor_parent=minor, + child_ssi=child_ssi, + **adult, + ) + ) + return households + + +HOUSEHOLDS = _households() +N = len(HOUSEHOLDS) + + +def _roles(h): + roles = ["head"] + if h["married"]: + roles.append("spouse") + if h["adult"]: + roles.append("adult") + if h["minor_parent"]: + roles += ["minor_parent", "baby"] + roles.append("child") + return roles + + +def _person(h, role, adult_kind=None): + if role in ("head", "spouse"): + record = { + "age": {YEAR: 55 if role == "head" else 53}, + "is_tax_unit_dependent": {YEAR: False}, + "receives_ssi": {YEAR: role == "head" and h["head_ssi"]}, + "mo_tanf_is_non_parent_caretaker": {YEAR: h["marked"]}, + } + if role == "head" and h["adult"] == "own_children": + # The head's own children in the home are the dependent adult and + # the 16-year-old mother, so the default marks the head a parent + # too; the non-parent mark must still decide for a head. + count = 1 + h["minor_parent"] + record["own_children_in_household"] = {YEAR: count} + return record + if role == "adult": + kind = adult_kind or h["adult"] + record = { + "age": {YEAR: h["adult_age"]}, + "is_tax_unit_dependent": {YEAR: True}, + "is_in_secondary_school": {YEAR: h["adult_school"]}, + "employment_income_before_lsr": {YEAR: 12 * h["adult_earned"]}, + "unemployment_compensation": {YEAR: 12 * h["adult_unearned"]}, + # SSI receipt stays with the household when adult_kind overrides + # only whether the adult is marked a parent. + "receives_ssi": {YEAR: h["adult"] == "ssi_parent"}, + } + if kind != "parent" and kind != "ssi_parent": + record["own_children_in_household"] = {YEAR: 1} + if kind != "own_children": + record["mo_tanf_is_parent_of_dependent_child"] = { + YEAR: kind in PARENT_KINDS + } + return record + if role == "minor_parent": + return { + "age": {YEAR: 16}, + "is_tax_unit_dependent": {YEAR: True}, + "own_children_in_household": {YEAR: 1}, + } + if role == "baby": + return {"age": {YEAR: 0}, "is_tax_unit_dependent": {YEAR: True}} + return { + "age": {YEAR: 6}, + "is_tax_unit_dependent": {YEAR: True}, + "receives_ssi": {YEAR: h["child_ssi"]}, + } + + +def _situation(households, adult_kind=None): + people, tax_units, spm_units, units = {}, {}, {}, {} + for i, h in enumerate(households): + names = [] + for role in _roles(h): + name = f"h{i}_{role}" + people[name] = _person(h, role, adult_kind) + names.append(name) + tax_units[f"t{i}"] = {"members": names} + spm_units[f"s{i}"] = {"members": names} + units[f"hh{i}"] = {"members": names, "state_code": {YEAR: "MO"}} + return { + "people": people, + "tax_units": tax_units, + "spm_units": spm_units, + "households": units, + } + + +PEOPLE = [(i, role) for i, h in enumerate(HOUSEHOLDS) for role in _roles(h)] + + +def _calc(sim, variable): + return sim.calculate(variable, PERIOD) + + +def _payment(size): + return STANDARD_OF_NEED.get(int(size), 0) * PAYMENT_PERCENTAGE + + +def _adult_is_dependent_child(h): + # 13 CSR 40-2.325(1)(A): under 18, or 18 and in secondary school. + return h["adult"] is not None and h["adult_age"] == 18 and h["adult_school"] + + +def _adult_is_parent(h, kind=None): + kind = kind or h["adult"] + return kind in PARENT_KINDS + + +def _parent_in_home(h, kind=None): + """A parent of the children lives in the home (DSS Manual 0210.005.10).""" + unmarked_caretaker = not h["marked"] + adult_ssi = h["adult"] == "ssi_parent" + # A parent who is a cash-eligible child does not count (0210.005.05); the + # 16-year-old mother is always one. + cash_eligible_child = _adult_is_dependent_child(h) and not adult_ssi + adult_parent = ( + h["adult"] is not None and _adult_is_parent(h, kind) and not cash_eligible_child + ) + return unmarked_caretaker or adult_parent + + +def _expected_member(h, role, kind=None): + """Restated membership, or None where the non-parent caretaker rules decide.""" + kind = kind or h["adult"] + if role == "head": + if not h["marked"]: + return not h["head_ssi"] + return None + if role == "spouse": + return None if h["marked"] else True + if role == "adult": + ssi = h["adult"] == "ssi_parent" + if _adult_is_dependent_child(h): + return not ssi + return _adult_is_parent(h, kind) and not ssi + if role in ("minor_parent", "baby"): + return True + return not h["child_ssi"] + + +@pytest.fixture(scope="module") +def sim(): + return Simulation(situation=_situation(HOUSEHOLDS)) + + +@pytest.fixture(scope="module") +def non_parent_sim(): + # The same households with the dependent adult marked as not a parent of + # the children: the rule before parents claimed as dependents were members. + return Simulation(situation=_situation(HOUSEHOLDS, adult_kind="non_parent")) + + +def test_membership_matches_the_restated_rule(sim): + member = _calc(sim, "mo_tanf_is_assistance_unit_member") + npcr = _calc(sim, "mo_tanf_non_parent_caretaker") + included = _calc(sim, "mo_tanf_non_parent_caretaker_included") + for k, (i, role) in enumerate(PEOPLE): + h = HOUSEHOLDS[i] + expected = _expected_member(h, role) + if expected is None: + # A marked head or spouse is a member only as an included + # non-parent caretaker, which requires no parent in the home. + assert member[k] == (npcr[k] and included[i]), (h, role) + if member[k]: + assert not _parent_in_home(h), (h, role) + else: + assert member[k] == expected, (h, role) + + +def test_a_dependent_parent_member_excludes_the_non_parent_caretaker(sim): + member = _calc(sim, "mo_tanf_is_assistance_unit_member") + npcr = _calc(sim, "mo_tanf_non_parent_caretaker") + has_npcr = np.zeros(N, dtype=bool) + parent_member = np.zeros(N, dtype=bool) + for k, (i, role) in enumerate(PEOPLE): + has_npcr[i] |= npcr[k] + if role == "adult" and not _adult_is_dependent_child(HOUSEHOLDS[i]): + parent_member[i] |= member[k] + assert parent_member.any() + assert not (parent_member & has_npcr).any() + # The non-parent caretaker rules see a parent in the home exactly when + # the restated rule does. + for i, h in enumerate(HOUSEHOLDS): + if _parent_in_home(h): + assert not has_npcr[i], h + + +def test_marking_a_parent_adds_only_that_parent(sim, non_parent_sim): + on = _calc(sim, "mo_tanf_is_assistance_unit_member") + off = _calc(non_parent_sim, "mo_tanf_is_assistance_unit_member") + off_npcr = _calc(non_parent_sim, "mo_tanf_non_parent_caretaker") + for k, (i, role) in enumerate(PEOPLE): + if on[k] and not off[k]: + assert role == "adult", (HOUSEHOLDS[i], role) + if off[k] and not on[k]: + # Only an included non-parent caretaker can leave, displaced by + # the parent now seen in the home. + assert off_npcr[k], (HOUSEHOLDS[i], role) + # Before this rule, a dependent adult who is not a dependent child was + # never a member. + for k, (i, role) in enumerate(PEOPLE): + if role == "adult" and not _adult_is_dependent_child(HOUSEHOLDS[i]): + assert not off[k] + + +def test_unit_income_is_members_income(sim): + member = _calc(sim, "mo_tanf_is_assistance_unit_member") + earned = _calc(sim, "mo_tanf_gross_earned_income") + unearned = _calc(sim, "mo_tanf_gross_unearned_income") + expected_earned = np.zeros(N) + expected_unearned = np.zeros(N) + for k, (i, role) in enumerate(PEOPLE): + h = HOUSEHOLDS[i] + if role != "adult" or not member[k]: + continue + expected_unearned[i] += h["adult_unearned"] + # 13 CSR 40-2.310(9)(A)1: a student dependent child's earnings are + # exempt. + if not _adult_is_dependent_child(h): + expected_earned[i] += h["adult_earned"] + assert earned == pytest.approx(expected_earned) + assert unearned == pytest.approx(expected_unearned) + + +def test_zero_income_grant_is_the_payment_standard(sim, non_parent_sim): + size = _calc(sim, "mo_tanf_assistance_unit_size") + grant = _calc(sim, "mo_tanf") + off_grant = _calc(non_parent_sim, "mo_tanf") + checked = 0 + for i, h in enumerate(HOUSEHOLDS): + assert 0 <= grant[i] <= _payment(size[i]) + 1e-3 + no_income = h["adult"] is None or ( + h["adult_earned"] == 0 and h["adult_unearned"] == 0 + ) + if not no_income: + continue + checked += 1 + assert grant[i] == pytest.approx(_payment(size[i]), abs=1e-3), h + # A parent with no income adds needs and no income, so marking them + # never lowers the grant. Intended exception: a parent on SSI is not a + # member but still excludes the non-parent caretaker (0210.005.10). + if h["adult"] != "ssi_parent": + assert grant[i] >= off_grant[i] - 1e-3, h + assert checked > N / 4 + + +def test_default_input_matches_marking_the_parent(): + marked = [h for h in HOUSEHOLDS if h["adult"] == "parent"] + own_children = [{**h, "adult": "own_children"} for h in marked] + default_sim = Simulation(situation=_situation(own_children)) + marked_sim = Simulation(situation=_situation(marked)) + flag = default_sim.calculate("mo_tanf_is_parent_of_dependent_child", YEAR) + roles = [role for h in own_children for role in _roles(h)] + # The default is own_children_in_household > 0: the head, the dependent + # adult and the 16-year-old mother, nobody else. + assert flag.tolist() == [ + role in ("head", "adult", "minor_parent") for role in roles + ] + member = _calc(default_sim, "mo_tanf_is_assistance_unit_member") + k = 0 + for h in own_children: + for role in _roles(h): + expected = _expected_member(h, role) + if expected is not None: + assert member[k] == expected, (h, role) + k += 1 + for variable in ( + "mo_tanf_is_assistance_unit_member", + "mo_tanf_non_parent_caretaker", + "mo_tanf_assistance_unit_size", + "mo_tanf", + ): + assert _calc(default_sim, variable) == pytest.approx( + _calc(marked_sim, variable) + ), variable + + +@pytest.mark.parametrize("i", range(0, N, 97)) +def test_each_household_alone_matches_the_shared_simulation(sim, i): + alone = Simulation(situation=_situation([HOUSEHOLDS[i]])) + for variable in ("mo_tanf_assistance_unit_size", "mo_tanf"): + assert _calc(alone, variable)[0] == pytest.approx(_calc(sim, variable)[i]) + + +def test_default_parent_input_matches_the_pairwise_age_rule(): + # The default is own_children_in_household > 0 and being 12 to 50 years + # older than some dependent child in the tax unit. The formula checks only + # the youngest and oldest child; restate it pair by pair here. + adult_ages = [14, 17, 19, 22, 30, 40, 45, 50, 55, 58, 60, 62, 64, 68, 70, 80] + # Each entry: ages of the other children, and whether an 18-year-old + # secondary-school student (a dependent child) is also in the unit. + child_sets = [ + ([0], False), + ([8], False), + ([10], False), + ([17], False), + ([0, 17], False), + ([5, 12], False), + ([2], True), + ([], True), + ] + cases = [ + (age, children, student, own) + for age, (children, student), own in itertools.product( + adult_ages, child_sets, (0, 1) + ) + ] + people, tax_units, spm_units, units = {}, {}, {}, {} + for i, (age, children, student, own) in enumerate(cases): + names = [f"c{i}_head", f"c{i}_person"] + people[names[0]] = {"age": {YEAR: 45}, "is_tax_unit_dependent": {YEAR: False}} + people[names[1]] = { + "age": {YEAR: age}, + "is_tax_unit_dependent": {YEAR: True}, + "own_children_in_household": {YEAR: own}, + } + for j, child_age in enumerate(children): + names.append(f"c{i}_child{j}") + people[names[-1]] = { + "age": {YEAR: child_age}, + "is_tax_unit_dependent": {YEAR: True}, + } + if student: + names.append(f"c{i}_student") + people[names[-1]] = { + "age": {YEAR: 18}, + "is_tax_unit_dependent": {YEAR: True}, + "is_in_secondary_school": {YEAR: True}, + } + tax_units[f"t{i}"] = {"members": names} + spm_units[f"s{i}"] = {"members": names} + units[f"hh{i}"] = {"members": names, "state_code": {YEAR: "MO"}} + simulation = Simulation( + situation={ + "people": people, + "tax_units": tax_units, + "spm_units": spm_units, + "households": units, + } + ) + flag = simulation.calculate("mo_tanf_is_parent_of_dependent_child", YEAR) + position = 0 + for age, children, student, own in cases: + person_flag = flag[position + 1] + position += 2 + len(children) + student + child_ages = list(children) + ([18] if student else []) + # A person under 18 is a dependent child too, but their own age + # difference of zero never falls in the window. + plausible = any(12 <= age - c <= 50 for c in child_ages) + assert person_flag == (own > 0 and plausible), (age, children, student, own) diff --git a/policyengine_us/tests/core/test_mo_tanf_non_parent_caretaker_properties.py b/policyengine_us/tests/core/test_mo_tanf_non_parent_caretaker_properties.py index 1f21026fc09..a853ddc52bf 100644 --- a/policyengine_us/tests/core/test_mo_tanf_non_parent_caretaker_properties.py +++ b/policyengine_us/tests/core/test_mo_tanf_non_parent_caretaker_properties.py @@ -13,9 +13,10 @@ those rules, restated here independently of the variable formulas: 1. With no one marked as a non-parent caretaker, unit membership, countable - resources and the teen-parent earnings exemption equal the pre-NPCR rule. - No other existing Missouri TANF formula reads the new variables, so every - existing household is unchanged. + resources and the teen-parent earnings exemption equal the pre-NPCR rule, + plus the dependent adult parent, who is a member as a parent of the + children (DSS Manual 0210.005.05; see + test_mo_tanf_dependent_parent_properties.py). 2. Identification and neediness equal the restated rules: no NPCR when a parent is in the home (an unmarked caretaker, or a dependent adult with their own children in the household); otherwise the NPCR is needy when @@ -195,8 +196,9 @@ def test_unflagged_households_follow_the_pre_npcr_rule(unflagged_sim): elif role == "spouse": expected.append(not h["spouse_ssi"]) elif role == "adult_parent": - # A dependent aged 22 is neither a caretaker nor a child. - expected.append(False) + # A dependent aged 22 with a child of her own in the home is a + # parent of the children, so she is a member (0210.005.05). + expected.append(True) else: expected.append(not (h["child_ssi"] and c == 0)) assert member.tolist() == expected diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml new file mode 100644 index 00000000000..3ac066c9550 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml @@ -0,0 +1,472 @@ +# Parents in the Missouri TANF assistance unit when someone else claims them +# as a tax dependent: 13 CSR 40-2.300(5)(C), 13 CSR 40-2.310(1)(F) and +# DSS Manual 0210.005.05, 0210.005.10 and 0210.005.30. +# Standard of need: size 1 $393, size 2 $678, size 3 $846. Payment is 34.526% +# of the standard: size 1 $135.687, size 2 $234.086, size 3 $292.090. + +- name: A mother claimed as her own mother's tax dependent is the caretaker for her child + period: 2026-01 + input: + people: + grandparent: + age: 55 + mo_tanf_is_non_parent_caretaker: true + mother: + age: 20 + is_tax_unit_dependent: true + mo_tanf_is_parent_of_dependent_child: true + child: + age: 3 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandparent, mother, child] + spm_units: + spm_unit: + members: [grandparent, mother, child] + households: + household: + members: [grandparent, mother, child] + state_code: MO + output: + # DSS Manual 0210.005.05 includes the "Biological or adoptive parents of + # one or more of the eligible children" whatever their tax filing. The + # grandparent is excluded because a parent is in the home (0210.005.10). + mo_tanf_non_parent_caretaker: [false, false, false] + mo_tanf_is_assistance_unit_member: [false, true, true] + mo_tanf_assistance_unit_size: 2 + mo_tanf: 234.086 + +- name: The dependent mother's earnings count and the excluded grandparent's do not + period: 2026-01 + input: + people: + grandparent: + age: 55 + employment_income_before_lsr: 24_000 # $2,000/month + mo_tanf_is_non_parent_caretaker: true + mother: + age: 20 + is_tax_unit_dependent: true + employment_income_before_lsr: 3_600 # $300/month + own_children_in_household: 1 + child: + age: 3 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandparent, mother, child] + spm_units: + spm_unit: + members: [grandparent, mother, child] + households: + household: + members: [grandparent, mother, child] + state_code: MO + output: + # 0210.005.05: "Include the needs and income of the following persons". + # The mother's $300 less the $90 work expense, $30 and one-third of the + # remaining $180 leaves $120 (13 CSR 40-2.310(9)(A)2-3): + # 234.086 - 120 = 114.086. The mother is 20, so no minor-parent deeming + # of the grandparent's income applies (13 CSR 40-2.310(8)(B)1.F). + mo_tanf_is_assistance_unit_member: [false, true, true] + mo_tanf_gross_earned_income: 300 + mo_tanf_countable_income: 120 + mo_tanf: 114.086 + +- name: An 18-year-old mother out of school is the caretaker, not a child + period: 2026-01 + input: + people: + grandparent: + age: 50 + mo_tanf_is_non_parent_caretaker: true + mother: + age: 18 + is_tax_unit_dependent: true + is_in_secondary_school: false + own_children_in_household: 1 + baby: + age: 0 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandparent, mother, baby] + spm_units: + spm_unit: + members: [grandparent, mother, baby] + households: + household: + members: [grandparent, mother, baby] + state_code: MO + output: + # An 18-year-old not in secondary school is not a dependent child + # (13 CSR 40-2.325(1)(A)), so she is the parent member. + mo_tanf_dependent_child: [false, false, true] + mo_tanf_is_assistance_unit_member: [false, true, true] + mo_tanf_assistance_unit_size: 2 + mo_tanf: 234.086 + +- name: An 18-year-old mother in high school is a cash-eligible child with her own mother as caretaker + period: 2026-01 + input: + people: + grandparent: + age: 50 + mother: + age: 18 + is_tax_unit_dependent: true + is_in_secondary_school: true + employment_income_before_lsr: 6_000 # $500/month + own_children_in_household: 1 + baby: + age: 0 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandparent, mother, baby] + spm_units: + spm_unit: + members: [grandparent, mother, baby] + households: + household: + members: [grandparent, mother, baby] + state_code: MO + output: + # She is a dependent child (under 19 and in secondary school), so she is a + # member as an eligible child and her student earnings are exempt + # (13 CSR 40-2.310(9)(A)1). DSS Manual 0210.005.30 lets the family file as + # one three-generation assistance group with her mother as caretaker. + mo_tanf_dependent_child: [false, true, true] + mo_tanf_is_assistance_unit_member: [true, true, true] + mo_tanf_gross_earned_income: 0 + mo_tanf_assistance_unit_size: 3 + mo_tanf: 292.090 + +- name: A 16-year-old mother, her baby and her mother form one three-generation group + period: 2026-01 + input: + people: + grandparent: + age: 40 + mother: + age: 16 + is_tax_unit_dependent: true + own_children_in_household: 1 + baby: + age: 0 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandparent, mother, baby] + spm_units: + spm_unit: + members: [grandparent, mother, baby] + households: + household: + members: [grandparent, mother, baby] + state_code: MO + output: + # DSS Manual 0210.005.30: "If a three-generation family ... files together + # as one assistance group, consider all their needs and income as a single + # assistance group". The minor mother is a member as an eligible child. + mo_tanf_is_assistance_unit_member: [true, true, true] + mo_tanf_assistance_unit_size: 3 + mo_tanf: 292.090 + +- name: An unmarried father claimed as the mother's dependent is a member with their child + period: 2026-01 + input: + people: + mother: + age: 30 + father: + age: 28 + is_tax_unit_dependent: true + own_children_in_household: 1 + child: + age: 2 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [mother, father, child] + spm_units: + spm_unit: + members: [mother, father, child] + households: + household: + members: [mother, father, child] + state_code: MO + output: + # DSS Manual 0210.005.05: "Ms. Devin and Mr. Roberts are not married, but + # they have an in-common child ... Complete an application for the + # household with everyone included." + mo_tanf_is_assistance_unit_member: [true, true, true] + mo_tanf_assistance_unit_size: 3 + mo_tanf: 292.090 + +- name: Two dependent parents living with the child's grandparents are both members + period: 2026-01 + input: + people: + grandfather: + age: 60 + mo_tanf_is_non_parent_caretaker: true + grandmother: + age: 58 + mo_tanf_is_non_parent_caretaker: true + mother: + age: 22 + is_tax_unit_dependent: true + own_children_in_household: 1 + father: + age: 21 + is_tax_unit_dependent: true + own_children_in_household: 1 + child: + age: 1 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandfather, grandmother, mother, father, child] + spm_units: + spm_unit: + members: [grandfather, grandmother, mother, father, child] + households: + household: + members: [grandfather, grandmother, mother, father, child] + state_code: MO + output: + mo_tanf_non_parent_caretaker: [false, false, false, false, false] + mo_tanf_is_assistance_unit_member: [false, false, true, true, true] + mo_tanf_assistance_unit_size: 3 + mo_tanf: 292.090 + +- name: A dependent adult who is a parent, but not of an eligible child, is not a member + period: 2026-01 + input: + people: + grandmother: + age: 58 + is_tax_unit_dependent: true + unemployment_compensation: 6_000 # $500/month + # Her own child in the household is the mother, an adult. + own_children_in_household: 1 + mo_tanf_is_parent_of_dependent_child: false + mother: + age: 38 + child: + age: 12 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandmother, mother, child] + spm_units: + spm_unit: + members: [grandmother, mother, child] + households: + household: + members: [grandmother, mother, child] + state_code: MO + output: + # 0210.005.05 includes only parents "of one or more of the eligible + # children"; 0210.005.10 excludes other household members' needs and + # income. + mo_tanf_is_assistance_unit_member: [false, true, true] + mo_tanf_assistance_unit_size: 2 + mo_tanf_gross_unearned_income: 0 + mo_tanf: 234.086 + +- name: Without the input, a grandmother young enough to be the child's parent is counted as one + period: 2026-01 + input: + people: + grandmother: + age: 58 + is_tax_unit_dependent: true + unemployment_compensation: 6_000 + own_children_in_household: 1 + mother: + age: 38 + child: + age: 12 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandmother, mother, child] + spm_units: + spm_unit: + members: [grandmother, mother, child] + households: + household: + members: [grandmother, mother, child] + state_code: MO + output: + # Known limit of the default: own_children_in_household cannot say whose + # parent she is, and a 46-year gap is possible for a parent, so she is + # counted with her $500 (292.090 - 500 < 0). Callers who know she is the + # mother's mother set mo_tanf_is_parent_of_dependent_child to false, as in + # the case above. + mo_tanf_is_parent_of_dependent_child: [true, false, false] + mo_tanf_is_assistance_unit_member: [true, true, true] + mo_tanf: 0 + +- name: The default rules out a dependent too old to be the child's parent + period: 2026-01 + input: + people: + grandmother: + age: 64 + is_tax_unit_dependent: true + unemployment_compensation: 6_000 + own_children_in_household: 1 + mother: + age: 40 + child: + age: 10 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandmother, mother, child] + spm_units: + spm_unit: + members: [grandmother, mother, child] + households: + household: + members: [grandmother, mother, child] + state_code: MO + output: + # 64 - 10 = 54 years, more than the 50 the default allows between a + # parent and child, so her own child in the home must be someone else. + mo_tanf_is_parent_of_dependent_child: [false, false, false] + mo_tanf_is_assistance_unit_member: [false, true, true] + mo_tanf: 234.086 + +- name: The default rules out a dependent too young to be the child's parent + period: 2026-01 + input: + people: + parent: + age: 40 + sibling: + age: 19 + is_tax_unit_dependent: true + # Her baby lives elsewhere; the child here is her 9-year-old brother. + own_children_in_household: 1 + child: + age: 9 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [parent, sibling, child] + spm_units: + spm_unit: + members: [parent, sibling, child] + households: + household: + members: [parent, sibling, child] + state_code: MO + output: + # 19 - 9 = 10 years, fewer than the 12 the default requires. + mo_tanf_is_parent_of_dependent_child: [false, false, false] + mo_tanf_is_assistance_unit_member: [true, false, true] + mo_tanf: 234.086 + +- name: A dependent parent on SSI is not a member but still excludes the non-parent caretaker + period: 2026-01 + input: + people: + grandparent: + age: 55 + mo_tanf_is_non_parent_caretaker: true + mother: + age: 20 + is_tax_unit_dependent: true + receives_ssi: true + own_children_in_household: 1 + child: + age: 3 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandparent, mother, child] + spm_units: + spm_unit: + members: [grandparent, mother, child] + households: + household: + members: [grandparent, mother, child] + state_code: MO + output: + # 13 CSR 40-2.310(1)(F) excludes the SSI recipient; 0210.005.10 excludes + # "A legal guardian or NPCR if a biological or adoptive parent is in the + # home." + mo_tanf_non_parent_caretaker: [false, false, false] + mo_tanf_is_assistance_unit_member: [false, false, true] + mo_tanf_assistance_unit_size: 1 + mo_tanf: 135.687 + +- name: A grandparent marked as a non-parent stays out even when their own adult child is counted + period: 2026-01 + input: + people: + grandparent: + age: 45 + # The grandparent's own child in the household is the mother. + own_children_in_household: 1 + mo_tanf_is_non_parent_caretaker: true + mother: + age: 20 + is_tax_unit_dependent: true + own_children_in_household: 1 + child: + age: 3 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandparent, mother, child] + spm_units: + spm_unit: + members: [grandparent, mother, child] + households: + household: + members: [grandparent, mother, child] + state_code: MO + output: + # own_children_in_household cannot say whose parent the grandparent is, + # and at 45 she is young enough to be the child's parent, so the default + # marks her; the non-parent mark decides for a head or spouse. + mo_tanf_is_parent_of_dependent_child: [true, true, false] + mo_tanf_is_assistance_unit_member: [false, true, true] + mo_tanf_assistance_unit_size: 2 + mo_tanf: 234.086 + +- name: A dependent adult whose only child in the home is an adult is not a member + period: 2026-01 + input: + people: + head: + age: 70 + parent: + age: 45 + is_tax_unit_dependent: true + own_children_in_household: 1 + adult_child: + age: 20 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, parent, adult_child] + spm_units: + spm_unit: + members: [head, parent, adult_child] + households: + household: + members: [head, parent, adult_child] + state_code: MO + output: + # No one here is an eligible child (13 CSR 40-2.325(1)(A)), so there is + # no parent of an eligible child and no unit. + mo_tanf_dependent_child: [false, false, false] + mo_tanf_is_assistance_unit_member: [false, false, false] + mo_tanf: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/non_parent_caretaker/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/non_parent_caretaker/integration.yaml index a5ed11fa0fd..fc30561c8db 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/non_parent_caretaker/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/non_parent_caretaker/integration.yaml @@ -441,13 +441,14 @@ # DSS Manual 0210.005.10 excludes "A legal guardian or NPCR if a # biological or adoptive parent is in the home." The mother is an adult, # not a cash-eligible child, so the grandparent cannot be included. - # Known underpayment, not changed here: under 0210.005.05 the mother is - # the caretaker payee and belongs in the unit, but the existing rule - # leaves out adults claimed as tax dependents, so the unit is the child - # alone. + # Under 0210.005.05 the mother is the caretaker payee and a member even + # though the grandparent claims her as a tax dependent, so the unit is + # the mother and child (size 2), not the child alone (size 1, $135.687). mo_tanf_non_parent_caretaker: [false, false, false] mo_tanf_non_parent_caretaker_needy: false - mo_tanf_is_assistance_unit_member: [false, false, true] + mo_tanf_is_assistance_unit_member: [false, true, true] + mo_tanf_assistance_unit_size: 2 + mo_tanf: 234.086 - name: A mother who is herself a dependent child does not block her aunt as caretaker relative period: 2026-01 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/non_parent_caretaker/mo_tanf_non_parent_caretaker.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/non_parent_caretaker/mo_tanf_non_parent_caretaker.yaml index d13a33a219f..aeb93628404 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/non_parent_caretaker/mo_tanf_non_parent_caretaker.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/non_parent_caretaker/mo_tanf_non_parent_caretaker.yaml @@ -200,7 +200,7 @@ mo_tanf_assistance_unit_size: 2 mo_tanf: 234.086 -- name: Without the override the same count of own children excludes the grandparent +- name: Without the override the default rules out a great-grandparent too old to be the child's parent period: 2026-01 input: people: @@ -225,8 +225,13 @@ members: [grandparent, great_grandparent, grandchild] state_code: MO output: - # mo_tanf_is_parent_of_dependent_child defaults to is_parent, which - # cannot tell whose parent she is. - mo_tanf_is_parent_of_dependent_child: [false, true, false] - mo_tanf_non_parent_caretaker: [false, false, false] + # own_children_in_household cannot tell whose parent she is, but the + # default also requires being 12 to 50 years older than a dependent + # child, and 80 - 8 = 72. So no parent is in the home and the + # grandparent is the caretaker relative, as with the override above. + mo_tanf_is_parent_of_dependent_child: [false, false, false] + mo_tanf_non_parent_caretaker: [true, false, false] + mo_tanf_non_parent_caretaker_included: true + mo_tanf_assistance_unit_size: 2 + mo_tanf: 234.086 diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py index d9e3d58f619..4134290220e 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py @@ -7,6 +7,7 @@ class mo_tanf_is_assistance_unit_member(Variable): label = "Missouri TANF assistance unit member" definition_period = MONTH reference = ( + "https://my.mo.gov/cms_fsd?id=kb_article_view&sys_kb_id=98e1ef0c1b543650ba12657ae54bcbd1", "https://dssmanuals.mo.gov/temporary-assistance-case-management/0210-005-05/", "https://dssmanuals.mo.gov/temporary-assistance-case-management/0210-005-10/", "https://dssmanuals.mo.gov/temporary-assistance-case-management/0210-005-35/", @@ -49,11 +50,9 @@ def formula(person, period, parameters): # eligibility for the payee and/or second parent" — the SSI child # is excluded from the unit's needs but still establishes the case # for the caretaker. + has_dependent_child = person.tax_unit.any(dependent_child) caretaker = ( - head_or_spouse - & ~is_dependent - & ~is_ssi_recipient - & person.tax_unit.any(dependent_child) + head_or_spouse & ~is_dependent & ~is_ssi_recipient & has_dependent_child ) # A parent is a mandatory member (DSS Manual 0210.005.05). A # non-parent caretaker relative or legal guardian is an optional @@ -62,6 +61,25 @@ def formula(person, period, parameters): # (13 CSR 40-2.300(5)(D); DSS Manual 0210.005.15 and 0210.005.35). non_parent = person("mo_tanf_is_non_parent_caretaker", period.this_year) parent_caretaker = caretaker & ~non_parent + # Membership as a parent does not depend on who claims whom for + # taxes. 13 CSR 40-2.300(5)(C) and the Combined IM Policy Manual + # 4.2.2 (formerly DSS Manual 0210.005.05) make the "Biological or + # adoptive parents of one or more of the eligible children" + # mandatory members, so a parent claimed as someone else's tax + # dependent, such as a 20-year-old mother claimed by her own mother, + # is a member too. These are the same people + # mo_tanf_non_parent_caretaker treats as a parent in the home, less + # those on SSI. A parent who is a dependent child is a member as an + # eligible child instead, with their own parent as the caretaker: + # the manual's minor parent provision (4.2.4; formerly 0210.005.30) + # lets that three-generation family file as one assistance group. + other_parent = ( + person("mo_tanf_is_parent_of_dependent_child", period.this_year) + & ~non_parent + & ~dependent_child + & ~is_ssi_recipient + & has_dependent_child + ) npcr = person("mo_tanf_non_parent_caretaker", period) npcr_included = person.spm_unit("mo_tanf_non_parent_caretaker_included", period) - return eligible_child | parent_caretaker | (npcr & npcr_included) + return eligible_child | parent_caretaker | other_parent | (npcr & npcr_included) diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py index 4b913f26818..32b4104ba9a 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py @@ -7,22 +7,46 @@ class mo_tanf_is_parent_of_dependent_child(Variable): label = "Missouri TANF parent of a dependent child in the tax unit" documentation = ( "Whether this person is the biological or adoptive parent of one of " - "the tax unit's dependent children. It matters only when someone in " - "the tax unit is marked as a non-parent caretaker: a parent in the " - "home, other than a cash-eligible child, excludes that caretaker. " - "Defaults to having one's own children in the household " - "(own_children_in_household), which also counts adult children and " - "children outside the tax unit; set this input directly when that " - "count does not match. Heads and spouses not marked as non-parent " - "caretakers are always treated as parents; this input applies to " - "other tax-unit members." + "the tax unit's dependent children. Such a parent is a member of the " + "assistance unit even when someone else claims them as a tax " + "dependent, unless they receive SSI or are a dependent child " + "themselves (then they are a member as a child). A parent in the " + "home, other than a cash-eligible child, also excludes a non-parent " + "caretaker. Defaults to having one's own children in the household " + "(own_children_in_household) and being 12 to 50 years older than one " + "of the tax unit's dependent children. The count also includes adult " + "children and children outside the tax unit, so set this input " + "directly when it does not match: false, for example, for a " + "dependent whose own child in the home is an adult. Setting it for " + "one person sets it to false for everyone else not given a value, so " + "set it for every parent it applies to. Heads and spouses not marked " + "as non-parent caretakers are always treated as parents; this input " + "applies to other tax-unit members." ) definition_period = YEAR reference = ( + "https://my.mo.gov/cms_fsd?id=kb_article_view&sys_kb_id=98e1ef0c1b543650ba12657ae54bcbd1", "https://dssmanuals.mo.gov/temporary-assistance-case-management/0210-005-05/", "https://dssmanuals.mo.gov/temporary-assistance-case-management/0210-005-10/", ) defined_for = StateCode.MO def formula(person, period, parameters): - return person("is_parent", period) + # own_children_in_household counts own children of any age, so on + # its own it also marks, for example, the head's elderly mother, + # whose own child in the home is the head. Also require the person + # to be 12 to 50 years older than at least one of the tax unit's + # dependent children: a parent is at least 12 at a child's birth, + # and births after 50 are rare. Dependent children are all under 19, + # so their ages span less than the 38-year window, and "some child + # is 12 to 50 years younger" reduces to the youngest being at least + # 12 years younger and the oldest at most 50. In the CPS-based + # default dataset this keeps every weighted parent of a dependent + # child in the unit and drops the weighted non-parents the count + # alone marks. + has_own_children = person("is_parent", period) + age = person("age", period) + dependent_child = person("mo_tanf_dependent_child", period.first_month) + youngest = person.tax_unit.min(where(dependent_child, age, np.inf)) + oldest = person.tax_unit.max(where(dependent_child, age, -np.inf)) + return has_own_children & (age - youngest >= 12) & (age - oldest <= 50) diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/income/is_mo_tanf_earned_income_exempt.py b/policyengine_us/variables/gov/states/mo/dss/tanf/income/is_mo_tanf_earned_income_exempt.py index 78a99b6ab67..4ec38613809 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/income/is_mo_tanf_earned_income_exempt.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/income/is_mo_tanf_earned_income_exempt.py @@ -30,7 +30,9 @@ def formula(person, period, parameters): head_or_spouse = person("is_tax_unit_head_or_spouse", period.this_year) is_dependent = person("is_tax_unit_dependent", period.this_year) # The exemption is for a parent; a non-parent caretaker relative or - # legal guardian does not qualify. + # legal guardian does not qualify. A parent claimed as someone else's + # tax dependent needs no separate test: under 19 and in secondary + # school, they are a dependent child, exempt above. non_parent = person("mo_tanf_is_non_parent_caretaker", period.this_year) parent = ( head_or_spouse From 41c2c2c41c99d808d775c750e408bc099429da9e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 10:07:37 -0400 Subject: [PATCH 02/10] Test the dependent-child guard and caretaker identification; correct scope wording Review round on #9676 (findings 1, 5, 6, 7, 9-12): - Replace the vacuous adult-only-child YAML case with a two-tax-unit household where an explicit parent flag must not add someone whose own tax unit has no dependent child (fails if the guard is dropped). - Add the mandatory dependent parent's income case (her $1,200 makes the unit of four ineligible; main pays the three children $292.08996) and its zero-income pair ($341.8074). - Property suite: restate non-parent caretaker identification person by person, including households where one must be found, and an explicit-flag guard grid; say where expectations still read model outputs and that the flag-off comparison does not run pre-change code. - Label the 18-year-old high-school parent's one-unit grouping as the retained interpretation (i), not settled law. - Parent flag docs: January snapshot, adoptive parents outside the 12-50 window need the input, explicit input zeroes others only for that year; drop the dataset-specific accuracy claim from the formula comment. Co-Authored-By: Claude Opus 5.5 --- ...est_mo_tanf_dependent_parent_properties.py | 146 +++++++++++++++--- .../mo_tanf_is_assistance_unit_member.yaml | 132 +++++++++++++--- .../mo_tanf_is_assistance_unit_member.py | 15 +- .../mo_tanf_is_parent_of_dependent_child.py | 34 ++-- 4 files changed, 268 insertions(+), 59 deletions(-) diff --git a/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py b/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py index 4d62542a7be..e7db0dcee9b 100644 --- a/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py +++ b/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py @@ -4,32 +4,45 @@ adoptive parents of one or more of the eligible children" in the assistance unit, whoever claims them for taxes. SSI recipients are excluded (13 CSR 40-2.310(1)(F)). A parent who is also a cash-eligible child is a member as a -child, and DSS Manual 0210.005.30 lets that three-generation family file as -one group. A non-parent caretaker relative is excluded while a parent is in -the home (0210.005.10). +child. For a minor parent (the grid's 16-year-old), DSS Manual 0210.005.30 +lets that three-generation family file as one group. For an 18-year-old +parent in secondary school the sources leave the grouping open; the grid +checks the model's retained interpretation (i), one combined unit. A +non-parent caretaker relative is excluded while a parent is in the home +(0210.005.10). Every combination in a grid of households shares one simulation and is -checked against those rules, restated here independently of the formulas: +checked against those rules as restated here. Where an expectation still +reads a model output, the item says so: 1. Membership: children not on SSI; unmarked heads and spouses not on SSI; a tax-dependent adult when they are a dependent child, or when they are a - parent of the children and not on SSI; a marked head or spouse only as an - included non-parent caretaker, which needs no parent in the home. -2. A dependent parent who is a member always excludes the non-parent - caretaker. -3. Marking the dependent adult as a parent adds exactly that adult to the - unit and removes, at most, an included non-parent caretaker. + parent of the children and not on SSI; a marked head or spouse only as the + non-parent caretaker identified in item 2, when the model includes them. + Inclusion (neediness and the grant comparison) is the model's own + mo_tanf_non_parent_caretaker_included, not restated here. +2. Non-parent caretaker identification, person by person: with no parent in + the home, the marked head, or the marked spouse when the head is on SSI; + with a parent in the home, nobody. A dependent parent who is a member + always excludes the non-parent caretaker. +3. Marking the dependent adult as a parent, against the same households with + the adult marked not a parent, adds exactly that adult to the unit and + removes, at most, an included non-parent caretaker. Both sides run the + current code; the comparison does not execute the code before this rule. 4. Unit income is the sum of members' income, less the student exemption. 5. With no income anywhere, the grant is the payment standard for the unit - size, and marking a parent never lowers it, unless the parent receives - SSI: then they stay out of the unit but still exclude the non-parent - caretaker (intended). + size, and marking a parent never lowers it (against the model's grant + with the adult marked not a parent), unless the parent receives SSI: then + they stay out of the unit but still exclude the non-parent caretaker + (intended). 6. The default input (own children in the household) gives the same answer as marking the parent directly, and each household gets the same answer alone as in the shared simulation. 7. The default marks a person with own children in the household only when some dependent child in the tax unit is 12 to 50 years younger, checked pair by pair against the formula's youngest-and-oldest shortcut. +8. An explicit parent flag adds a person only when their own tax unit has a + dependent child. """ import itertools @@ -217,6 +230,22 @@ def _parent_in_home(h, kind=None): return unmarked_caretaker or adult_parent +def _expected_non_parent_caretaker(h, role): + """The non-parent caretaker considered for the unit, restated. + + 13 CSR 40-2.300(5)(D) admits one only "if there are no natural or + adoptive parents in the home"; DSS Manual 0210.005.35 bars one on SSI. + One per couple: the head, or the spouse when the head is on SSI. Only + the head and spouse are marked, and the spouse is never on SSI here. + """ + eligible = h["marked"] and not _parent_in_home(h) + if role == "head": + return eligible and not h["head_ssi"] + if role == "spouse": + return eligible and h["head_ssi"] + return False + + def _expected_member(h, role, kind=None): """Restated membership, or None where the non-parent caretaker rules decide.""" kind = kind or h["adult"] @@ -250,21 +279,48 @@ def non_parent_sim(): def test_membership_matches_the_restated_rule(sim): member = _calc(sim, "mo_tanf_is_assistance_unit_member") - npcr = _calc(sim, "mo_tanf_non_parent_caretaker") + # Inclusion of an identified caretaker is the model's own decision. included = _calc(sim, "mo_tanf_non_parent_caretaker_included") for k, (i, role) in enumerate(PEOPLE): h = HOUSEHOLDS[i] expected = _expected_member(h, role) if expected is None: - # A marked head or spouse is a member only as an included - # non-parent caretaker, which requires no parent in the home. - assert member[k] == (npcr[k] and included[i]), (h, role) - if member[k]: - assert not _parent_in_home(h), (h, role) + # A marked head or spouse is a member only as the identified + # non-parent caretaker, and only when included. + npcr = _expected_non_parent_caretaker(h, role) + assert member[k] == (npcr and included[i]), (h, role) else: assert member[k] == expected, (h, role) +def test_non_parent_caretaker_identification_matches_independent_rule(sim): + npcr = _calc(sim, "mo_tanf_non_parent_caretaker") + expected = np.array( + [_expected_non_parent_caretaker(HOUSEHOLDS[i], role) for i, role in PEOPLE] + ) + mismatches = [ + (HOUSEHOLDS[i], role, bool(npcr[k])) + for k, (i, role) in enumerate(PEOPLE) + if npcr[k] != expected[k] + ] + assert not mismatches, mismatches[:5] + has_npcr = np.zeros(N, dtype=bool) + for k, (i, role) in enumerate(PEOPLE): + has_npcr[i] |= npcr[k] + expected_has_npcr = np.array( + [ + h["marked"] + and not _parent_in_home(h) + and (not h["head_ssi"] or h["married"]) + for h in HOUSEHOLDS + ] + ) + assert has_npcr.tolist() == expected_has_npcr.tolist() + # The grid has households where a caretaker must be found and where none + # may be, so neither "never" nor "always" passes. + assert expected_has_npcr.any() and not expected_has_npcr.all() + + def test_a_dependent_parent_member_excludes_the_non_parent_caretaker(sim): member = _calc(sim, "mo_tanf_is_assistance_unit_member") npcr = _calc(sim, "mo_tanf_non_parent_caretaker") @@ -276,8 +332,8 @@ def test_a_dependent_parent_member_excludes_the_non_parent_caretaker(sim): parent_member[i] |= member[k] assert parent_member.any() assert not (parent_member & has_npcr).any() - # The non-parent caretaker rules see a parent in the home exactly when - # the restated rule does. + # A parent in the home rules out a caretaker; the converse, that one is + # found when no parent is home, is in the identification test above. for i, h in enumerate(HOUSEHOLDS): if _parent_in_home(h): assert not has_npcr[i], h @@ -294,8 +350,9 @@ def test_marking_a_parent_adds_only_that_parent(sim, non_parent_sim): # Only an included non-parent caretaker can leave, displaced by # the parent now seen in the home. assert off_npcr[k], (HOUSEHOLDS[i], role) - # Before this rule, a dependent adult who is not a dependent child was - # never a member. + # With the adult marked not a parent, a dependent adult who is not a + # dependent child is never a member. This runs the current code with + # that input, not the code before parents claimed as dependents counted. for k, (i, role) in enumerate(PEOPLE): if role == "adult" and not _adult_is_dependent_child(HOUSEHOLDS[i]): assert not off[k] @@ -380,6 +437,49 @@ def test_each_household_alone_matches_the_shared_simulation(sim, i): assert _calc(alone, variable)[0] == pytest.approx(_calc(sim, variable)[i]) +def test_parent_flag_needs_a_dependent_child_in_the_persons_tax_unit(): + # Each household: a mother (30) heading a tax unit with her 5-year-old, + # and an adult explicitly marked a parent who either files alone (no + # dependent child in their tax unit) or is claimed in the mother's unit. + cases = list(itertools.product((18, 25, 40, 60), (False, True))) + people, tax_units, spm_units, units = {}, {}, {}, {} + for i, (age, files_alone) in enumerate(cases): + mother, child, adult = (f"g{i}_{r}" for r in ("mother", "child", "adult")) + people[mother] = {"age": {YEAR: 30}, "is_tax_unit_dependent": {YEAR: False}} + people[child] = {"age": {YEAR: 5}, "is_tax_unit_dependent": {YEAR: True}} + people[adult] = { + "age": {YEAR: age}, + "is_tax_unit_dependent": {YEAR: not files_alone}, + "mo_tanf_is_parent_of_dependent_child": {YEAR: True}, + } + if files_alone: + tax_units[f"g{i}_family"] = {"members": [mother, child]} + tax_units[f"g{i}_adult"] = {"members": [adult]} + else: + tax_units[f"g{i}_family"] = {"members": [mother, child, adult]} + spm_units[f"g{i}_spm"] = {"members": [mother, child, adult]} + units[f"g{i}_hh"] = { + "members": [mother, child, adult], + "state_code": {YEAR: "MO"}, + } + simulation = Simulation( + situation={ + "people": people, + "tax_units": tax_units, + "spm_units": spm_units, + "households": units, + } + ) + member = _calc(simulation, "mo_tanf_is_assistance_unit_member") + size = _calc(simulation, "mo_tanf_assistance_unit_size") + grant = _calc(simulation, "mo_tanf") + for i, (age, files_alone) in enumerate(cases): + expected = [True, True, not files_alone] + assert member[3 * i : 3 * i + 3].tolist() == expected, (age, files_alone) + assert size[i] == sum(expected), (age, files_alone) + assert grant[i] == pytest.approx(_payment(sum(expected)), abs=1e-3) + + def test_default_parent_input_matches_the_pairwise_age_rule(): # The default is own_children_in_household > 0 and being 12 to 50 years # older than some dependent child in the tax unit. The formula checks only diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml index 3ac066c9550..a6305cf261e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml @@ -107,7 +107,7 @@ mo_tanf_assistance_unit_size: 2 mo_tanf: 234.086 -- name: An 18-year-old mother in high school is a cash-eligible child with her own mother as caretaker +- name: An 18-year-old mother in high school is a cash-eligible child, grouped with her own mother under one reading period: 2026-01 input: people: @@ -135,8 +135,13 @@ output: # She is a dependent child (under 19 and in secondary school), so she is a # member as an eligible child and her student earnings are exempt - # (13 CSR 40-2.310(9)(A)1). DSS Manual 0210.005.30 lets the family file as - # one three-generation assistance group with her mother as caretaker. + # (13 CSR 40-2.310(9)(A)1). She is not a minor parent: archived DSS Manual + # 0210.005.30 and current Combined Manual 4.3 define one as "a parent under + # the age of 18, including the month turning 18". The sources do not + # settle her grouping. This case records the model's retained + # interpretation (i), one combined three-generation unit with her mother + # (size 3). The other readings give a unit of two ($234.08628 at zero + # income). mo_tanf_dependent_child: [false, true, true] mo_tanf_is_assistance_unit_member: [true, true, true] mo_tanf_gross_earned_income: 0 @@ -441,32 +446,123 @@ mo_tanf_assistance_unit_size: 2 mo_tanf: 234.086 -- name: A dependent adult whose only child in the home is an adult is not a member +- name: A mandatory dependent parent's income can eliminate the children's grant period: 2026-01 input: people: - head: - age: 70 - parent: - age: 45 + grandparent: + age: 55 + mo_tanf_is_non_parent_caretaker: true + mother: + age: 31 is_tax_unit_dependent: true - own_children_in_household: 1 - adult_child: - age: 20 + own_children_in_household: 3 + disability_benefits: 14_400 # $1,200/month + child1: + age: 10 + is_tax_unit_dependent: true + child2: + age: 8 + is_tax_unit_dependent: true + child3: + age: 6 is_tax_unit_dependent: true tax_units: tax_unit: - members: [head, parent, adult_child] + members: [grandparent, mother, child1, child2, child3] spm_units: spm_unit: - members: [head, parent, adult_child] + members: [grandparent, mother, child1, child2, child3] households: household: - members: [head, parent, adult_child] + members: [grandparent, mother, child1, child2, child3] state_code: MO output: - # No one here is an eligible child (13 CSR 40-2.325(1)(A)), so there is - # no parent of an eligible child and no unit. - mo_tanf_dependent_child: [false, false, false] - mo_tanf_is_assistance_unit_member: [false, false, false] + # The mother is a mandatory member (13 CSR 40-2.300(5)(C)), so her + # $14,400 / 12 = $1,200 counts. It exceeds the $990 standard of need for + # four, so the grant is 0. Without her, the three children alone would + # get 846 * 0.34526 = $292.08996. + mo_tanf_is_assistance_unit_member: [false, true, true, true, true] + mo_tanf_assistance_unit_size: 4 + mo_tanf_gross_unearned_income: 1_200 + mo_tanf_countable_income: 1_200 + mo_tanf_standard_of_need: 990 + mo_tanf_standard_of_need_test: false mo_tanf: 0 + +- name: The same household with no income gets the payment standard for four + period: 2026-01 + input: + people: + grandparent: + age: 55 + mo_tanf_is_non_parent_caretaker: true + mother: + age: 31 + is_tax_unit_dependent: true + own_children_in_household: 3 + disability_benefits: 0 + child1: + age: 10 + is_tax_unit_dependent: true + child2: + age: 8 + is_tax_unit_dependent: true + child3: + age: 6 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [grandparent, mother, child1, child2, child3] + spm_units: + spm_unit: + members: [grandparent, mother, child1, child2, child3] + households: + household: + members: [grandparent, mother, child1, child2, child3] + state_code: MO + output: + # 990 * 0.34526 = 341.8074, $49.71744 more than the three children's + # $292.08996 without their mother. + mo_tanf_is_assistance_unit_member: [false, true, true, true, true] + mo_tanf_assistance_unit_size: 4 + mo_tanf_gross_earned_income: 0 + mo_tanf_gross_unearned_income: 0 + mo_tanf_countable_income: 0 + mo_tanf_standard_of_need_test: true + mo_tanf: 341.8074 + +- name: An explicit parent flag requires a dependent child in that person's tax unit + period: 2026-01 + input: + people: + mother: + age: 30 + is_tax_unit_dependent: false + child: + age: 5 + is_tax_unit_dependent: true + adult: + age: 25 + is_tax_unit_dependent: false + mo_tanf_is_parent_of_dependent_child: true + tax_units: + family: + members: [mother, child] + other: + members: [adult] + spm_units: + spm_unit: + members: [mother, child, adult] + households: + household: + members: [mother, child, adult] + state_code: MO + output: + # The adult files alone, so their tax unit has no dependent child and the + # flag does not make them a member. Two members: 678 * 0.34526 = + # 234.08628. Without the dependent-child guard the adult would join, for + # size 3 and 846 * 0.34526 = 292.08996. + mo_tanf_is_assistance_unit_member: [true, true, false] + mo_tanf_assistance_unit_size: 2 + mo_tanf: 234.08628 diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py index 4134290220e..4b2f9a43a76 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py @@ -69,10 +69,17 @@ def formula(person, period, parameters): # dependent, such as a 20-year-old mother claimed by her own mother, # is a member too. These are the same people # mo_tanf_non_parent_caretaker treats as a parent in the home, less - # those on SSI. A parent who is a dependent child is a member as an - # eligible child instead, with their own parent as the caretaker: - # the manual's minor parent provision (4.2.4; formerly 0210.005.30) - # lets that three-generation family file as one assistance group. + # those on SSI. The parent's own tax unit must have a dependent + # child, so an explicit true input alone does not add someone whose + # tax unit has none. + # A parent who is a dependent child is a member as an eligible child + # instead, with their own parent as the caretaker. For a minor + # parent (under 18, including the month of turning 18), the + # manual's minor parent provision (4.2.4; formerly 0210.005.30) lets + # that three-generation family file as one assistance group. For an + # 18-year-old parent in secondary school the sources do not settle + # the grouping; keeping her in one combined unit with her own parent + # is one of several readings, not settled law. other_parent = ( person("mo_tanf_is_parent_of_dependent_child", period.this_year) & ~non_parent diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py index 32b4104ba9a..203ffde0f1b 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py @@ -14,13 +14,16 @@ class mo_tanf_is_parent_of_dependent_child(Variable): "home, other than a cash-eligible child, also excludes a non-parent " "caretaker. Defaults to having one's own children in the household " "(own_children_in_household) and being 12 to 50 years older than one " - "of the tax unit's dependent children. The count also includes adult " - "children and children outside the tax unit, so set this input " - "directly when it does not match: false, for example, for a " - "dependent whose own child in the home is an adult. Setting it for " - "one person sets it to false for everyone else not given a value, so " - "set it for every parent it applies to. Heads and spouses not marked " - "as non-parent caretakers are always treated as parents; this input " + "of the tax unit's dependent children, as of January of the year. " + "The age window is an imputation rule, not law. The count also " + "includes adult children and children outside the tax unit, so set " + "this input directly when it does not match: false, for example, for " + "a dependent whose own child in the home is an adult, and true for an " + "adoptive parent outside the 12-to-50-year window. Setting it for one " + "person for a year sets it to false for everyone else not given a " + "value for that year, so set it for every parent it applies to; other " + "years still use the default. Heads and spouses not marked as " + "non-parent caretakers are always treated as parents; this input " "applies to other tax-unit members." ) definition_period = YEAR @@ -37,13 +40,16 @@ def formula(person, period, parameters): # whose own child in the home is the head. Also require the person # to be 12 to 50 years older than at least one of the tax unit's # dependent children: a parent is at least 12 at a child's birth, - # and births after 50 are rare. Dependent children are all under 19, - # so their ages span less than the 38-year window, and "some child - # is 12 to 50 years younger" reduces to the youngest being at least - # 12 years younger and the oldest at most 50. In the CPS-based - # default dataset this keeps every weighted parent of a dependent - # child in the unit and drops the weighted non-parents the count - # alone marks. + # and births after 50 are rare. This window is an imputation rule, + # not law: the rule covers adoptive parents of any age, so a genuine + # adoptive parent outside the window needs this input set to true. + # Dependent children are all under 19, so their ages span less than + # the 38-year window, and "some child is 12 to 50 years younger" + # reduces to the youngest being at least 12 years younger and the + # oldest at most 50. + # The flag is annual and reads January's dependent children, so a + # change in who is a dependent child later in the year (for example + # a child aging out through monthly_age inputs) does not update it. has_own_children = person("is_parent", period) age = person("age", period) dependent_child = person("mo_tanf_dependent_child", period.first_month) From aec34394e4fb69aa4411ab700ba783bfde4e97df Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 13:44:35 -0400 Subject: [PATCH 03/10] Quote each minor-parent definition from its own manual; note the age window in the default-input test Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_mo_tanf_dependent_parent_properties.py | 7 +++++-- .../mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml | 7 ++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py b/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py index e7db0dcee9b..630038dd7d3 100644 --- a/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py +++ b/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py @@ -406,8 +406,11 @@ def test_default_input_matches_marking_the_parent(): marked_sim = Simulation(situation=_situation(marked)) flag = default_sim.calculate("mo_tanf_is_parent_of_dependent_child", YEAR) roles = [role for h in own_children for role in _roles(h)] - # The default is own_children_in_household > 0: the head, the dependent - # adult and the 16-year-old mother, nobody else. + # The default is own_children_in_household > 0 and being 12 to 50 years + # older than some dependent child in the tax unit. Here the head (55), + # the dependent adult (18 or 22) and the 16-year-old mother all have + # their own children in the home and pass the age window; nobody else + # has own children. assert flag.tolist() == [ role in ("head", "adult", "minor_parent") for role in roles ] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml index a6305cf261e..bb8702027ac 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml @@ -136,9 +136,10 @@ # She is a dependent child (under 19 and in secondary school), so she is a # member as an eligible child and her student earnings are exempt # (13 CSR 40-2.310(9)(A)1). She is not a minor parent: archived DSS Manual - # 0210.005.30 and current Combined Manual 4.3 define one as "a parent under - # the age of 18, including the month turning 18". The sources do not - # settle her grouping. This case records the model's retained + # 0210.005.30 defines one as "a parent under the age of 18, including the + # month turning 18", and current Combined Manual 4.3 as a parent "under + # the age of 18, including the month of their 18th birthday". The sources + # do not settle her grouping. This case records the model's retained # interpretation (i), one combined three-generation unit with her mother # (size 3). The other readings give a unit of two ($234.08628 at zero # income). From 7f16e87e8d2b73cc6276e1bc4c8924a826ae8c7b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 20:17:28 -0400 Subject: [PATCH 04/10] Label retained Missouri parent grouping and age imputation precisely --- .../tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py | 2 +- .../mo_tanf_is_parent_of_dependent_child.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py index 4b2f9a43a76..3a032d14f64 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py @@ -79,7 +79,7 @@ def formula(person, period, parameters): # that three-generation family file as one assistance group. For an # 18-year-old parent in secondary school the sources do not settle # the grouping; keeping her in one combined unit with her own parent - # is one of several readings, not settled law. + # is retained interpretation (i), one of several readings. other_parent = ( person("mo_tanf_is_parent_of_dependent_child", period.this_year) & ~non_parent diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py index 203ffde0f1b..0765032e790 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py @@ -39,8 +39,8 @@ def formula(person, period, parameters): # its own it also marks, for example, the head's elderly mother, # whose own child in the home is the head. Also require the person # to be 12 to 50 years older than at least one of the tax unit's - # dependent children: a parent is at least 12 at a child's birth, - # and births after 50 are rare. This window is an imputation rule, + # dependent children. The bounds approximate typical birth ages; + # they do not establish parenthood. This window is an imputation rule, # not law: the rule covers adoptive parents of any age, so a genuine # adoptive parent outside the window needs this input set to true. # Dependent children are all under 19, so their ages span less than From b9e7fff948caf4f33313c942c09caa7c7ccc3dea Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 20:18:25 -0400 Subject: [PATCH 05/10] Clarify birthday-month scope in the retained parent grouping example --- .../states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml index bb8702027ac..f19289333c0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml @@ -135,7 +135,8 @@ output: # She is a dependent child (under 19 and in secondary school), so she is a # member as an eligible child and her student earnings are exempt - # (13 CSR 40-2.310(9)(A)1). She is not a minor parent: archived DSS Manual + # (13 CSR 40-2.310(9)(A)1). School enrollment alone does not make her a + # minor parent: archived DSS Manual # 0210.005.30 defines one as "a parent under the age of 18, including the # month turning 18", and current Combined Manual 4.3 as a parent "under # the age of 18, including the month of their 18th birthday". The sources From 02cc4487468509323e64cb42de7f85101b4fd48a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 07:02:54 -0400 Subject: [PATCH 06/10] Test and document Missouri TANF parent input limits --- ...mo_tanf_non_parent_caretaker_properties.py | 2 +- .../mo_tanf_is_assistance_unit_member.yaml | 40 +++++++ .../mo_tanf_is_parent_of_dependent_child.yaml | 109 ++++++++++++++++++ .../mo_tanf_is_assistance_unit_member.py | 9 +- .../mo_tanf_is_parent_of_dependent_child.py | 7 +- 5 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_parent_of_dependent_child.yaml diff --git a/policyengine_us/tests/core/test_mo_tanf_non_parent_caretaker_properties.py b/policyengine_us/tests/core/test_mo_tanf_non_parent_caretaker_properties.py index a853ddc52bf..dc5ce09318e 100644 --- a/policyengine_us/tests/core/test_mo_tanf_non_parent_caretaker_properties.py +++ b/policyengine_us/tests/core/test_mo_tanf_non_parent_caretaker_properties.py @@ -182,7 +182,7 @@ def _payment(size): return STANDARD_OF_NEED.get(size, 0) * PAYMENT_PERCENTAGE -def test_unflagged_households_follow_the_pre_npcr_rule(unflagged_sim): +def test_unflagged_households_include_dependent_parents(unflagged_sim): member = _calc(unflagged_sim, "mo_tanf_is_assistance_unit_member") assert not _calc(unflagged_sim, "mo_tanf_non_parent_caretaker").any() assert not _calc(unflagged_sim, "mo_tanf_non_parent_caretaker_included").any() diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml index f19289333c0..c723459a5e5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_assistance_unit_member.yaml @@ -568,3 +568,43 @@ mo_tanf_is_assistance_unit_member: [true, true, false] mo_tanf_assistance_unit_size: 2 mo_tanf: 234.08628 + +- name: A real second parent filing separately stays excluded despite an explicit true parent flag + period: 2026-01 + input: + people: + mother: + age: 30 + is_tax_unit_dependent: false + child: + age: 5 + is_tax_unit_dependent: true + father: + age: 35 + is_tax_unit_dependent: false + own_children_in_household: 1 + mo_tanf_is_parent_of_dependent_child: true + tax_units: + mother_and_child: + members: [mother, child] + father_return: + members: [father] + spm_units: + spm_unit: + members: [mother, child, father] + households: + household: + members: [mother, child, father] + state_code: MO + output: + # The father is the child's biological parent and legally mandatory + # under 13 CSR 40-2.300(5)(C), but child grouping uses each person's own + # tax unit. The explicit flag remains true without attaching the child + # from the mother's return: this unmodeled cross-tax-unit relationship + # leaves size 2 and 678 * 0.34526 = 234.08628 rather than size 3. + # Annual explicit input also sets unspecified people's parent flags to + # false; the mother still qualifies as the head with a dependent child. + mo_tanf_is_parent_of_dependent_child: [false, false, true] + mo_tanf_is_assistance_unit_member: [true, true, false] + mo_tanf_assistance_unit_size: 2 + mo_tanf: 234.08628 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_parent_of_dependent_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_parent_of_dependent_child.yaml new file mode 100644 index 00000000000..14062d4b981 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_parent_of_dependent_child.yaml @@ -0,0 +1,109 @@ +- name: An explicit annual parent flag leaves other years using the default + period: 2026 + input: + people: + head: + age: + 2025: 45 + 2026: 45 + 2027: 45 + own_children_in_household: + 2025: 1 + 2026: 1 + 2027: 1 + mother: + age: + 2025: 25 + 2026: 25 + 2027: 25 + is_tax_unit_dependent: + 2025: true + 2026: true + 2027: true + own_children_in_household: + 2025: 1 + 2026: 1 + 2027: 1 + mo_tanf_is_parent_of_dependent_child: + 2026: false + child: + age: + 2025: 5 + 2026: 5 + 2027: 5 + is_tax_unit_dependent: + 2025: true + 2026: true + 2027: true + tax_units: + tax_unit: + members: [head, mother, child] + spm_units: + spm_unit: + members: [head, mother, child] + households: + household: + members: [head, mother, child] + state_code: + 2025: MO + 2026: MO + 2027: MO + output: + mo_tanf_is_parent_of_dependent_child: + 2025: [true, true, false] + # Providing one person's flag fills unset people with false only for + # that year; it does not suppress their formulas in other years. + 2026: [false, false, false] + 2027: [true, true, false] + +- name: January's parent snapshot remains in use after a child ages out + period: 2026-01 + input: + people: + head: + age: 40 + monthly_age: + 2026-01: 40 + 2026-07: 40 + dependent_adult: + age: 60 + monthly_age: + 2026-01: 60 + 2026-07: 60 + is_tax_unit_dependent: true + own_children_in_household: 1 + older_child: + age: 17 + monthly_age: + 2026-01: 17 + 2026-07: 18 + is_tax_unit_dependent: true + is_in_secondary_school: false + younger_child: + age: 0 + monthly_age: + 2026-01: 0 + 2026-07: 0 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, dependent_adult, older_child, younger_child] + spm_units: + spm_unit: + members: [head, dependent_adult, older_child, younger_child] + households: + household: + members: [head, dependent_adult, older_child, younger_child] + state_code: MO + output: + mo_tanf_dependent_child: + 2026-01: [false, false, true, true] + 2026-07: [false, false, false, true] + mo_tanf_is_parent_of_dependent_child: + 2026: [false, true, false, false] + mo_tanf_is_assistance_unit_member: + 2026-01: [true, true, true, true] + # January's 43-year gap still establishes the adult's parent flag. + # Recomputing against July's remaining infant would fail the window + # because the age gap is 60, but the annual snapshot stays in force. + 2026-07: [true, true, false, true] diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py index 3a032d14f64..5902986762a 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py @@ -69,9 +69,12 @@ def formula(person, period, parameters): # dependent, such as a 20-year-old mother claimed by her own mother, # is a member too. These are the same people # mo_tanf_non_parent_caretaker treats as a parent in the home, less - # those on SSI. The parent's own tax unit must have a dependent - # child, so an explicit true input alone does not add someone whose - # tax unit has none. + # those on SSI. The formulas group children within tax units; the + # boolean parent input cannot attach a child in another tax unit. + # The parent's own tax unit must therefore have a dependent child, + # so even an explicit true excludes a real parent filing separately + # when their own tax unit has none. Mandatory membership for that + # parent under 13 CSR 40-2.300(5)(C) remains an unmodeled limitation. # A parent who is a dependent child is a member as an eligible child # instead, with their own parent as the caretaker. For a minor # parent (under 18, including the month of turning 18), the diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py index 0765032e790..a5ed547185a 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py @@ -24,7 +24,12 @@ class mo_tanf_is_parent_of_dependent_child(Variable): "value for that year, so set it for every parent it applies to; other " "years still use the default. Heads and spouses not marked as " "non-parent caretakers are always treated as parents; this input " - "applies to other tax-unit members." + "applies to other tax-unit members. This input cannot associate a " + "parent filing a separate tax return with a child in another tax " + "unit: even an explicit true leaves that parent excluded when their " + "own tax unit has no dependent child. The assistance-unit formulas " + "use tax-unit child grouping, so mandatory parent membership across " + "tax units is not modeled." ) definition_period = YEAR reference = ( From b18832c6d54f605d7417b5c13771e7ddebd5917d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 07:04:22 -0400 Subject: [PATCH 07/10] Record Missouri TANF review follow-up and validation --- 9676-r2-report.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 9676-r2-report.md diff --git a/9676-r2-report.md b/9676-r2-report.md new file mode 100644 index 00000000000..6ae023f3b28 --- /dev/null +++ b/9676-r2-report.md @@ -0,0 +1,15 @@ +Addressed all four review r1 findings for [PolicyEngine US #9676](https://github.com/PolicyEngine/policyengine-us/pull/9676), starting from `b9e7fff948`. + +- Updated the PR body with both regression directions, including the grandmother aged 58 with $500/month UC: $234.08628 → $0. Saved paired entitlement records gaining/losing are 0/0 unmodified, 0/1 with grandparents marked, and 2/1 with marks plus bank assets zeroed, in both 2025 and 2026. Actual TANF counts remain 0/0. +- Disclosed the review's three Missouri NPCR changes: the great-grandparent case, integration underpayment case, and adult-parent property expectation. The 623 partner cases retain unchanged expectations. Renamed the NPCR property test to describe current behavior. +- Documented why an explicit parent flag cannot attach a child across tax units. A real parent filing separately remains excluded when their own unit has no dependent child; this legally mandatory membership remains unmodeled. + +Ran two YAML files sequentially on Python 3.14.4 / Core 3.32.8: annual parent-input tests **2 passed**, membership tests **17 passed**. Each new case passed: + +- Annual override leaves adjacent years using the default. +- January snapshot persists after a child ages out in July. +- Separate-return biological parent retains explicit true while remaining excluded. + +`make format` passed before each commit, including lint. Formula ASTs are unchanged; partner files are untouched. No formula change affects the default dataset, and no microsimulations were run. The 12–50 window and interpretation (i) remain unchanged and queued for Max as d1049. + +Code/test commit: `02cc448746`. Changes and this report were committed on `mo-tanf-dependent-parent-member` and pushed as a fast-forward; the PR body was updated. No merge or force-push. All writes stayed in the assigned workspace, using local Git metadata at `.r2/git` because the sandbox blocks writes to the caller's repository metadata. Test logs and the body draft are in `.r2/`. From baf3fd187a16b0a77116f088a6d17ba1152d5027 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 07:37:31 -0400 Subject: [PATCH 08/10] Remove the hub's round-2 report committed by mistake Co-Authored-By: Claude Opus 5.5 --- 9676-r2-report.md | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 9676-r2-report.md diff --git a/9676-r2-report.md b/9676-r2-report.md deleted file mode 100644 index 6ae023f3b28..00000000000 --- a/9676-r2-report.md +++ /dev/null @@ -1,15 +0,0 @@ -Addressed all four review r1 findings for [PolicyEngine US #9676](https://github.com/PolicyEngine/policyengine-us/pull/9676), starting from `b9e7fff948`. - -- Updated the PR body with both regression directions, including the grandmother aged 58 with $500/month UC: $234.08628 → $0. Saved paired entitlement records gaining/losing are 0/0 unmodified, 0/1 with grandparents marked, and 2/1 with marks plus bank assets zeroed, in both 2025 and 2026. Actual TANF counts remain 0/0. -- Disclosed the review's three Missouri NPCR changes: the great-grandparent case, integration underpayment case, and adult-parent property expectation. The 623 partner cases retain unchanged expectations. Renamed the NPCR property test to describe current behavior. -- Documented why an explicit parent flag cannot attach a child across tax units. A real parent filing separately remains excluded when their own unit has no dependent child; this legally mandatory membership remains unmodeled. - -Ran two YAML files sequentially on Python 3.14.4 / Core 3.32.8: annual parent-input tests **2 passed**, membership tests **17 passed**. Each new case passed: - -- Annual override leaves adjacent years using the default. -- January snapshot persists after a child ages out in July. -- Separate-return biological parent retains explicit true while remaining excluded. - -`make format` passed before each commit, including lint. Formula ASTs are unchanged; partner files are untouched. No formula change affects the default dataset, and no microsimulations were run. The 12–50 window and interpretation (i) remain unchanged and queued for Max as d1049. - -Code/test commit: `02cc448746`. Changes and this report were committed on `mo-tanf-dependent-parent-member` and pushed as a fast-forward; the PR body was updated. No merge or force-push. All writes stayed in the assigned workspace, using local Git metadata at `.r2/git` because the sandbox blocks writes to the caller's repository metadata. Test logs and the body draft are in `.r2/`. From a9dcf78c7d07b27f3c3c96b04adb3f2d78129307 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 12:24:07 -0400 Subject: [PATCH 09/10] Use explicit parent IDs for Missouri TANF units --- .../mo-tanf-dependent-parent-member.fixed.md | 2 +- ...est_mo_tanf_dependent_parent_properties.py | 79 +++- .../mo_tanf_is_parent_of_dependent_child.yaml | 391 ++++++++++++++++++ .../mo_tanf_is_assistance_unit_member.py | 21 +- .../mo_tanf_is_non_parent_caretaker.py | 6 +- .../mo_tanf_is_parent_of_dependent_child.py | 66 +-- .../mo_tanf_non_parent_caretaker.py | 21 +- 7 files changed, 539 insertions(+), 47 deletions(-) diff --git a/changelog.d/mo-tanf-dependent-parent-member.fixed.md b/changelog.d/mo-tanf-dependent-parent-member.fixed.md index f5b2bb2dde7..c89f25b96a0 100644 --- a/changelog.d/mo-tanf-dependent-parent-member.fixed.md +++ b/changelog.d/mo-tanf-dependent-parent-member.fixed.md @@ -1 +1 @@ -Missouri TANF now counts a parent claimed as someone else's tax dependent, such as a young mother claimed by her own parent, as a member of the assistance unit with their child. +Missouri TANF now counts a parent claimed as someone else's tax dependent, such as a young mother claimed by her own parent, as a member of the assistance unit with their child. Explicit parent IDs now take precedence over relationship imputation, with the unchanged age-window fallback used only for children whose parent IDs are unknown. diff --git a/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py b/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py index 630038dd7d3..8c63b9e127d 100644 --- a/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py +++ b/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py @@ -6,8 +6,8 @@ 40-2.310(1)(F)). A parent who is also a cash-eligible child is a member as a child. For a minor parent (the grid's 16-year-old), DSS Manual 0210.005.30 lets that three-generation family file as one group. For an 18-year-old -parent in secondary school the sources leave the grouping open; the grid -checks the model's retained interpretation (i), one combined unit. A +parent in secondary school Max's d1049 approves interpretation (i); the +grid checks one combined unit with her baby and her own parent. A non-parent caretaker relative is excluded while a parent is in the home (0210.005.10). @@ -38,11 +38,14 @@ 6. The default input (own children in the household) gives the same answer as marking the parent directly, and each household gets the same answer alone as in the shared simulation. -7. The default marks a person with own children in the household only when - some dependent child in the tax unit is 12 to 50 years younger, checked - pair by pair against the formula's youngest-and-oldest shortcut. +7. With unknown parent IDs, the default marks a person with own children in + the household only when some dependent child in the tax unit is 12 to + 50 years younger, checked pair by pair against the formula's + youngest-and-oldest shortcut. 8. An explicit parent flag adds a person only when their own tax unit has a dependent child. +9. Known parent IDs identify the named parent independently of ages and + own-child counts. Reusing IDs in separate households cannot link them. """ import itertools @@ -549,3 +552,69 @@ def test_default_parent_input_matches_the_pairwise_age_rule(): # difference of zero never falls in the window. plausible = any(12 <= age - c <= 50 for c in child_ages) assert person_flag == (own > 0 and plausible), (age, children, student, own) + + +def test_known_parent_ids_override_ages_and_stay_within_households(): + # Pin dependency status to isolate parent identification from the age + # rules deciding who is a dependent child. The expected relationship + # stays the same across adult and child ages and both own-child counts. + cases = list( + itertools.product( + (19, 20, 35, 60, 90), + (0, 5, 17), + (0, 1), + ("first", "second", "both"), + ("adult", "head", "absent"), + ) + ) + people, tax_units, spm_units, households = {}, {}, {}, {} + expected = [] + for i, (adult_age, child_age, own_children, slot, parent) in enumerate(cases): + head, adult, child = ( + f"linked{i}_{role}" for role in ("head", "adult", "child") + ) + named_id = {"head": 11, "adult": 22, "absent": 99}[parent] + # The ID contract allows these IDs to repeat in separate households + # when every tax unit stays within its household. This deliberately + # catches a global match that connects unrelated households. + people[head] = { + "person_id": {YEAR: 11}, + "age": {YEAR: 45}, + "is_tax_unit_dependent": {YEAR: False}, + "own_children_in_household": {YEAR: 1}, + "mo_tanf_dependent_child": {PERIOD: False}, + } + people[adult] = { + "person_id": {YEAR: 22}, + "age": {YEAR: adult_age}, + "is_tax_unit_dependent": {YEAR: True}, + "own_children_in_household": {YEAR: own_children}, + "mo_tanf_dependent_child": {PERIOD: False}, + } + people[child] = { + "person_id": {YEAR: 33}, + "age": {YEAR: child_age}, + "is_tax_unit_dependent": {YEAR: True}, + "mo_tanf_dependent_child": {PERIOD: True}, + "parent_1_id": {YEAR: named_id if slot in ("first", "both") else 0}, + "parent_2_id": {YEAR: named_id if slot in ("second", "both") else 0}, + } + members = [head, adult, child] + tax_units[f"linked{i}_tax"] = {"members": members} + spm_units[f"linked{i}_spm"] = {"members": members} + households[f"linked{i}_household"] = { + "members": members, + "state_code": {YEAR: "MO"}, + } + expected.append([parent == "head", parent == "adult", False]) + simulation = Simulation( + situation={ + "people": people, + "tax_units": tax_units, + "spm_units": spm_units, + "households": households, + } + ) + flag = simulation.calculate("mo_tanf_is_parent_of_dependent_child", YEAR) + for case, actual, wanted in zip(cases, flag.reshape(-1, 3), expected): + assert actual.tolist() == wanted, case diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_parent_of_dependent_child.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_parent_of_dependent_child.yaml index 14062d4b981..29a88109f97 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_parent_of_dependent_child.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_is_parent_of_dependent_child.yaml @@ -107,3 +107,394 @@ # Recomputing against July's remaining infant would fail the window # because the age gap is 60, but the annual snapshot stays in force. 2026-07: [true, true, false, true] + +- name: An explicit parent ID identifies an adoptive parent with a 55-year gap + period: 2026-01 + input: + people: + head: + person_id: 11 + age: 85 + adoptive_parent: + person_id: 22 + age: 60 + is_tax_unit_dependent: true + own_children_in_household: 0 + child: + person_id: 33 + age: 5 + is_tax_unit_dependent: true + parent_1_id: 22 + parent_2_id: 0 + tax_units: + tax_unit: + members: [head, adoptive_parent, child] + spm_units: + spm_unit: + members: [head, adoptive_parent, child] + households: + household: + members: [head, adoptive_parent, child] + state_code: MO + output: + mo_tanf_is_parent_of_dependent_child: [false, true, false] + +- name: A known parent ID excludes an unrelated adult with a 30-year gap + period: 2026-01 + input: + people: + head: + person_id: 11 + age: 50 + own_children_in_household: 1 + unrelated_adult: + person_id: 22 + age: 35 + is_tax_unit_dependent: true + own_children_in_household: 1 + child: + person_id: 33 + age: 5 + is_tax_unit_dependent: true + # One known slot suffices: the unknown second slot does not allow + # an age-based relationship to the unrelated adult. + parent_1_id: 11 + parent_2_id: 0 + tax_units: + tax_unit: + members: [head, unrelated_adult, child] + spm_units: + spm_unit: + members: [head, unrelated_adult, child] + households: + household: + members: [head, unrelated_adult, child] + state_code: MO + output: + mo_tanf_is_parent_of_dependent_child: [true, false, false] + +- name: The second parent ID also overrides the age window and own-child count + period: 2026-01 + input: + people: + head: + person_id: 11 + age: 85 + adoptive_parent: + person_id: 22 + age: 60 + is_tax_unit_dependent: true + own_children_in_household: 0 + child: + person_id: 33 + age: 5 + is_tax_unit_dependent: true + parent_1_id: 0 + parent_2_id: 22 + tax_units: + tax_unit: + members: [head, adoptive_parent, child] + spm_units: + spm_unit: + members: [head, adoptive_parent, child] + households: + household: + members: [head, adoptive_parent, child] + state_code: MO + output: + mo_tanf_is_parent_of_dependent_child: [false, true, false] + +- name: A known absent parent suppresses the age fallback + period: 2026-01 + input: + people: + head: + person_id: 11 + age: 50 + unrelated_adult: + person_id: 22 + age: 35 + is_tax_unit_dependent: true + own_children_in_household: 1 + child: + person_id: 33 + age: 5 + is_tax_unit_dependent: true + parent_1_id: 99 + parent_2_id: 0 + tax_units: + tax_unit: + members: [head, unrelated_adult, child] + spm_units: + spm_unit: + members: [head, unrelated_adult, child] + households: + household: + members: [head, unrelated_adult, child] + state_code: MO + output: + mo_tanf_is_parent_of_dependent_child: [false, false, false] + +- name: Two unknown parent IDs preserve the own-child and age-window fallback + period: 2026-01 + input: + people: + head: + person_id: 11 + age: 85 + adult_in_window: + person_id: 22 + age: 35 + is_tax_unit_dependent: true + own_children_in_household: 1 + adult_outside_window: + person_id: 33 + age: 60 + is_tax_unit_dependent: true + own_children_in_household: 1 + adult_without_own_children: + person_id: 44 + age: 35 + is_tax_unit_dependent: true + own_children_in_household: 0 + child: + person_id: 55 + age: 5 + is_tax_unit_dependent: true + parent_1_id: 0 + parent_2_id: 0 + tax_units: + tax_unit: + members: [head, adult_in_window, adult_outside_window, adult_without_own_children, child] + spm_units: + spm_unit: + members: [head, adult_in_window, adult_outside_window, adult_without_own_children, child] + households: + household: + members: [head, adult_in_window, adult_outside_window, adult_without_own_children, child] + state_code: MO + output: + mo_tanf_is_parent_of_dependent_child: [false, true, false, false, false] + +- name: A child with unknown IDs still permits fallback beside a linked child + period: 2026-01 + input: + people: + head: + person_id: 11 + age: 45 + adult: + person_id: 22 + age: 35 + is_tax_unit_dependent: true + own_children_in_household: 1 + linked_child: + person_id: 33 + age: 0 + is_tax_unit_dependent: true + parent_1_id: 11 + parent_2_id: 0 + unlinked_child: + person_id: 44 + age: 5 + is_tax_unit_dependent: true + parent_1_id: 0 + parent_2_id: 0 + tax_units: + tax_unit: + members: [head, adult, linked_child, unlinked_child] + spm_units: + spm_unit: + members: [head, adult, linked_child, unlinked_child] + households: + household: + members: [head, adult, linked_child, unlinked_child] + state_code: MO + output: + mo_tanf_is_parent_of_dependent_child: [true, true, false, false] + +- name: A linked child cannot supply the age fallback for an unrelated adult + period: 2026-01 + input: + people: + head: + person_id: 11 + age: 45 + adult: + person_id: 22 + age: 60 + is_tax_unit_dependent: true + own_children_in_household: 1 + linked_child: + person_id: 33 + age: 17 + is_tax_unit_dependent: true + parent_1_id: 11 + parent_2_id: 0 + unlinked_child: + person_id: 44 + age: 0 + is_tax_unit_dependent: true + parent_1_id: 0 + parent_2_id: 0 + tax_units: + tax_unit: + members: [head, adult, linked_child, unlinked_child] + spm_units: + spm_unit: + members: [head, adult, linked_child, unlinked_child] + households: + household: + members: [head, adult, linked_child, unlinked_child] + state_code: MO + output: + mo_tanf_is_parent_of_dependent_child: [true, false, false, false] + +- name: Known parent IDs exclude an unrelated unmarked tax-unit head + period: 2026-01 + input: + people: + unrelated_head: + person_id: 11 + age: 30 + own_children_in_household: 1 + mother: + person_id: 22 + age: 25 + is_tax_unit_dependent: true + own_children_in_household: 0 + baby: + person_id: 33 + age: 0 + is_tax_unit_dependent: true + parent_1_id: 22 + parent_2_id: 0 + tax_units: + tax_unit: + members: [unrelated_head, mother, baby] + spm_units: + spm_unit: + members: [unrelated_head, mother, baby] + households: + household: + members: [unrelated_head, mother, baby] + state_code: MO + output: + mo_tanf_is_parent_of_dependent_child: [false, true, false] + mo_tanf_is_assistance_unit_member: [false, true, true] + mo_tanf_assistance_unit_size: 2 + +- name: Known parent IDs retain a named head and spouse outside the age window + period: 2026-01 + input: + people: + head: + person_id: 11 + age: 70 + # Isolate parent identification from computed old-age SSI. + ssi: 0 + is_tax_unit_head: true + is_tax_unit_dependent: false + own_children_in_household: 0 + spouse: + person_id: 22 + age: 60 + ssi: 0 + is_tax_unit_spouse: true + is_tax_unit_dependent: false + own_children_in_household: 0 + child: + person_id: 33 + age: 5 + is_tax_unit_dependent: true + parent_1_id: 11 + parent_2_id: 22 + tax_units: + tax_unit: + members: [head, spouse, child] + spm_units: + spm_unit: + members: [head, spouse, child] + households: + household: + members: [head, spouse, child] + state_code: MO + output: + mo_tanf_is_parent_of_dependent_child: [true, true, false] + mo_tanf_is_assistance_unit_member: [true, true, true] + mo_tanf_assistance_unit_size: 3 + +- name: Known IDs stop an unrelated unmarked head from blocking a non-parent caretaker + period: 2026-01 + input: + people: + unrelated_head: + person_id: 11 + age: 50 + is_tax_unit_head: true + is_tax_unit_dependent: false + own_children_in_household: 1 + mo_tanf_is_non_parent_caretaker: false + caretaker_spouse: + person_id: 22 + age: 35 + is_tax_unit_spouse: true + is_tax_unit_dependent: false + own_children_in_household: 1 + mo_tanf_is_non_parent_caretaker: true + child: + person_id: 33 + age: 5 + is_tax_unit_dependent: true + parent_1_id: 99 + parent_2_id: 0 + tax_units: + tax_unit: + members: [unrelated_head, caretaker_spouse, child] + spm_units: + spm_unit: + members: [unrelated_head, caretaker_spouse, child] + households: + household: + members: [unrelated_head, caretaker_spouse, child] + state_code: MO + output: + mo_tanf_is_parent_of_dependent_child: [false, false, false] + mo_tanf_non_parent_caretaker: [false, true, false] + +- name: Explicit parent IDs retain the approved three-generation unit for an 18-year-old student mother + period: 2026-01 + input: + people: + grandmother: + person_id: 11 + age: 45 + own_children_in_household: 0 + mother: + person_id: 22 + age: 18 + is_tax_unit_dependent: true + is_in_secondary_school: true + own_children_in_household: 0 + parent_1_id: 11 + parent_2_id: 0 + baby: + person_id: 33 + age: 0 + is_tax_unit_dependent: true + parent_1_id: 22 + parent_2_id: 0 + tax_units: + tax_unit: + members: [grandmother, mother, baby] + spm_units: + spm_unit: + members: [grandmother, mother, baby] + households: + household: + members: [grandmother, mother, baby] + state_code: MO + output: + mo_tanf_is_parent_of_dependent_child: [true, true, false] + mo_tanf_is_assistance_unit_member: [true, true, true] + mo_tanf_assistance_unit_size: 3 diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py index 5902986762a..7423730f5d3 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.household.demographic.person._parent_links import ( + has_parent_ids, +) class mo_tanf_is_assistance_unit_member(Variable): @@ -42,8 +45,8 @@ def formula(person, period, parameters): # 13 CSR 40-2.300(5)(C) admits only natural or adoptive parents, # and a stepparent's needs are not taken into account per # 2.310(8)(B)1.D.(II); the tax-unit structure cannot distinguish - # a stepparent from a parent, so a spouse who is not the child's - # parent is counted here. + # a stepparent from a parent when child parent ids are unknown. + # Known ids instead determine whether the spouse is a parent. # The caretaker test looks for any dependent child in the home, not # only a payable one. Per DSS Manual 0210.005.05, "when the only # child in the EU receives SSI, explore Temporary Assistance (TA) @@ -60,7 +63,12 @@ def formula(person, period, parameters): # and otherwise included per mo_tanf_non_parent_caretaker_included # (13 CSR 40-2.300(5)(D); DSS Manual 0210.005.15 and 0210.005.35). non_parent = person("mo_tanf_is_non_parent_caretaker", period.this_year) - parent_caretaker = caretaker & ~non_parent + parent = person("mo_tanf_is_parent_of_dependent_child", period.this_year) + unknown_child = dependent_child & ~has_parent_ids(person, period.this_year) + # Known parent ids take precedence over the legacy head/spouse proxy. + # Preserve that proxy when a child's two parent ids are unknown. + presumed_parent = parent | person.tax_unit.any(unknown_child) + parent_caretaker = caretaker & ~non_parent & presumed_parent # Membership as a parent does not depend on who claims whom for # taxes. 13 CSR 40-2.300(5)(C) and the Combined IM Policy Manual # 4.2.2 (formerly DSS Manual 0210.005.05) make the "Biological or @@ -80,11 +88,10 @@ def formula(person, period, parameters): # parent (under 18, including the month of turning 18), the # manual's minor parent provision (4.2.4; formerly 0210.005.30) lets # that three-generation family file as one assistance group. For an - # 18-year-old parent in secondary school the sources do not settle - # the grouping; keeping her in one combined unit with her own parent - # is retained interpretation (i), one of several readings. + # 18-year-old parent in secondary school, Max's d1049 approves + # interpretation (i): her, her baby and her own parent form one unit. other_parent = ( - person("mo_tanf_is_parent_of_dependent_child", period.this_year) + parent & ~non_parent & ~dependent_child & ~is_ssi_recipient diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_non_parent_caretaker.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_non_parent_caretaker.py index d45c1c84bfe..23f59099856 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_non_parent_caretaker.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_non_parent_caretaker.py @@ -12,8 +12,10 @@ class mo_tanf_is_non_parent_caretaker(Variable): "guardian. Missouri calls a related caretaker a non-parent caretaker " "relative (NPCR) and applies the same rules to a related or " "unrelated legal guardian. Mark both spouses when neither is a " - "parent of the children; a spouse left false is treated as a parent, " - "which excludes the other. Leave false for parents. A stepparent " + "parent of the children. With unknown child parent ids, a spouse left " + "false is presumed to be a parent and excludes the other. Known " + "parent_1_id and parent_2_id inputs instead identify the parents. " + "Leave false for parents. A stepparent " "living with the child's parent is not a non-parent caretaker, and " "neither is a grandparent whose own minor child (the children's " "parent) is one of the dependent children. A non-parent caretaker " diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py index a5ed547185a..89e3d85a19a 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py @@ -1,4 +1,8 @@ from policyengine_us.model_api import * +from policyengine_us.variables.household.demographic.person._parent_links import ( + co_resident_parent_indices, + has_parent_ids, +) class mo_tanf_is_parent_of_dependent_child(Variable): @@ -12,19 +16,25 @@ class mo_tanf_is_parent_of_dependent_child(Variable): "dependent, unless they receive SSI or are a dependent child " "themselves (then they are a member as a child). A parent in the " "home, other than a cash-eligible child, also excludes a non-parent " - "caretaker. Defaults to having one's own children in the household " - "(own_children_in_household) and being 12 to 50 years older than one " - "of the tax unit's dependent children, as of January of the year. " - "The age window is an imputation rule, not law. The count also " - "includes adult children and children outside the tax unit, so set " - "this input directly when it does not match: false, for example, for " - "a dependent whose own child in the home is an adult, and true for an " - "adoptive parent outside the 12-to-50-year window. Setting it for one " + "caretaker. Reads January's dependent children in the person's tax " + "unit. First uses each child's parent_1_id and parent_2_id, resolved " + "to a co-resident person_id: a named parent qualifies regardless of " + "ages or own_children_in_household. Only a child whose two parent " + "ids are both unknown (0) permits the existing fallback: is_parent " + "and an age gap of 12 to 50 years. Known ids naming another person " + "or an absent parent do not permit age inference for that child. " + "This follows d1049 and the principle to make inputs as leaf-nodey " + "as possible; the age window imputes a relationship, not law. " + "The fallback's own-child count can include adult children and " + "children outside the tax unit, so supply parent ids when known, " + "or set this input directly when the fallback does not match. " + "Setting it for one " "person for a year sets it to false for everyone else not given a " "value for that year, so set it for every parent it applies to; other " "years still use the default. Heads and spouses not marked as " - "non-parent caretakers are always treated as parents; this input " - "applies to other tax-unit members. This input cannot associate a " + "non-parent caretakers retain their presumed-parent status only " + "when a dependent child's parent ids are unknown, or this flag " + "identifies them as a parent. This input cannot associate a " "parent filing a separate tax return with a child in another tax " "unit: even an explicit true leaves that parent excluded when their " "own tax unit has no dependent child. The assistance-unit formulas " @@ -40,24 +50,28 @@ class mo_tanf_is_parent_of_dependent_child(Variable): defined_for = StateCode.MO def formula(person, period, parameters): - # own_children_in_household counts own children of any age, so on - # its own it also marks, for example, the head's elderly mother, - # whose own child in the home is the head. Also require the person - # to be 12 to 50 years older than at least one of the tax unit's - # dependent children. The bounds approximate typical birth ages; - # they do not establish parenthood. This window is an imputation rule, - # not law: the rule covers adoptive parents of any age, so a genuine - # adoptive parent outside the window needs this input set to true. - # Dependent children are all under 19, so their ages span less than - # the 38-year window, and "some child is 12 to 50 years younger" - # reduces to the youngest being at least 12 years younger and the - # oldest at most 50. # The flag is annual and reads January's dependent children, so a # change in who is a dependent child later in the year (for example # a child aging out through monthly_age inputs) does not update it. + dependent_child = person("mo_tanf_dependent_child", period.first_month) + # Use relationship leaf inputs first (Max's d1049). Resolve within + # households using the shared helpers, so repeated ids in different + # households cannot join unrelated families. Preserve the tax-unit + # child grouping used by the assistance-unit formulas. + tax_unit = person.tax_unit.reference_entity.members_entity_id + linked_parent = np.zeros(person.count, dtype=bool) + for parent in co_resident_parent_indices(person, period): + relevant = dependent_child & (parent >= 0) & (tax_unit[parent] == tax_unit) + linked_parent |= np.bincount(parent[relevant], minlength=person.count) > 0 + + # Only fully unknown child links permit the unchanged 12–50 fallback. + # Known but absent parents are still known and do not permit it. + # Dependent children's ages span less than the 38-year window, so + # the youngest/oldest shortcut is equivalent to checking each child. + unknown_child = dependent_child & ~has_parent_ids(person, period) has_own_children = person("is_parent", period) age = person("age", period) - dependent_child = person("mo_tanf_dependent_child", period.first_month) - youngest = person.tax_unit.min(where(dependent_child, age, np.inf)) - oldest = person.tax_unit.max(where(dependent_child, age, -np.inf)) - return has_own_children & (age - youngest >= 12) & (age - oldest <= 50) + youngest = person.tax_unit.min(where(unknown_child, age, np.inf)) + oldest = person.tax_unit.max(where(unknown_child, age, -np.inf)) + fallback = has_own_children & (age - youngest >= 12) & (age - oldest <= 50) + return linked_parent | fallback diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_non_parent_caretaker.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_non_parent_caretaker.py index e8b193c6825..a5f4ab181e1 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_non_parent_caretaker.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_non_parent_caretaker.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.household.demographic.person._parent_links import ( + has_parent_ids, +) class mo_tanf_non_parent_caretaker(Variable): @@ -32,8 +35,10 @@ def formula(person, period, parameters): # the home", and DSS Manual 0210.005.10 excludes "A legal guardian # or NPCR if a biological or adoptive parent is in the home." Two # signals mark a parent in the home: - # - a caretaker not marked as a non-parent, including one who - # receives SSI (excluded from the unit, but still in the home); + # - a caretaker not marked as a non-parent, presumed to be a parent + # only for unknown child parent ids, or identified by the parent + # flag, including one who receives SSI (excluded from the unit, + # but still in the home); # - any other member of the tax unit marked as a parent of a # dependent child, such as an adult daughter claimed as the # grandparent's dependent. A parent who is a cash-eligible child @@ -45,11 +50,15 @@ def formula(person, period, parameters): # separate return is not seen. is_ssi_recipient = (person("ssi", period) > 0) | person("receives_ssi", period) cash_eligible_child = dependent_child & ~is_ssi_recipient - other_parent = ( - person("mo_tanf_is_parent_of_dependent_child", period.this_year) - & ~cash_eligible_child + parent = person("mo_tanf_is_parent_of_dependent_child", period.this_year) + other_parent = parent & ~cash_eligible_child + unknown_child = dependent_child & ~has_parent_ids(person, period.this_year) + # A known child's ids override the presumed parenthood of an + # unmarked head/spouse; retain the proxy only for unknown links. + parent_caretaker = caretaker & (parent | person.tax_unit.any(unknown_child)) + parent_in_home = person.tax_unit.any( + (parent_caretaker | other_parent) & ~non_parent ) - parent_in_home = person.tax_unit.any((caretaker | other_parent) & ~non_parent) # DSS Manual 0210.005.35: an NPCR who receives SSI, SSI-SP or SP # cannot have their needs and income included. SP receipt is not # observable (see mo_tanf_is_assistance_unit_member). From 87d460fd1a6bf5fb8f7054fb7632f1234bd1ac15 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 20:07:55 -0400 Subject: [PATCH 10/10] Document Missouri TANF parent-ID limits and verify unknown-ID invariants --- .../mo-tanf-dependent-parent-member.fixed.md | 2 +- ...est_mo_tanf_dependent_parent_properties.py | 147 ++++++++++++ .../tanf/mo_tanf_parent_input_contract.yaml | 224 ++++++++++++++++++ .../mo_tanf_is_assistance_unit_member.py | 5 +- .../mo_tanf_is_non_parent_caretaker.py | 6 +- .../mo_tanf_is_parent_of_dependent_child.py | 13 + 6 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_parent_input_contract.yaml diff --git a/changelog.d/mo-tanf-dependent-parent-member.fixed.md b/changelog.d/mo-tanf-dependent-parent-member.fixed.md index c89f25b96a0..8633616f183 100644 --- a/changelog.d/mo-tanf-dependent-parent-member.fixed.md +++ b/changelog.d/mo-tanf-dependent-parent-member.fixed.md @@ -1 +1 @@ -Missouri TANF now counts a parent claimed as someone else's tax dependent, such as a young mother claimed by her own parent, as a member of the assistance unit with their child. Explicit parent IDs now take precedence over relationship imputation, with the unchanged age-window fallback used only for children whose parent IDs are unknown. +Missouri TANF now counts a parent claimed as someone else's tax dependent, such as a young mother claimed by her own parent, as a member of the assistance unit with their child. Explicit parent IDs now take precedence over relationship imputation, with the unchanged age-window fallback used only for children whose parent IDs are unknown. The Missouri parent input documents the shared IDs' step-parent ambiguity and annual-flag override, and known-ID non-parent caretakers must be explicitly marked. diff --git a/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py b/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py index 8c63b9e127d..5312218b384 100644 --- a/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py +++ b/policyengine_us/tests/core/test_mo_tanf_dependent_parent_properties.py @@ -46,6 +46,9 @@ dependent child. 9. Known parent IDs identify the named parent independently of ages and own-child counts. Reusing IDs in separate households cannot link them. +10. Omitted and explicitly zero parent IDs preserve the frozen pre-d1049 + parent flags, NPCR identification, membership, unit sizes and zero-income + grants, checked against independent expectations from b9e7fff. """ import itertools @@ -618,3 +621,147 @@ def test_known_parent_ids_override_ages_and_stay_within_households(): flag = simulation.calculate("mo_tanf_is_parent_of_dependent_child", YEAR) for case, actual, wanted in zip(cases, flag.reshape(-1, 3), expected): assert actual.tolist() == wanted, case + + +@pytest.mark.parametrize("explicit_zero_ids", (False, True)) +def test_unknown_parent_ids_reproduce_the_frozen_pre_d1049_rule(explicit_zero_ids): + """Unknown ids preserve pre-d1049 flags, caretakers, members, sizes and grants. + + Independently restate the three changed formulas at immutable commit + b9e7fff948caf4f33313c942c09caa7c7ccc3dea, rather than reading any current + model output to construct expectations. Cover 224 households (784 people) + for each of omitted parent ids and explicitly zero parent ids. The grid + includes both age-window boundaries, own-child counts, caretaker markings, + couples, SSI exclusions and an 18-year-old secondary-school student. + """ + cases = list( + itertools.product( + ( + (11, False), + (12, False), + (18, False), + (18, True), + (19, False), + (50, False), + (51, False), + ), + (0, 1), + (False, True), + (False, True), + ("none", "head", "adult", "child"), + ) + ) + people, tax_units, spm_units, households = {}, {}, {}, {} + expected = { + "mo_tanf_is_parent_of_dependent_child": [], + "mo_tanf_non_parent_caretaker": [], + "mo_tanf_is_assistance_unit_member": [], + "mo_tanf_assistance_unit_size": [], + "mo_tanf": [], + } + for i, ((adult_age, student), own, marked, married, ssi_role) in enumerate(cases): + roles = np.array( + ["head"] + (["spouse"] if married else []) + ["adult", "child"] + ) + ages = np.array( + [ + {"head": 55, "spouse": 53, "adult": adult_age, "child": 0}[role] + for role in roles + ] + ) + own_children = np.where(roles == "adult", own, 0) + dependent_child = (roles == "child") | ( + (roles == "adult") & ((adult_age < 18) | ((adult_age == 18) & student)) + ) + receives_ssi = roles == ssi_role + caretaker = np.isin(roles, ("head", "spouse")) + non_parent = caretaker & marked + + # Frozen b9e7fff parent formula, expressed pairwise instead of through + # the youngest/oldest shortcut. Unknown ids must retain this fallback. + parent = np.array( + [ + count > 0 + and any( + 12 <= age - child_age <= 50 for child_age in ages[dependent_child] + ) + for age, count in zip(ages, own_children) + ] + ) + cash_eligible_child = dependent_child & ~receives_ssi + parent_in_home = np.any( + (caretaker | (parent & ~cash_eligible_child)) & ~non_parent + ) + candidate = caretaker & non_parent & ~parent_in_home & ~receives_ssi + # Frozen b9e7fff NPCR formula: head first, otherwise the spouse. + npcr = candidate & ((roles == "head") | ~candidate[0]) + # With zero non-SSI income and resources, every identified NPCR is + # needy and adding their needs raises the grant, so they are included. + member = ( + cash_eligible_child + | (caretaker & ~non_parent & ~receives_ssi) + | (parent & ~non_parent & ~dependent_child & ~receives_ssi) + | npcr + ) + expected["mo_tanf_is_parent_of_dependent_child"].extend(parent) + expected["mo_tanf_non_parent_caretaker"].extend(npcr) + expected["mo_tanf_is_assistance_unit_member"].extend(member) + expected["mo_tanf_assistance_unit_size"].append(int(member.sum())) + expected["mo_tanf"].append(_payment(member.sum())) + + names = [] + for role, age, count, ssi in zip(roles, ages, own_children, receives_ssi): + name = f"unknown{i}_{role}" + names.append(name) + people[name] = { + "age": {YEAR: int(age)}, + "is_tax_unit_head": {YEAR: bool(role == "head")}, + "is_tax_unit_spouse": {YEAR: bool(role == "spouse")}, + "is_tax_unit_dependent": {YEAR: role in ("adult", "child")}, + "own_children_in_household": {YEAR: int(count)}, + "is_in_secondary_school": {YEAR: bool(role == "adult" and student)}, + "mo_tanf_is_non_parent_caretaker": { + YEAR: role in ("head", "spouse") and marked + }, + # Freeze both SSI signals; computing SSI from otherwise empty + # income inputs would introduce unrelated benefit eligibility. + "receives_ssi": {YEAR: bool(ssi)}, + "ssi": {PERIOD: 100 if ssi else 0}, + "employment_income_before_lsr": {YEAR: 0}, + "unemployment_compensation": {YEAR: 0}, + } + if explicit_zero_ids: + people[name]["parent_1_id"] = {YEAR: 0} + people[name]["parent_2_id"] = {YEAR: 0} + tax_units[f"unknown{i}_tax"] = {"members": names} + spm_units[f"unknown{i}_spm"] = {"members": names} + households[f"unknown{i}_household"] = { + "members": names, + "state_code": {YEAR: "MO"}, + } + + assert len(cases) == 224 and len(people) == 784 + # Both sides of each boolean rule occur, so constant outputs cannot pass. + for variable in ( + "mo_tanf_is_parent_of_dependent_child", + "mo_tanf_non_parent_caretaker", + "mo_tanf_is_assistance_unit_member", + ): + assert any(expected[variable]) and not all(expected[variable]), variable + simulation = Simulation( + situation={ + "people": people, + "tax_units": tax_units, + "spm_units": spm_units, + "households": households, + } + ) + for variable, values in expected.items(): + period = YEAR if variable == "mo_tanf_is_parent_of_dependent_child" else PERIOD + actual = simulation.calculate(variable, period) + if variable == "mo_tanf": + np.testing.assert_allclose( + actual, values, rtol=0, atol=1e-3, err_msg=variable + ) + else: + np.testing.assert_array_equal(actual, values, err_msg=variable) diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_parent_input_contract.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_parent_input_contract.yaml new file mode 100644 index 00000000000..b8fb815e7d8 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/dss/tanf/mo_tanf_parent_input_contract.yaml @@ -0,0 +1,224 @@ +# R3 P2: shared parent IDs include step-parents. These paired years retain +# those leaf IDs and demonstrate the documented Missouri annual-flag remedy. +# The automatic result is preserved as evidence of the disclosed limitation; +# exclusion from member income does not establish step-parent income deeming. +- name: A named step-parent is excluded by a complete annual parent-flag override + period: 2026-01 + input: + people: + mother: + person_id: + 2026: 11 + 2027: 11 + age: + 2026: 35 + 2027: 35 + is_tax_unit_dependent: + 2026: false + 2027: false + own_children_in_household: + 2026: 0 + 2027: 0 + mo_tanf_is_parent_of_dependent_child: + 2027: true + stepfather: + person_id: + 2026: 22 + 2027: 22 + age: + 2026: 37 + 2027: 37 + is_tax_unit_dependent: + 2026: false + 2027: false + own_children_in_household: + 2026: 0 + 2027: 0 + unemployment_compensation: + 2026: 2400 + 2027: 2400 + mo_tanf_is_parent_of_dependent_child: + 2027: false + child: + person_id: + 2026: 33 + 2027: 33 + age: + 2026: 5 + 2027: 5 + is_tax_unit_dependent: + 2026: true + 2027: true + parent_1_id: + 2026: 11 + 2027: 11 + parent_2_id: + 2026: 22 + 2027: 22 + mo_tanf_is_parent_of_dependent_child: + 2027: false + tax_units: + tax_unit: + members: [mother, stepfather, child] + spm_units: + spm_unit: + members: [mother, stepfather, child] + households: + household: + members: [mother, stepfather, child] + state_code: + 2026: MO + 2027: MO + output: + mo_tanf_is_parent_of_dependent_child: + 2026: [true, true, false] + 2027: [true, false, false] + mo_tanf_is_assistance_unit_member: + 2026-01: [true, true, true] + 2027-01: [true, false, true] + mo_tanf_assistance_unit_size: + 2026-01: 3 + 2027-01: 2 + mo_tanf_gross_unearned_income: + 2026-01: 200 + 2027-01: 0 + +- name: A named step-parent does not block a marked caretaker after the annual override + period: 2026-01 + input: + people: + grandmother: + person_id: + 2026: 11 + 2027: 11 + age: + 2026: 55 + 2027: 55 + is_tax_unit_dependent: + 2026: false + 2027: false + mo_tanf_is_non_parent_caretaker: + 2026: true + 2027: true + mo_tanf_is_parent_of_dependent_child: + 2027: false + stepfather: + person_id: + 2026: 22 + 2027: 22 + age: + 2026: 35 + 2027: 35 + is_tax_unit_dependent: + 2026: true + 2027: true + own_children_in_household: + 2026: 0 + 2027: 0 + mo_tanf_is_parent_of_dependent_child: + 2027: false + child: + person_id: + 2026: 33 + 2027: 33 + age: + 2026: 5 + 2027: 5 + is_tax_unit_dependent: + 2026: true + 2027: true + # The natural parent is absent; the co-resident ID names only a step-parent. + parent_1_id: + 2026: 99 + 2027: 99 + parent_2_id: + 2026: 22 + 2027: 22 + mo_tanf_is_parent_of_dependent_child: + 2027: false + tax_units: + tax_unit: + members: [grandmother, stepfather, child] + spm_units: + spm_unit: + members: [grandmother, stepfather, child] + households: + household: + members: [grandmother, stepfather, child] + state_code: + 2026: MO + 2027: MO + output: + mo_tanf_is_parent_of_dependent_child: + 2026: [false, true, false] + 2027: [false, false, false] + mo_tanf_non_parent_caretaker: + 2026-01: [false, false, false] + 2027-01: [true, false, false] + mo_tanf_is_assistance_unit_member: + 2026-01: [false, true, true] + 2027-01: [true, false, true] + +# R3 P3: known IDs suppress the head's parent proxy but cannot identify an NPCR. +# In otherwise identical years, only the explicit caretaker mark changes. +- name: Known absent parent IDs require an explicit mark to include a needy grandmother + period: 2026-01 + input: + people: + grandmother: + person_id: + 2026: 11 + 2027: 11 + age: + 2026: 55 + 2027: 55 + is_tax_unit_dependent: + 2026: false + 2027: false + own_children_in_household: + 2026: 1 + 2027: 1 + mo_tanf_is_non_parent_caretaker: + 2026: false + 2027: true + grandchild: + person_id: + 2026: 33 + 2027: 33 + age: + 2026: 5 + 2027: 5 + is_tax_unit_dependent: + 2026: true + 2027: true + parent_1_id: + 2026: 99 + 2027: 99 + parent_2_id: + 2026: 0 + 2027: 0 + tax_units: + tax_unit: + members: [grandmother, grandchild] + spm_units: + spm_unit: + members: [grandmother, grandchild] + households: + household: + members: [grandmother, grandchild] + state_code: + 2026: MO + 2027: MO + output: + mo_tanf_is_parent_of_dependent_child: + 2026: [false, false] + 2027: [false, false] + mo_tanf_non_parent_caretaker: + 2026-01: [false, false] + 2027-01: [true, false] + mo_tanf_is_assistance_unit_member: + 2026-01: [false, true] + 2027-01: [true, true] + mo_tanf_assistance_unit_size: + 2026-01: 1 + 2027-01: 2 diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py index 7423730f5d3..e7edffcb50a 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/mo_tanf_is_assistance_unit_member.py @@ -46,7 +46,10 @@ def formula(person, period, parameters): # and a stepparent's needs are not taken into account per # 2.310(8)(B)1.D.(II); the tax-unit structure cannot distinguish # a stepparent from a parent when child parent ids are unknown. - # Known ids instead determine whether the spouse is a parent. + # Shared parent ids also include stepparents and cannot distinguish + # parent type. When those ids name a stepparent, use the documented + # annual parent-flag override for children with known links. This + # does not implement the required stepparent income deeming. # The caretaker test looks for any dependent child in the home, not # only a payable one. Per DSS Manual 0210.005.05, "when the only # child in the EU receives SSI, explore Temporary Assistance (TA) diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_non_parent_caretaker.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_non_parent_caretaker.py index 23f59099856..0b3fe2c04fc 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_non_parent_caretaker.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_non_parent_caretaker.py @@ -14,7 +14,11 @@ class mo_tanf_is_non_parent_caretaker(Variable): "unrelated legal guardian. Mark both spouses when neither is a " "parent of the children. With unknown child parent ids, a spouse left " "false is presumed to be a parent and excludes the other. Known " - "parent_1_id and parent_2_id inputs instead identify the parents. " + "parent_1_id and parent_2_id inputs instead identify named adults " + "without distinguishing step-parents; see the parent flag's " + "documented override. A caretaker relative is not inferred from " + "known child parent ids: explicitly mark the NPCR. When no parent " + "is present, leaving the NPCR unmarked keeps the unit child-only. " "Leave false for parents. A stepparent " "living with the child's parent is not a non-parent caretaker, and " "neither is a grandparent whose own minor child (the children's " diff --git a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py index 89e3d85a19a..68d462715ff 100644 --- a/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py +++ b/policyengine_us/variables/gov/states/mo/dss/tanf/assistance_unit/non_parent_caretaker/mo_tanf_is_parent_of_dependent_child.py @@ -25,6 +25,18 @@ class mo_tanf_is_parent_of_dependent_child(Variable): "or an absent parent do not permit age inference for that child. " "This follows d1049 and the principle to make inputs as leaf-nodey " "as possible; the age window imputes a relationship, not law. " + "The shared parent ids include step-parents and do not record parent " + "type, so this default also treats a named step-parent as biological " + "or adoptive. Missouri admits only natural or adoptive parents under " + "13 CSR 40-2.300(5)(C); a step-parent must not add needs or block an " + "NPCR under (5)(D). Preserve shared parent ids for other programs. " + "When they include a step-parent, explicitly set this flag true for " + "every applicable biological or adoptive parent and false for " + "step-parents and everyone else for that year. This corrects parent " + "membership and NPCR identification only when all relevant children " + "have known links: any child with two unknown ids retains the " + "head/spouse proxy even with an explicit false. Step-parent income " + "deeming under 13 CSR 40-2.310(8)(B)1.D.(II) is not modeled. " "The fallback's own-child count can include adult children and " "children outside the tax unit, so supply parent ids when known, " "or set this input directly when the fallback does not match. " @@ -46,6 +58,7 @@ class mo_tanf_is_parent_of_dependent_child(Variable): "https://my.mo.gov/cms_fsd?id=kb_article_view&sys_kb_id=98e1ef0c1b543650ba12657ae54bcbd1", "https://dssmanuals.mo.gov/temporary-assistance-case-management/0210-005-05/", "https://dssmanuals.mo.gov/temporary-assistance-case-management/0210-005-10/", + "https://www.sos.mo.gov/cmsimages/adrules/csr/current/13csr/13c40-2.pdf#page=30", ) defined_for = StateCode.MO