PolicyEngine / PolicyEngine/policyengine-us

SNAP excludes a lone minor household head's earnings — 7 CFR 273.9(c)(7) parental-control condition not modeled

Open
#9,157 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
162
Forks
212
Avg merge
3d 3h
Merged PRs (30d)
97

Description

## Summary
`snap_excluded_child_earner` excludes the employment income of any under-18 K-12 student, including a minor who is their **own** household head living alone. The regulatory exclusion applies only to a child **under the parental control of another household member**; a lone minor's wages are countable.

7 CFR 273.1(b)(1)(iii) (household composition) conditions child inclusion on living "with and \[being\] under the parental control of a household member other than his or her parent," and 273.9(c)'s minor-earnings exclusions are likewise tied to being "under the parental control of another \[adult\] member" (see also the WIA/WIOA clause in 273.9(c), which applies "to household members under 19 years of age who are under the parental control of another adult member"). A one-person household headed by a 15–17-year-old has no such other member, so the student-earnings exclusion cannot apply.

## Reproduction (verified on 1.767.3 and 1.752.2, core 3.30.3/3.28.0)
One person, age 17, `employment_income` = $217,027.52, own household (AL, 2026):

- `is_in_k12_school` imputes/defaults to True
- `snap_excluded_child_earner` = **True**
- `snap_earned_income` = **$0**, gross/net income tests pass
- `is_snap_eligible` = **True**, `snap` = **$298/yr**

Age-18 control with identical income: earnings counted, correctly ineligible. Same result for age 15 (NC). Full script attached below (adapted from our oracle-comparison harness; found via Axiom↔PolicyEngine SNAP conformance sweeps on Enhanced CPS, where a synthetic clone mass point at $217,027.52 produces 12 such records across AL/MA/NC/SC/TN).

repro.py

```python
"""Reproduce the SNAP lone-minor income bug in PolicyEngine US 1.767.3."""

from importlib.metadata import version

from policyengine_us import Simulation

YEAR = "2026"
MONTH = "2026-01"
ANNUAL_EARNINGS = 217_027.52

def make_simulation(
age: int, state: str, in_k12: bool | None = None
) -> Simulation:
person = {
"age": {YEAR: age},
"employment_income": {YEAR: ANNUAL_EARNINGS},
"is_household_head": True,
}
if in_k12 is not None:
person["is_in_k12_school"] = {YEAR: in_k12}

return Simulation(
situation={
"people": {"person": person},
"families": {"family": {"members": ["person"]}},
"marital_units": {"marital_unit": {"members": ["person"]}},
"spm_units": {"spm_unit": {"members": ["person"]}},
"tax_units": {"tax_unit": {"members": ["person"]}},
"households": {
"household": {
"members": ["person"],
"state_code": {YEAR: state},
}
},
}
)

def scalar(simulation: Simulation, variable: str, period: str):
return simulation.calculate(variable, period)[0].item()

def show(
label: str, age: int, state: str, in_k12: bool | None = None
) -> None:
simulation = make_simulation(age, state, in_k12)
variables = {
"employment_income_annual": ("employment_income", YEAR),
"is_in_k12_school": ("is_in_k12_school", YEAR),
"monthly_age": ("monthly_age", MONTH),
"is_household_head": ("is_household_head", YEAR),
"is_tax_unit_head": ("is_tax_unit_head", YEAR),
"is_tax_unit_dependent": ("is_tax_unit_dependent", YEAR),
"spm_unit_size": ("spm_unit_size", YEAR),
"snap_unit_size": ("snap_unit_size", MONTH),
"snap_excluded_child_earner": (
"snap_excluded_child_earner",
MONTH,
),
"snap_countable_earner": ("snap_countable_earner", MONTH),
"snap_earned_income_person": ("snap_earned_income_person", MONTH),
"snap_earned_income": ("snap_earned_income", MONTH),
"snap_gross_income": ("snap_gross_income", MONTH),
"meets_snap_gross_income_test": (
"meets_snap_gross_income_test",
MONTH,
),
"snap_net_income": ("snap_net_income", MONTH),
"meets_snap_net_income_test": (
"meets_snap_net_income_test",
MONTH,
),
"is_snap_eligible": ("is_snap_eligible", MONTH),
"snap": ("snap", MONTH),
}
print(f"\n{label}")
for label_name, (variable, period) in variables.items():
print(f"{label_name}={scalar(simulation, variable, period)}")

print(f"policyengine-us={version('policyengine-us')}")
print(f"policyengine-core={version('policyengine-core')}")
print(f"spm-calculator={version('spm-calculator')}")
show("AL, age 17, K-12 imputed", 17, "AL")
show("NC, age 15, K-12 imputed", 15, "NC")
show("AL, age 17, K-12 explicitly false", 17, "AL", False)
show("AL, age 18, K-12 imputed", 18, "AL")
```

## Expected
The child-earner exclusion should require the child to be under the parental control of **another** SNAP household member (not the child themself as head of a one-person unit), per 7 CFR 273.1(b)(1)(iii) and 273.9(c).

## Impact
Any lone-minor unit with earnings is scored eligible regardless of income. On Enhanced CPS 2024 this affects 12 records concentrated at a clone mass point.

Contributor guide

Open the contributing guide

Research direction

Start by tracing the snap_excluded_child_earner calculation and its relationship to snap_countable_earner and snap_earned_income_person. Run the supplied reproduction for a one-person 15–17-year-old household and compare it with the age-18 case. Done means a lone minor's earnings are countable while the child-earner exclusion still applies when another adult household member has parental control.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.