Repository navigation
Exclude input dependents from tax unit head and spouse inference - #9630
Merged
Merged
Conversation
is_tax_unit_head picked the oldest non-child and is_tax_unit_spouse the next oldest adult, ignoring an input is_tax_unit_dependent. A dependent elderly parent therefore became the head (and the real head the spouse), and an adult child input as a dependent became a single parent's spouse, turning the return JOINT. The web app flags every dependent it adds and never sends head or spouse, so this reached app users who set a dependent's age to 18 or over. Head and spouse now rank only adults not input as dependents, unless a tax unit has no such adult, in which case every adult stays a candidate as before. is_tax_unit_dependent's formula depends on head and spouse, so the inputs are read with simulation.get_array, which never runs a formula. Without dependent inputs the rule is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9630 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 4 3 -1
Lines 76 36 -40
Branches 2 1 -1
=========================================
- Hits 76 36 -40
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… spouse CI on 9b2c416 found both. In each, an 18-year-old input dependent was inferred as the filer's spouse on main, so the unit filed jointly. With input dependents excluded from head and spouse inference, the parent files alone as head of household: - MA TAFDC integration Case 2: the dependent care deduction is no longer halved for a joint filer, so 7600 * 0.5 - 528 = 3272 and countable earned income is 3272 * 6/12 = 1636 (was 1768); TAFDC is 6439 (was 6307). The old comment's arithmetic (1900 - 528 = 1768) did not add up. - AFA other dependent credit: the head-of-household higher threshold is 300,000, not the joint 400,000. The "without limited phase-out" case lowers the head's wages to 290,000 to keep its intent; the "fully phased out" case's phase-out is ceil(150,000 / 1,000) * 50 = 7,500. Both files pass locally (3 and 12 cases). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Merged by the US + core hub on its merge gates. Audit:
|
MaxGhenis
added a commit
that referenced
this pull request
Oct 6, 2026
Conflicts: - pyproject.toml, uv.lock: main already added hypothesis>=6.100.0 to the dev group, this branch's only new dependency, so both files are main's; `uv lock` leaves uv.lock unchanged. - is_tax_unit_head.py, is_tax_unit_spouse.py: #9630 narrowed the age-ordered fallback to head_or_spouse_candidates (skipping input dependents); this branch wraps that fallback in where(tax_unit_roles_supplied, <supplied role>, <fallback>). Kept both: supplied roles still decide when every member has one, and otherwise the fallback uses main's candidates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 6, 2026
MaxGhenis
added a commit
that referenced
this pull request
Oct 6, 2026
Brings in #9841 (28 percent rate gain and unrecaptured section 1250 gain netted against losses), #9801, #9790, #9779, #9768, #9741 and #9630. Only test_section_911_tax_stacking.py changed on both sides, in different functions, and git merged it without conflict. #9841 passes Schedule D lines 18 and 19 entered already netted through unchanged, and this branch's capital_gains_tax, reforms and tests read the section 911 amounts and Schedule D inputs that #9841 keeps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 6, 2026
MaxGhenis
added a commit
that referenced
this pull request
Oct 6, 2026
Filing status is a policy calculation, so the model computes it from the household's facts, as on main. Supplied data may set who is head, spouse or dependent, but not the filing status. - filing_status.py returns to main's formula; no supplied status is read. - Remove filing_status_input and its YAML tests. The Populace build's filing_status_input column now matches no variable, so the loader skips it. - Revert the romney remove_head_of_household cases, which tested only how the repeal re-derived a supplied status. - Keep tax_unit_role_input, tax_unit_roles_supplied and the head/spouse formulas composed with #9630's inference. - Unit tests: drop the supplied-status tests; add a dataset test showing a filing_status_input column that disagrees with the rules is not read, in the data year and an extended year. - Property tests: roles only. Filing status is joint exactly when there is a spouse; the role input matches pinning the role flags (neither side pins filing_status); a new property composes supplied roles with input dependents, against an oracle that skips them as #9630 does. - Microsim test: check the build's roles only. - Docs: roles only, the fallback as it stands after #9630, and dependents' above-the-line deductions after #9801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 6, 2026
The age-ordered fallback skips adults input as tax unit dependents unless every adult in the unit is one, in which case every adult stays a candidate. Say so in tax_unit_role_input's documentation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 6, 2026
MaxGhenis
added a commit
that referenced
this pull request
Oct 8, 2026
* Honor dataset-supplied tax-unit roles and filing status Populace's tax-unit constructor (microunit) writes each person's role (tax_unit_role_input: HEAD, SPOUSE, DEPENDENT) and each unit's filing status (filing_status_input) into the certified build, but no variable read them, so the loader dropped both and the role variables fell back to age ordering. Add the two input variables under the producer's exact column and member names, with an UNSPECIFIED default, and make is_tax_unit_head, is_tax_unit_spouse and filing_status defer to them when supplied. Malformed supplied values raise instead of falling back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Let a reform switch off only the status whose rule it removes Review found that neutralizing filing_status_input in remove_head_of_household re-derived every supplied status, moving 158 non-head-of-household units on the certified build, and that the gov.abolitions parameters for head_of_household_eligible and surviving_spouse_eligible did nothing to supplied statuses. filing_status now re-derives a supplied HEAD_OF_HOUSEHOLD or SURVIVING_SPOUSE only when that rule is neutralized or abolished, using core's own switch-off test, and the reform is back to its original single neutralization. Also: check a supplied JOINT status against the supplied roles so abolishing a role variable cannot raise; keep an explicit head out of the supplied spouse role; document precedence, the fallback's separation gate, and that supplied adult dependents' income counts toward no return. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the role and filing-status invariants with Hypothesis State seven properties of supplied tax-unit roles and filing statuses (partition, supplied roles hold, age-ordering fallback, supplied statuses hold, equivalence with the explicit-pin workaround, switch-off locality, error contract) and check each over generated households. Adds hypothesis to the dev extra; uv.lock regenerated with CI's uv 0.12.13 so it changes only by that package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Guarantee each abolition property case holds its target status Independent review of d8b02c1 found the switch-off-locality property could generate no unit supplied with the abolished status, so it never exercised the re-derivation it claims to check. Each example now holds one forced target unit beside other supplied units. Also narrow the docs and changelog: a dependent's income is left out of the claiming return's gross income, but the deductible share of a dependent's self-employment tax still lowers the filer's AGI through self_employment_tax_ald. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Honor supplied tax-unit roles only; compute filing status Filing status is a policy calculation, so the model computes it from the household's facts, as on main. Supplied data may set who is head, spouse or dependent, but not the filing status. - filing_status.py returns to main's formula; no supplied status is read. - Remove filing_status_input and its YAML tests. The Populace build's filing_status_input column now matches no variable, so the loader skips it. - Revert the romney remove_head_of_household cases, which tested only how the repeal re-derived a supplied status. - Keep tax_unit_role_input, tax_unit_roles_supplied and the head/spouse formulas composed with #9630's inference. - Unit tests: drop the supplied-status tests; add a dataset test showing a filing_status_input column that disagrees with the rules is not read, in the data year and an extended year. - Property tests: roles only. Filing status is joint exactly when there is a spouse; the role input matches pinning the role flags (neither side pins filing_status); a new property composes supplied roles with input dependents, against an oracle that skips them as #9630 does. - Microsim test: check the build's roles only. - Docs: roles only, the fallback as it stands after #9630, and dependents' above-the-line deductions after #9801. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Describe the role fallback with #9630's all-dependent case The age-ordered fallback skips adults input as tax unit dependents unless every adult in the unit is one, in which case every adult stays a candidate. Say so in tax_unit_role_input's documentation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Rewrite the changelog fragment for roles only Per Max's ruling d318, the dataset supplies tax-unit roles but not filing status, so the fragment no longer says filing_status_input sets filing_status. It drops the 2024 aggregates measured with supplied filing status, which no longer describe this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Let a tax unit with no members fall back instead of raising The loader declares every tax unit in a dataset's tax_unit_id column, members or not, and all() is True over no members. So tax_unit_roles_supplied counted a memberless unit as supplied with no HEAD and raised, where main gives the unit no head. It now needs at least one supplied member. The certified default build has no memberless units, so its results do not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: clarify filing status input overrides --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
is_tax_unit_headpicks the oldest non-child in the tax unit, andis_tax_unit_spousepicks the next oldest adult. Neither reads an inputis_tax_unit_dependent. So a person the caller explicitly marks as a dependent can become the filer or the filer's spouse:This PR makes head and spouse inference skip people input as tax-unit dependents. The fallback: if a tax unit has no adult who isn't an input dependent, every adult stays a candidate, exactly as before. Without dependent inputs, the rule is unchanged.
Changed expectations (CI found these at 9b2c416)
Two existing tests had an 18-year-old input dependent whom main inferred as the filer's spouse, so the unit filed jointly. With this fix the parent files alone as head of household:
ma/dta/tcap/tafdc/integration.yamlCase 2: the dependent care deduction is no longer halved for a joint filer. Countable earned income is 7,600 × 50% − 528 = 3,272 for the year, times 6/12 after the full disregard, so 1,636 (was 1,768). TAFDC is 6,439 (was 6,307).contrib/congress/afa/afa_other_dependent_credit.yaml: the head-of-household higher threshold is 300,000, not the joint 400,000. The "without limited phase-out" case now uses 290,000 of wages to keep its intent. The "fully phased out" case's phase-out is ceil(150,000 / 1,000) × 50 = 7,500.Reproduction
Period 2015, AR. People: head 40, spouse 40, parent 70 with
is_tax_unit_dependent: true. All three are in one tax unit, with explicit marital units and no role inputs.is_tax_unit_headis_tax_unit_spouseage_headfiling_statusHouseholds shaped exactly like the web app's payloads (2026, TX, primary earner $50,000), federal
income_tax:Who this reaches
Findings below come from code read on each repo's
origin/mainfor this PR.Household.addChildsetsis_tax_unit_dependent: {year: true}on every US dependent (app/src/models/Household.ts:1105-1117). No runtime code setsis_tax_unit_headoris_tax_unit_spouse. Age is an editable basic input for every person, andHouseholdValidation.tsonly checks that it is present. The v1 API (policyengine-api @ aac95d3b) addsNoneplaceholders for formula variables, so the roles stay computed, and then buildsSimulation(situation=household)(policyengine_api/country.py:542). Result: any app user who gives a "dependent" an age of 18 or over hits this bug. Default households (children aged 10) are unaffected.calculate_householdpasses caller dicts through verbatim and never adds role variables (src/policyengine/tax_benefit_models/us/household.py:99-131). Callers who flag an adult dependent are affected. This includes the docstring pattern (head flagged, dependent flagged, spouse not flagged), where an older dependent becomes the spouse. It pins policyengine-us 2.2.1, which has the same formulas.libs/household-api/policyengine_household_api/country.py:885-911).tests/policy/baseline/partners/). Unchanged. Across 143 files and 620 cases, 26 adults are input as dependents. Every case containing one also inputsis_tax_unit_headandis_tax_unit_spouse, so neither formula runs for them.DEFAULT_DATASET. The pinnedpopulace_us_2024.h5(sha2566496cc43…, matchingDEFAULT_DATASET_SHA256) has nois_tax_unit_*column in any table. Its person table carriestax_unit_role_inputinstead, and that is not a policyengine-us variable on main. With no stored dependent input, the helper returns the same~is_childmask as before.Prior reports: #9132 worked around this in the childless EITC age test ("an older adult dependent is itself classified as the unit 'head'"). #9612 lists it as a pre-existing issue.
Design
is_tax_unit_dependent's formula is~is_tax_unit_head & ~is_tax_unit_spouse. So head and spouse can't callperson("is_tax_unit_dependent", period): when the flag isn't input, that call raisesCycleError(head → dependent → head).The new helper
head_or_spouse_candidates(_head_or_spouse_candidates.py) reads the stored value withperson.simulation.get_array("is_tax_unit_dependent", period). That core call is branch-aware and never runs a formula (policyengine_core/simulations/simulation.py:1233-1241).~is_child, as before.test_computed_dependents_do_not_change_rolescovers this.Invariants
These hold for every input and are checked in
tests/core/test_tax_unit_role_inference.py. That file enumerates every tax unit of up to three people drawn from ages {10, 18, 40, 70} × dependent input {true, false}: 584 units, including age ties and the age-18 boundary.falseinputs, roles are identical to the previous oldest-adult rule.is_tax_unit_dependentreturns its input unchanged.is_tax_unit_dependentbefore head and spouse works, and gives the same roles when head and spouse are recomputed.The invariant test fails on main in checks 1, 3 and 6, and 5 of the 9 new YAML cases fail on main. The rest are controls that pass on both.
Tests
household/demographic/tax_unit/is_tax_unit_head.yamlandis_tax_unit_spouse.yaml:falseinputs, a lone adult input as a dependent, and every adult input as a dependent.tests/core); results to follow in a comment.Overlap with #9617
#9617 (dataset-supplied
tax_unit_role_input) edits the same lines. The two compose: its age fallback (oldest_adult/next_oldest_adult) should rankhead_or_spouse_candidates(person, period)instead of~is_child. Whichever PR merges second takes that one-line change per formula.axiom: n/a: household role inference (which input person is the filer), not a legal provision
🤖 Generated with Claude Code