Repository navigation
Conversation
is_tax_unit_spouse dropped the spouse whenever any member of the tax unit had is_separated set, so a separated adult dependent (or any other separated member) turned a married couple's return into head of household or single. Separation under 26 U.S.C. 7703 concerns the filer's own marriage, and filing_status and head_of_household_eligible already limit it to the head and spouse. The spouse is now dropped only when the head or the would-be spouse is separated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Hypothesis test (plus a seeded population) draws tax units of one to five people with tied ages, children and separated members, with no role inputs, input dependents or an input head. It checks that there is at most one spouse, never the head or a child; that head and spouse match a pure-Python statement of the rule; and that flipping the separation of anyone but the head and the would-be spouse never changes the spouse. New YAML cases cover an input head with a separated dependent, the all-dependent fallback with a separated third adult, and a younger separated adult beside an older non-separated one. The filing-status case now gives the spouse wages, so it exercises the income main dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR Review: Ignore a dependent's separation when inferring the tax unit spouse (26 U.S.C. 7703)PR #9932 (author: MaxGhenis, head SummaryWhen roles are not supplied, Is it still needed? Yes. After #9617 (merged 2026-10-08), Does it conflict? Yes, in two ways. GitHub reports the PR as The logic itself is correct and well tested:
Source documents
Critical (must fix)1. After the rebase onto
Should address1. Two
Suggestions1. Pin or remove the member-order dependence the PR introduces for tied ages.
2. Use one reference implementation for the two Hypothesis files after the rebase.
3. Give the variable a
4. Refresh the PR body after the rebase.
Verified correct
Out of scope (not counted)
Axiom line assessment
CI statusAll 35 checks pass on
These runs predate #9617. GitHub now reports the PR as Branch status
Verdict: REQUEST_CHANGES |
DTrim99
left a comment
There was a problem hiding this comment.
The fix is still needed: main's age-order fallback keeps the unit-wide is_separated gate for households without supplied roles. The law supports gating only on the head and the would-be spouse (7703(a)(2), Reg. 1.7703-1(a), and 152(b)(2)), and the new tests are functional. One critical (details in the comment above): the branch conflicts with #9617, and with the conflict resolved as the PR body prescribes, 2 of main's 4 role property tests fail, because _age_rule still encodes the any-member gate. Rebase, then update _age_rule and docstring invariant 3; all 4 then pass. One should item: reword tax_unit_role_input.py:18 and docs/usage/microsimulation.md:155-157, which still describe the old rule.
Summary
is_tax_unit_spousedropped the spouse whenever any member of the tax unit hadis_separatedset, so a separated adult dependent (or any other separated member) turned a married couple's return into head of household or single. Separation under 26 U.S.C. 7703 concerns the filer's own marriage ("an individual legally separated from his spouse ... shall not be considered as married"; (b) "such individual's spouse is not a member of such household").filing_statusandhead_of_household_eligiblealready limit separation to the head and spouse (#9630 excluded dependent candidates from the spouse role but left this check).The spouse is now the would-be spouse (the oldest candidate other than the head, as before) unless the head or that person is separated.
Fixes #9620. Found again by the 2026-10-06 invariants audit and confirmed on main
6440b3e604by its independent verifier: ages[40, 38, 18], dependent flags[F, F, T]; setting only the 18-year-old'sis_separatedturned spouse[F, T, F]into[F, F, F].Invariants
is_tax_unit_spouse.Tests (fail before, pass after)
is_tax_unit_spouse.yaml(fail on main, plus two status-quo guards that pass on both): a separated adult dependent; a separated dependent older than the couple; a separated younger non-dependent adult; a separated head; a separated would-be spouse with a younger adult (no promotion); a younger separated adult with an older non-separated one; an input head with a separated dependent; the all-dependent fallback with a separated third adult; two tax units in one household.filing_status.yaml: a couple with $50,000 and $30,000 of wages plus a separated 18-year-old dependent, 2024:JOINT, standard deduction $29,200, taxable income $50,800, tax before credits $5,632, income tax $5,132 after the $500 credit for other dependents. The spouse's wages exercise the income that main dropped.tests/core/test_tax_unit_spouse_separation_invariants.py(Hypothesis plus a seeded population): units of one to five people with tied ages, children, separated members, and in turn no role inputs, input dependents or an input head. Checks: at most one spouse, never the head or a child; head and spouse equal a pure-Python statement of the rule; flipping the separation of everyone but the head and would-be spouse never changes the spouse.Run locally under the shared test lock, the new test files against each code version (
policyengine-core test <file> -c policyengine_usper YAML file; pytest for the property test):6b80a8e447is_tax_unit_spouse.yamlfiling_status.yamlHEAD_OF_HOUSEHOLDvsJOINT), 14 passedtest_tax_unit_spouse_separation_invariants.pyThe 8 cases that pass on main are the pre-existing ones plus two status-quo guards (a separated head; a separated would-be spouse with a younger adult), which pass on both by design.
Impact (real microsimulation)
Default dataset (
populace_us_2024.h5@populace-us-2024-spm-20260909), main6b80a8e447vs this branch, run through the shared lock withrun.py/compare.py. Weighted totals; "records" are sample tax units or people.2025
is_tax_unit_spouse(people)tax_unit_marriedincome_taxstate_income_taxeitcrefundable_ctcnon_refundable_ctcpremium_tax_credithousehold_benefitshousehold_net_incomeis_tax_unit_head,is_separated2026: run queued behind the shared lock; this section is updated when it finishes.
Of the 48 units that gain a spouse, 45 match the dataset's own head-and-spouse pair (
tax_unit_role_input, which main ignores). The other 3 (about 12.5k weighted) have no source spouse: the age rule pairs a never-married parent with an adult child (for example ages 42, 21, 18 separated, 13, 7, where the 21-year-old becomes the spouse). Main left those 3 unpaired only because of the separated member; it already pairs the same households that way when nobody is separated, and #9617 fixes the age rule itself.Why tax rises: on main, the partner who lost the spouse role became a dependent (
is_tax_unit_dependentis~head & ~spousewhen not input), andirs_gross_incomecounts nothing for dependents (not_dependent * ...), so the partner's income left the return. Restoring the spouse files the couple jointly with both incomes. The dataset has 81 sample tax units with a separated person whose source role isDEPENDENT, 45 of them with a sourceSPOUSE(#9617 counts the same 45).Interaction with #9617
#9617 (dataset-supplied tax-unit roles, parked on a decision) edits the same formula and keeps the all-member separation check in its age fallback. A trial merge gives one conflict, the whole formula body; resolve it by computing this PR's gated spouse as
next_oldest_adult, thenwhere(supplied, supplied_spouse, next_oldest_adult). Three #9617 files, and #9617's PR description, describe the old rule and will not conflict, so they need updating by hand at that merge:tests/core/test_tax_unit_role_input_properties.py:_age_ruleand docstring invariant 3 (its comment already says to update the reference when this is fixed);variables/household/demographic/tax_unit/tax_unit_role_input.pydocumentation ("unless any member of the unit is separated");docs/usage/microsimulation.md, the same sentence.Once #9617 lands, the default dataset supplies every role (79,729 heads, 35,592 spouses, 51,000 dependents), so this change no longer moves the default microsim. It still applies to household calculations and to datasets without roles.
Methodology choice the law leaves open (not blocking)
26 U.S.C. 7703 says when an individual counts as married, not which co-resident adult is the filer's spouse; that is PolicyEngine's age-order inference. When the would-be spouse (second-oldest candidate) is separated, this PR keeps main's behaviour of inferring no spouse. #9620 proposed instead excluding separated people from
eligible, which would promote the next-oldest adult.get_rankbreaks ties by position), and so can decide whether the unit has a spouse at all: head 50 with two 38-year-olds, one separated, has a spouse only if the non-separated one is listed first. Main gave no spouse in either order.Related
ky_filing_status.pyaggregatesis_separatedover every member the same way (a single filer with a separated dependent files KentuckySEPARATE); #9924 fixes that.axiom: us:statutes/26/7703#taxpayer_married_under_general_rule encoded-correct. rulespec-us takes legal separation as the taxpayer's own tax-unit input (
input.legally_separated_under_decree_of_divorce_or_separate_maintenance), so no other member's status can enter. Companion testlegal_separation_prevents_married_statusinus/statutes/26/7703.test.yaml.Independent review: in progress (in-session Opus reviewers; Subfleet lanes are unavailable today).
🤖 Generated with Claude Code