Skip to content

Keep ABLE contributions in the saver's credit and apply 6012(a)(1) to every 2026 filer - #10590

Merged
MaxGhenis merged 12 commits into
mainfrom
statute-bugs-savers-filing-2026
Nov 3, 2026
Merged

MaxGhenis merged 12 commits into
mainfrom
statute-bugs-savers-filing-2026

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

We fix two places where PE-US departs from the statute. Where a form and the statute differ, we follow the statute, and we use forms for the gaps the statute leaves. The filing fix also covers 6012(a)(1)(A)(iv)'s claimable-spouse exclusion and (C)(ii).

us-savers-credit-able-contributions-2026 (2026+)

Source. 26 U.S.C. 25B as amended by Pub. L. 119-21 (PDF page 97).

What changed.

  • qualified_retirement_savings_contributions.yaml: we restore able_contributions_person to the 2026 list, which main dropped, and add a 2027 list that holds able_contributions_person only.
  • contributions_cap.yaml: $2,100 from 2027; $2,000 through 2026.
  • New distributions.yaml: the distributions that reduce qualified contributions. Through 2026, retirement_distributions and a new person input, savers_credit_relevant_able_distributions, for relevant ABLE distributions under 25B(d)(2); from 2027 that input alone. The caller applies the testing period, exceptions and spouse rule. PE-US doesn't infer the input from retirement_distributions.
  • We reword the able_contributions_person documentation: the input holds contributions each person made to an ABLE account. Under 25B(d)(1), a person counts only contributions to their own account as designated beneficiary; Arizona and Colorado subtract ABLE contributions in general through able_contributions, which adds it up. PE-US has no separate input for a beneficiary's own contributions, so its saver's credit counts the whole amount.
  • From 2027 savers_credit counts ABLE contributions only, so it falls to 0 for units that contribute only to retirement plans or IRAs. The population data holds no ABLE contributions, so on it the 2027 credit falls to $0. ABLE beneficiaries who contribute to their own accounts still qualify, but we can't see them in the data. PE-US doesn't model the Saver's Match that replaces the credit for other savers.

Witness. 2026: $1,000 IRA + $500 ABLE → qualified contributions $1,500 (main $1,000). 2027: $800 ABLE with $4,000 of IRA/401(k) contributions and $5,000 of retirement distributions → $800 (main $0 after distributions). Single filer, 2027, $3,000 ABLE, 50% rate → savers_credit_person $1,050 (cap $2,100).

Overlap. #10129 (draft) edits the same qualified_retirement_savings_contributions.yaml list (self-employed elective deferrals). Whichever lands second must carry the other's entries into the 2026 list. The 2027 list holds ABLE contributions only either way.

us-filing-requirement-2026-6012a1 (2026+)

Source. 26 U.S.C. 6012(a)(1)(A)–(C); 2026 amounts in Rev. Proc. 2025-32 §4.14 ($16,100 single basic deduction, $1,350 dependent minimum, $450 earned-income addition, $2,050 unmarried age addition). No parameter changes.

What changed in tax_unit_is_required_to_file when standard_deduction_rule.applies is false:

  • No positive-income gate. A married individual filing separately has a zero (A) threshold and no exception, so that individual must file even at $0.
  • The (A)/(B) threshold (basic standard deduction, age-only additions, "at least") applies to every filer, including a claimable one. For that filer basic_standard_deduction already holds the 63(c)(5) limited amount.
  • The formula adds (C)(i)(II), gross income over the standard deduction, for claimable filers and keeps (C)(i)(I), the unearned income test. They no longer replace (A).
  • (A)(iv)'s claimable-spouse exclusion. "Clause (iv) shall not apply if ... any other taxpayer is entitled to an exemption for such spouse under section 151(c)." On a JOINT return whose head (head_is_dependent_elsewhere) or spouse (spouse_is_dependent_elsewhere) another taxpayer can claim, the other spouse loses (iv) and has no exception. That spouse must file when their own gross income (person-level irs_gross_income) equals or exceeds the exemption amount. From 2026 that amount stays at $0, so every such couple must file. When another taxpayer can claim both, both lose (iv). The separate-return half needs no code: a SEPARATE unit already has no exception.
  • (C)(ii). The (A) exception "shall not apply to any individual ... for whom the standard deduction is zero under section 63(c)(6)". When separate_filer_itemizes is true (PE-US's only 63(c)(6) path), the threshold falls to the exemption amount for any filing status, so the age addition no longer revives an exception.
  • We leave 6012(f), 2018 to 2025, unchanged.

Limits.

  • (iv) applies "only if such individual and his spouse, at the close of the taxable year, had the same household as their home". PE-US has no year-end household input, and cohabitating_spouses (no default) can't stand in. PE-US treats the spouses of a joint return as sharing a household at year end, since PE-US puts a tax unit in one household.
  • The (iv) comparison uses each spouse's irs_gross_income alone. policyengine_us/tools/section_911.py gives the section 911 gross exclusion per tax unit only, so PE-US doesn't add a spouse's own section 911 exclusion to that spouse's own gross income. The unit-level tests still count it.

Witnesses (2026).

  • Separate filer with $0: required (main: not).
  • Blind single claimable filer with $17,000 of wages: required, since $17,000 ≥ limited basic $16,100 and blindness doesn't count (main: not, against $18,150).
  • Single claimable filer with exactly $16,100 of wages: required (main: not).
  • Joint return with a claimable spouse at exactly $32,200: required (main: not).
  • Joint return, head aged 30 with $10,000 of wages, claimable spouse aged 20 with no income: required, since the head's own $10,000 ≥ $0. With the spouse not claimable: not required ($10,000 < $32,200).
  • Joint return with no income and a claimable spouse, or a claimable head: required ($0 ≥ $0).
  • With separate_filer_itemizes on a head of household input aged 70 and $1,000 of wages, PE-US requires a return, since $1,000 ≥ $0. Under 63(c)(6)(A), only a married individual filing separately loses the standard deduction this way, and that individual already has no exception. PE-US's flag has no filing-status gate.

Witnesses (2017, (a)(1), exemption amount $4,050). Joint return with a claimable spouse and no other income: $3,000 of head wages → not required ($3,000 < $4,050); $5,000 → required. With $3,000 of head wages and $2,000 of spouse wages → not required. Once the head loses (iv), the head must file only if the head's own gross income, not the couple's $5,000, reaches $4,050.

Years. The same code path covers taxable years before 2018, when 6012(a)(1) also governed. No existing pre-2018 test changes, and we add four 2017 cases, one of them #10297's (C)(i)(I) joint case (Overlaps).

Overlaps. #10129 shares the saver's credit contributions list (above). #10297 landed first, and this branch merges it. Its 2026 joint case, a claimed spouse aged 65 with $2,000 of interest, expected no return, but under (A)(iv) the head must file. So we run that (C)(i)(I) check in 2017, when the exemption amount came to $4,050, and add a 2026 case with the same inputs that expects true. #10579 changes basic_standard_deduction.py and standard_deduction.py, which this formula reads, so we rerun tax_unit_is_required_to_file.yaml on whichever of the two lands second.

We update these tests and invariants:

  • tax_unit_is_required_to_file.yaml: Cases 19, 28 and 29, and eight new (A)(iv) and (C)(ii) cases.
  • tax_unit_is_required_to_file_dependents.yaml: one new at-threshold case, and Follow the statute where IRS forms differ: refundable CTC housing deduction, joint filing test, EITC trust royalties, former 32(c)(1)(F) #10297's (C)(i)(I) joint case in 2017 and 2026.
  • test_filing_requirement_invariants.py: property 7 now covers (iv), and the new test_joint_return_properties includes anchors with a claimable head as well as a claimable spouse.
  • test_filing_requirement_dependent_invariants.py and test_disability_filing_invariants.py.
  • savers_credit_qualified_contributions.yaml: 2023, 2026 and 2027 cases for the distribution list.

impact: we estimate these changes with the population data:

  • 2026: 55 tax units newly must file (96,270 weighted), all married filing separately with $0 of gross income. 28 of them (37,542 weighted) start filing and leave Medicaid's non-filer rules, and one person (0.003 weighted) loses Medicaid. No dollar output moves. Pair.
  • 2027: 55 tax units newly must file (96,816 weighted), and two people (6.0 weighted) lose Medicaid. The saver's credit falls from $1.57 billion to $0. Federal income tax rises $1.29 billion, state income tax falls $1.4 million, and household net income falls $1.29 billion. Pair.

axiom: TheAxiomFoundation/rulespec-us#1696 and TheAxiomFoundation/rulespec-us#2011 queued

🤖 Generated with Claude Code

Pub. L. 119-21 preserves ABLE contributions by the designated
beneficiary in the section 25B credit, which main dropped from 2026.
From 2027 the credit covers ABLE contributions only, with a $2,100 cap,
and 25B(d)(2) reduces them by relevant ABLE distributions rather than
retirement-plan distributions; a new person input records those.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6K4grkKNkfjkW6hbaN
From 2026 section 6012(a)(1)(A) excuses a return only when gross income
is less than the exemption amount plus the basic standard deduction. With
a zero exemption amount, a married individual filing separately has no
exception, so even $0 of gross income requires a return; main kept a
positive-income gate. Subparagraph (C) withdraws the (A) exception from a
filer who can be claimed in further cases; it does not replace (A), so a
claimable filer meets the (A) threshold too, with the section 63(c)(5)
limited basic deduction and the age-only addition. Tests and invariant
references that endorsed the zero-income exemption or the dependent
bypass are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6K4grkKNkfjkW6hbaN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6K4grkKNkfjkW6hbaN
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: afdeb924cce91439870ae217b00f0604415503fb

Per-item status

  • us-savers-credit-able-contributions-2026: fixed.
    • qualified_retirement_savings_contributions.yaml: 2026 list restores able_contributions_person; a 2027 list holds it alone.
    • contributions_cap.yaml: 2027-01-01: 2_100.
    • New retirement_saving/distributions.yaml; savers_credit_qualified_contributions.py reads it.
    • New input savers_credit_relevant_able_distributions.py.
    • able_contributions_person.py: documentation now limits it to the designated beneficiary's own contributions.
  • us-filing-requirement-2026-6012a1: fixed in part.
    • Gate removed: tax_unit_is_required_to_file.py:104-106.
    • (A)/(B) for every filer, with (C)(i)(II) added: :117-120.
    • Left as is: (C)(ii) and (A)(iv)'s claimable-spouse exclusion. The spec names them but its text is cut off, and the statute couldn't be fetched here (law.cornell.edu, govinfo and uscode.house.gov were all refused by the proxy). To finish them, the hub needs 6012(a)(1)(C)(ii) and (A)(iv) verbatim, plus the spec's "input limitation recorded below".

Ownership checks: neither branch existed; no open PR on the same change.

Tests (failing first, then passing; one file per process)

  • savers_credit_qualified_contributions.yaml: 3 failed on main → 6 passed. savers_credit_person.yaml: 2 failed on main → 18 passed.
  • savers_credit_eligible_person.yaml 4, savers_credit_potential.yaml 3, savers_credit_credit_limit.yaml 2: all passed. tests/test_federal_credit_limit_order.py 17 passed.
  • tax_unit_is_required_to_file.yaml: 3 failed on main → 39 passed. tax_unit_is_required_to_file_dependents.yaml: 1 failed on main → 56 passed.
  • test_filing_requirement_invariants.py 2 passed; test_filing_requirement_dependent_invariants.py 5 passed; test_disability_filing_invariants.py 1 passed.
  • Other readers, all passed: wftca.yaml 4, basic_standard_deduction.yaml 26, irs/tax_unit/tax_unit_is_filer.yaml 15, irs/tax_unit_is_filer.yaml 4, eitc_filer_gate.yaml 3, section_911_state_eitc.yaml 6, files_return_for_aca_ptc.yaml 5. Python: test_foreign_earned_income_exclusion_leaves.py 25, test_dependent_worksheet_earned_income_invariants.py 1.

Hand computations

  • 2026 blind single claimable filer, $17,000 wages: limited basic = min(16,100, 17,000 + 450) = 16,100; age additions 0; 17,000 ≥ 16,100 → required. Standard deduction 16,100 + 2,050 = 18,150 (unchanged).
  • 2026 claimable single filer, $16,100 wages: limited basic 16,100; 16,100 ≥ 16,100 → required.
  • 2027 saver's: min(3,000, 2,100) × 50% = 1,050. 2026: min(3,000, 2,000) × 50% = 1,000.

Mutants (each in its own process, code restored after)

  • S1, ABLE dropped from the 2026 list: 1 + 1 failed. Killed.
  • S2, 2027 cap removed: 1 failed. Killed.
  • S3, 2027 distributions back to retirement_distributions: 2 failed. Killed.
  • F1, positive-income gate restored: 1 failed. Killed.
  • F2, the dependent bypass restored: YAML 2 + 1 failed, dependent invariants 1 failed. Killed.

Process note: my first F2 run was invalid. git checkout of the not-yet-committed variable file put it back to main's version. I re-applied the change, committed it (b99a7e813c), re-ran all filing tests, and then re-ran F2. The results above are from the valid run.

CI

Draft, not run.

Judgment calls

  • The code comments and tests replaced cite two earlier internal decisions on these points. Per the statute-first ruling and this verified spec, I took the spec's reading and removed the decision ids from the text I rewrote. The hub should confirm the 2026-10-19 ruling supersedes them.
  • The pre-2018 years share the (a)(1) path, so a claimable filer from 2013 to 2017 now meets the (A) threshold too. No existing test covers those years.
  • savers_credit_relevant_able_distributions defaults to 0, so from 2027 qualified contributions aren't reduced unless it's supplied. That matches "don't infer it from retirement_distributions".
  • The spec says to count only the designated beneficiary's contributions. I did that by documenting able_contributions_person, whose only reader is the saver's credit list, rather than adding another input.
  • Overlaps: Count only elective deferrals of self-employed plan contributions in the saver's credit #10129 shares the contributions list; Follow the statute where IRS forms differ: refundable CTC housing deduction, joint filing test, EITC trust royalties, former 32(c)(1)(F) #10297 shares the filing variable (another block) and test_filing_requirement_dependent_invariants.py.

Proposed follow-up

  • Filing requirement: 6012(a)(1)(C)(ii) and (A)(iv) claimable-spouse exclusion — encode both once their text is in hand. (A)(iv) needs a year-end same-household input for joint filers.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

Under 6012(a)(1), clause (iv) does not apply when another taxpayer is
entitled to an exemption for the spouse, so on a joint return the other
spouse has no exception and must file once their own gross income reaches
the exemption amount ($0 from 2026). And (C)(ii) withdraws the (A)
exception when section 63(c)(6) zeroes the standard deduction, which the
age addition could otherwise revive. 6012(f), 2018 to 2025, is unchanged.

Adds hand-computed YAML cases for 2026 and 2017 and Hypothesis properties
for joint returns, and drops an internal id from a comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6KWdpQsFeBJEmuZXhx
The comments keep their meaning; the ids point to records outside the
repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6KWdpQsFeBJEmuZXhx
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 374e9a93ca14226a877d43941e6912e20cacb7bb (two commits on top of afdeb924, which hadn't moved). The PR is still a draft.

What the item asked, and how it's resolved

  1. Code (policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py, (a)(1) branch only; 6012(f) untouched)

    • (C)(ii), :108-116: no_exception = separate | separate_filer_itemizes (:113). With either one, the threshold is the exemption amount, so the age addition no longer revives an exception. The comment quotes "to any individual ... for whom the standard deduction is zero under section 63(c)(6)".
    • (A)(iv) exclusion, :118-151: on a JOINT unit, the head loses (iv) when the spouse is claimable, and the spouse loses it when the head is claimable (loses_joint_exception, :138). Both lose it when both are claimable. A spouse who loses (iv) must file when their own gross income is at least the exemption amount (own_gross_income_at_least_exemption, :148). That result is OR'd into the (a)(1) result (:164). The comment quotes "shall not apply if for the taxable year ... any other taxpayer is entitled to an exemption for such spouse under section 151(c)". The separate-return half needs no code, and the comment says so.
    • Own gross income: person-level irs_gross_income alone. tools/section_911.py gives only a tax-unit amount (section_911_gross_exclusion reads the tax-unit variables foreign_earned_income_exclusion_gross and foreign_earned_income_exclusion), so no per-spouse section 911 amount exists. The comment says this.
    • Year-end household: the comment at :129-132 says the spouses of a joint return are treated as sharing a household, since PE-US has no year-end household input and puts a tax unit in one household.
    • Docstring (:20-24) now describes both rules.
    • Ids removed: "(open as decision d1395)" at the old :130-131 (now :179). I searched every file the PR changes for other decision ids and removed four more. They were in comments only, and the meaning is unchanged:
      • test_filing_requirement_dependent_invariants.py:38, :64 (d1395);
      • tax_unit_is_required_to_file_dependents.yaml:8, :405 (d1395);
      • tax_unit_is_required_to_file.yaml:296, :317 (d1403).

    None remain; the only other d+digits matches are hashes in uv.lock, which the PR doesn't change.

  2. YAML: 8 new cases in tax_unit_is_required_to_file.yaml, arithmetic in each comment.

    • :603: 2026 JOINT, head 30 with $10,000 of wages, spouse 20 claimable, no income → required. Limited basic deduction min(32,200, max(1,350, 10,000 + 450)) = 10,450; 10,000 < 10,450, but the head loses (iv) and 10,000 ≥ 0. Also asserts basic_standard_deduction: 10_450.
    • :630: same, spouse not claimable → not required (10,000 < 32,200).
    • :651: 2026 JOINT, no income, spouse claimable → required (0 ≥ 0).
    • :674: 2026 JOINT, no income, head claimable → required. It sets tax_unit_role_input and asserts head_is_dependent_elsewhere: true. Without the roles, the older adult became head and the case tested the same branch as :651 (mutant (b) exposed this).
    • :700: 2017 JOINT, head $3,000 of wages, spouse claimable → not required. Exemption amount 4,050 (read from gov.irs.income.exemption.amount), and 3,000 < 4,050. (A): 2 × 4,050 + 3,350 = 11,450. (C)(i)(II): 3,000 is not over 3,350 + 4,050 = 7,400.
    • :728: same with $5,000 → required (5,000 ≥ 4,050). Without (iv): (A) is 13,450 and (C)(i)(II) is 9,400, so this case turns only on (iv).
    • :754 (extra, for mutant (d)): 2017 JOINT, head $3,000 and claimable spouse $2,000 of wages → not required. Own 3,000 < 4,050; combined 5,000 would meet it. (A) 13,450; (C)(i)(II) 9,400.
    • :784: 2026 HEAD_OF_HOUSEHOLD aged 70 with separate_filer_itemizes and $1,000 of wages → required under (C)(ii) (1,000 ≥ 0). At afdeb92 it was 1,000 < 0 + 2,050. Also asserts basic_standard_deduction: 0.
    • Existing cases: none contradicted the statute. All kept unchanged (39 in this file, 56 in the dependents file).
  3. Hypothesis (policyengine_us/tests/core/test_filing_requirement_invariants.py)

    • Property 7 (:442): its claimable-unit reference now adds "2026 and JOINT → required". Without that it would have failed, since it encoded the old rule.
    • New test_joint_return_properties (:501) draws JOINT units with any mix of age, blindness, claimability of each spouse, income (often at a boundary) and separate_filer_itemizes. It reuses units(), which now takes an optional status argument. It sets separate_filer_itemizes with set_input, so the shared simulate helper, which Follow the statute where IRS forms differ: refundable CTC housing deduction, joint filing test, EITC trust royalties, former 32(c)(1)(F) #10297 also edits, is unchanged.
      • Property 8: in 2026, every JOINT unit with either spouse claimable is required. It can't pass trivially: an anchor (claimable spouse, $0) and an assertion guarantee a claimable row with gross income under $1,350. The formula at afdeb92 excused every such row, because both the limited (A) threshold and the limited standard deduction are at least $1,350, and unearned income ≤ gross income < $1,350.
      • Property 9: a JOINT unit nobody can claim, with separate_filer_itemizes false, matches frozen_joint_reference (:456). That function freezes the afdeb92 formula from the published amounts: 2026 ≥ 32,200 + 1,650 × aged; 2025 > 31,500 + 1,600 × (aged + blind). It can't pass trivially: anchors sit at $32,199 and $32,200, incomes are drawn at boundaries, and the test asserts that both answers occur among the checked rows.
      • Both properties cover 2025 and 2026 only. Pre-2018 standard deduction amounts aren't in the parameters (see the follow-ups), so a reference built from published 2017 amounts wouldn't match.
  4. Mutants: each ran in its own process, with the code restored afterwards (verified by git diff).

    Mutant YAML (tax_unit_is_required_to_file.yaml) test_filing_requirement_invariants.py
    (a) (iv) exclusion removed 4 failed (:603, :651, :674, :728) 2 failed (test_filing_requirement_properties, test_joint_return_properties)
    (b) exclusion only when the head is claimable 3 failed (:603, :651, :728) 1 failed (test_joint_return_properties), before the role fix
    (c) (C)(ii) withdrawal removed 1 failed (:784) passed: only joint units are drawn with separate_filer_itemizes
    (d) combined gross income in the (iv) comparison 1 failed (:754) passed: with a $0 exemption amount, own and combined income give the same result
    extra (e): exclusion only when the spouse is claimable 1 failed (:674) not run

    (b)'s first YAML run failed 4 cases, :674 among them. That exposed the role problem; after the fix, (b) fails 3 and (e) fails :674.

  5. Readers of tax_unit_is_required_to_file at this head:

    • Direct:
      • variables/gov/irs/tax_unit_is_filer.py:40;
      • variables/gov/irs/credits/earned_income/eitc.py:25, which composes the filer condition without tax_unit_is_filer;
      • tools/state_eitc_helpers.py:10, used by dc_eitc_with_qualifying_child, dc_eitc_without_qualifying_child, co_eitc, il_eitc, and wa_working_families_tax_credit with its _age_expansion_eligible and _maximum_qualifying_income.
    • Through tax_unit_is_filer:
      • aca/ptc/files_return_for_aca_ptc.py:29;
      • hhs/medicaid/income/medicaid_uses_non_filer_rules.py:13;
      • hhs/medicaid/income/medicaid_tax_dependent_exception_living_with_both_parents.py:47;
      • states/nm/hca/premium_assistance/nm_premium_assistance_mih_eligible.py:41.
    • medicaid_person_is_required_to_file.py (read in full, not changed) is a separate Pub. 501 Table 2 implementation for each person. It treats a married person as SEPARATE and anyone else as SINGLE. It applies ">" tests for unearned income, earned income and gross income against the limited deduction, counting age and blindness. For a married person whose tax unit has separate_filer_itemizes, it uses gross income ≥ filing_requirement.dependent.spouse_itemizes_threshold ($5 from 2014).
      • It repeats 6012 logic that now differs from 2026. For (C)(ii), the tax-unit rule now requires a return at any gross income, $0 included, for any filing status. The Medicaid rule keeps $5, and only for a married person.
      • It has no joint-return concept, so (A)(iv) has no counterpart there.
      • Its ">" at equality and its blindness addition already differed from (a)(1) before this change.
      • Reported only.
  6. Tests run (one file per process):

    • tax_unit_is_required_to_file.yaml: 47 passed (39 existing + 8 new; rerun after the role fix);
    • tax_unit_is_required_to_file_dependents.yaml: 56 passed;
    • test_filing_requirement_invariants.py: 3 passed;
    • test_filing_requirement_dependent_invariants.py: 5 passed;
    • test_disability_filing_invariants.py: 1 passed;
    • wftca.yaml: 4;
    • basic_standard_deduction.yaml: 26;
    • tax_unit/tax_unit_is_filer.yaml: 15;
    • irs/tax_unit_is_filer.yaml: 4;
    • eitc_filer_gate.yaml: 3;
    • section_911_state_eitc.yaml: 6;
    • files_return_for_aca_ptc.yaml: 5;
    • test_foreign_earned_income_exclusion_leaves.py: 25 passed;
    • test_dependent_worksheet_earned_income_invariants.py: 1 passed;
    • also code_health/test_source_conflicts.py: 103 passed.

    No failures. ruff format --check and ruff check are clean on the changed Python files. No full suites and no microsimulations.

  7. Body updated through the REST PATCH and read back:

CI

The Pull request run at 374e9a93 completed with conclusion skipped, because the PR is a draft: https://github.com/PolicyEngine/policyengine-us/actions/runs/41477928207. No CI evidence exists for this head yet.

Judgment calls

  • Ownership window: the last commit (afdeb92, 09:23 UTC) came from the previous session, which posted its hand-off at 09:23:51. The brief names that head. I worked locally and posted the pick-up comment and pushed only after 16:00 UTC, once 6 hours had passed. The head hadn't moved.
  • (iv) is applied only to filing status JOINT.
  • (C)(ii) threshold: one exemption amount (not times the head and spouse count), without the age addition, for any status. In 2026 a unit with separate_filer_itemizes and $0 of gross income is therefore required (0 ≥ 0), as a SEPARATE unit is.
  • (C)(ii) under 6012(f), 2018 to 2025, is left alone, as the item says.
  • Comment quotes are kept to 25 words or fewer, using ellipses. Every quote comes from the brief's appendix; nothing is quoted from memory.
  • Extras: I added the combined-income YAML case (:754), because the 2017 pair alone can't kill mutant (d), whose claimable spouse has no income. I added mutant (e) to show that :674 covers the claimable-head branch.

Proposed follow-up issues

  • Pre-2018 standard deduction amounts are missing. gov.irs.deductions.standard.amount starts at 2018-01-01, so 6012(a)(1) years before 2018 read the TCJA amounts (joint $24,000 in a 2017 simulation). The 2017 cases here avoid the cap. The pre-TCJA values themselves are unverified here and need the Rev. Proc.
  • Align medicaid_person_is_required_to_file with 6012(a)(1) from 2026. Its Table 2 logic keeps ">" at equality, blindness, and a $5 spouse-itemizes threshold, while the tax-unit rule now follows (a)(1)(A) and (C)(ii). Decide which governs Medicaid dependent income from 2026.
  • A year-end household input for 6012(a)(1)(A)(iv). (iv) depends on whether the spouses shared a home at the close of the year. PE-US assumes they did for every joint unit; an input (or a default for cohabitating_spouses) would let separated joint filers lose (iv).

Generated by Claude Code

@MaxGhenis
MaxGhenis marked this pull request as ready for review November 2, 2026 16:07
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: REQUEST_CHANGES
374e9a9
METHODOLOGY_CHOICE: yes

Independent review, round 1. The head hasn't moved past 374e9a93ca. No earlier "Hub review in progress" comment exists, so this review starts from the beginning. The PR picks readings where the law leaves room: it treats a joint unit's spouses as sharing a household at year end, uses separate_filer_itemizes as the only 63(c)(6) path, and makes the 2027 ABLE distributions a caller-supplied input. Both hub readings in force (us:filing-requirement-joint-claimable-spouse, us:filing-requirement-joint-year-end-household) are applied as written.

I found no wrong result in the code at this head. Two P2s need fixing before landing: the overlap note understates #10297, and a documentation change narrows a shared input that Arizona and Colorado also read. The rest are P3s.

Findings

P2-1. Merging #10297 changes one of #10297's expected values; the body predicts only a textual conflict

Where: the PR body, "Overlaps" ("Expect a textual conflict in that test file"), and #10297's tax_unit_is_required_to_file_dependents.yaml case "2026, section 6012(a)(1)(C)(i)(I) adds the additional standard deduction".

Failure scenario: #10297's case is a 2026 JOINT unit: head 40 with no income, and a spouse aged 65, claimed, with $2,000 of interest. It expects false. Under this PR's (A)(iv) exclusion the head loses (iv), and $0 of own gross income meets the $0 exemption amount, so the merged model returns true. That is the hub's ruling, so the merged answer is right and the test is wrong. Whichever PR lands second must change the case.

Run: I merged pr-10297 (22e92cc0) into this head in a scratch tree. Git reported conflicts in three files, not one: tax_unit_is_required_to_file.py, tax_unit_is_required_to_file_dependents.yaml and test_filing_requirement_dependent_invariants.py. Every conflict hunk is comment text, and I resolved each one to #10297's side. Then I ran each file in its own process:

tax_unit_is_required_to_file.yaml                47 passed
tax_unit_is_required_to_file_dependents.yaml     1 failed, 60 passed
  Test '2026, section 6012(a)(1)(C)(i)(I) adds the additional standard deduction':
    tax_unit_is_required_to_file@2026: [1.] differs from 0.0
test_filing_requirement_dependent_invariants.py  5 passed
test_filing_requirement_invariants.py            3 passed

Replacement (body): "#10297 edits the same variable and two of this PR's test files. Merging them conflicts in comments in tax_unit_is_required_to_file.py, tax_unit_is_required_to_file_dependents.yaml and test_filing_requirement_dependent_invariants.py. It also changes one value: #10297's 2026 joint case with a claimed spouse aged 65 and $2,000 of interest becomes required under (A)(iv). Whichever PR lands second must move that (C)(i)(I) check to a filer whom (iv) doesn't reach. One option is a 2026 SINGLE claimed filer aged 65 with $2,000 of interest. That filer isn't required: $2,000 is under the (A) threshold of $1,350 + $2,050 = $3,400, and it isn't over the $1,350 + $2,050 unearned income amount."

P2-2. able_contributions_person's new documentation narrows an input that Arizona and Colorado also read

Where: policyengine_us/variables/household/expense/retirement/able_contributions_person.py:8-12.

Failure scenario: the variable now says it holds only contributions a person made "as the designated beneficiary, to their own ABLE account". But able_contributions (adds = ["able_contributions_person"]) feeds az_529a_able_account_subtraction and co_able_contribution_subtraction, and both subtract ABLE contributions in general, not only a beneficiary's own. Suppose a parent who contributes to a child's ABLE account follows the new documentation and enters 0. That parent's Colorado and Arizona subtractions drop to 0. The earlier hand-off says the variable's "only reader is the saver's credit list"; grep -rn able_contributions_person policyengine_us shows that's false. The PR changes no computed value, but it changes what users are told to enter.

Replacement:

    documentation = (
        "Contributions each person made to an ABLE account. The saver's "
        "credit (26 U.S.C. 25B(d)(1)) counts only contributions to the "
        "person's own account as designated beneficiary; Arizona and "
        "Colorado subtract ABLE contributions through able_contributions."
    )

Another option is a separate saver's-credit input for a beneficiary's own contributions. I'd leave that to a follow-up.

P3-1. A garbled sentence in the body

Where: the body, "(A)(iv)'s claimable-spouse exclusion": "From 2026 that amount is /bin/bash". A shell expanded $0 into this text.

Replacement: "From 2026 that amount is $0, so every such joint return must file."

P3-2. Before 2018, (C)(i)(II) compares gross income with the standard deduction plus the exemption amount; the new comments call that (C)(i)(II)

Where: tax_unit_is_required_to_file.py:78-83 (the threshold for income_over_standard_deduction, which adds exemption_amount), used at :165-168. The new comments at :153-158 and YAML :723, :750 and :779 ("(C)(i)(II): $3,000 does not exceed $3,350 + $4,050 = $7,400") describe that sum as (C)(i)(II).

Failure scenario: the brief quotes (C)(i)(II) as "total gross income in excess of the standard deduction". From 2026 the exemption amount is 0, so nothing changes. In 2017 it is $4,050. Take a single claimable filer with $1,000 of wages and $1,000 of interest. Gross income is $2,000, and the limited standard deduction is $1,350, so the quoted text requires a return. Both main and this head return false, because they compare $2,000 with $5,400.

W1 2017 single claimable 1000w+1000int
  main: tax_unit_is_required_to_file False, standard_deduction 1350.0
  PR:   tax_unit_is_required_to_file False, standard_deduction 1350.0

This is identical on main, so it's a follow-up, not this PR's defect. None of the three new 2017 cases changes under either reading. But the new comments state the wrong amount as the statute.

Replacement (YAML comments): "(C)(i)(II): $3,000 does not exceed the $3,350 standard deduction." Use the same form at :750 and :779 with $5,350. In the follow-up, check 6012(a)(1)(D) for the exemption amount of a filer described in 151(d)(2). I couldn't fetch it; see below.

Follow-up issue: "Pre-2018 (C)(i)(II) adds the exemption amount to the standard deduction". 6012(a)(1)(C)(i)(II) compares gross income with the standard deduction alone. In 2017 a claimable single filer with $1,000 of wages and $1,000 of interest is excused, against the $1,350 standard deduction.

P3-3. Property 8 doesn't catch an exclusion that ignores a claimable head

Where: policyengine_us/tests/core/test_filing_requirement_invariants.py:473 (JOINT_ANCHORS).

Failure scenario: mutant (e), which applies the exclusion only when the spouse is claimable, passes test_joint_return_properties. Only the YAML case at :674 and test_filing_requirement_properties (property 7) kill it. All six derandomized examples miss a low-income unit with a claimable head.

Replacement: add an anchor so property 8 covers both branches.

JOINT_ANCHORS = [
    (tax_unit("JOINT", [adult(30, False), adult(20, False, True)]), False),
    (tax_unit("JOINT", [adult(20, False, True), adult(30, False)]), False),
    ...

and assert that a claimable-head row with income under $1,350 exists, the same way the test already asserts a claimable row with income under $1,350.

P3-4. From 2027, IRA and 401(k) contributions leave the saver's credit, and nothing in the model replaces them

Where: qualified_retirement_savings_contributions.yaml (2027-01-01: [able_contributions_person]).

Failure scenario: from 2027, savers_credit falls to 0 for every unit whose only contributions are to an IRA or a 401(k). grep -rniE "6433|saver.?s.?match" policyengine_us finds no Saver's Match in the model. The body says the 2027 list is ABLE-only, but it doesn't say that the baseline saver's credit falls from 2027. I couldn't fetch 25B as amended to confirm the ABLE-only reading (see below).

Replacement (body, saver's credit "What changed"): "From 2027 savers_credit counts ABLE contributions only, so it falls to 0 for units that contribute only to retirement plans or IRAs. PE-US doesn't model the Saver's Match that replaces it."

Checked and sound

  • The defect at main. I ran this head's test files against main's code, one file per process:

    • tax_unit_is_required_to_file.yaml: 8 failed, 39 passed (Cases 19, 28 and 29, and five new cases: :603, :651, :674, :728, :784);
    • tax_unit_is_required_to_file_dependents.yaml: 1 failed;
    • savers_credit_qualified_contributions.yaml: 3 failed;
    • savers_credit_person.yaml: 2 failed.

    Every one of them passes at this head.

  • Hand values. I recomputed all eight new YAML values from the parameters before reading the code.

    • 2026 JOINT, $10,000 of wages, claimable spouse: limited basic min(32,200, max(1,350, 10,450)) = 10,450. (A) excuses, since 10,000 < 10,450, but the head loses (iv) (10,000 ≥ 0). Required.
    • The same without the claimable spouse: 10,000 < 32,200. Not required.
    • No income, with a claimable spouse, or with a claimable head: 0 ≥ 0. Required.
    • 2017, $3,000: own 3,000 < 4,050 (gov.irs.income.exemption.amount 2017 = 4,050); (A) 11,450; standard deduction 3,350. Not required.
    • 2017, $5,000: 5,000 ≥ 4,050. Required.
    • 2017, $3,000 + $2,000: own 3,000 < 4,050; (A) 13,450; standard deduction 5,350. Not required.
    • 2026 HEAD_OF_HOUSEHOLD aged 70 with separate_filer_itemizes and $1,000: 1,000 ≥ 0. Required.

    All match.

  • (A)(iv) and (C)(ii) code against the brief's quoted statute and the two hub readings.

    • JOINT units, head claimable: the spouse loses (iv).
    • JOINT units, spouse claimable: the head loses (iv).
    • Both claimable: both lose (iv). Witness: 2016 JOINT, both claimable, head $5,000: main False, PR True (5,000 ≥ 4,050).
    • The comparison uses each person's own irs_gross_income.
    • The branch runs only when standard_deduction_rule.applies is false, so 2018 to 2025 is untouched.
    • Applying (iv) only to JOINT units is right. Other statuses carry no spouse in the unit: SEPARATE has no exception anyway, and SINGLE, HOH and SURVIVING_SPOUSE have no spouse member.
    • (C)(ii) lowers the threshold to the exemption amount for every status with separate_filer_itemizes. Witness: 2026 SINGLE aged 70 with the flag and $1,000: main False, PR True.
  • Direction. At every point the change can only turn tax_unit_is_required_to_file from false to true:

    • the positive-income gate is removed;
    • the claimable branch is now a superset of main's (income_over_threshold ORs main's claimable test with the new ones);
    • the (C)(ii) threshold, the exemption amount, is never above basic_threshold.
  • 2017 with Start Vermont's 2017 taxable income from federal taxable income (former 32 V.S.A. § 5811(21)) #10595. I merged Start Vermont's 2017 taxable income from federal taxable income (former 32 V.S.A. § 5811(21)) #10595 (5c25092d) into this head; there were no conflicts. tax_unit_is_required_to_file.yaml passed 47 of 47. With Start Vermont's 2017 taxable income from federal taxable income (former 32 V.S.A. § 5811(21)) #10595 the 2017 JOINT standard deduction is $12,700, against $24,000 here. None of the three 2017 cases moves: their limited deduction (3,350 and 5,350) stays under the cap. Only a larger income shows the difference: with $13,000 of wages the standard deduction is 13,350 at this head and 12,700 merged, and both runs return required. The 2017 aged addition changes ($1,300 to $1,250), but no 2017 case is aged. All three 2017 values are right with either parameter set.

  • Tests at this head, one file per process:

    • tax_unit_is_required_to_file.yaml 47;
    • tax_unit_is_required_to_file_dependents.yaml 56;
    • savers_credit_person.yaml 18;
    • savers_credit_qualified_contributions.yaml 6;
    • test_filing_requirement_invariants.py 3;
    • test_filing_requirement_dependent_invariants.py 5;
    • test_disability_filing_invariants.py 1.

    All passed.

  • Readers, all passed:

    • wftca.yaml 4;
    • basic_standard_deduction.yaml 26;
    • tax_unit/tax_unit_is_filer.yaml 15;
    • irs/tax_unit_is_filer.yaml 4;
    • eitc_filer_gate.yaml 3;
    • section_911_state_eitc.yaml 6;
    • files_return_for_aca_ptc.yaml 5;
    • test_foreign_earned_income_exclusion_leaves.py 25;
    • test_dependent_worksheet_earned_income_invariants.py 1;
    • test_aca_ptc_filing_gate_invariants.py 1;
    • medicaid_person_is_required_to_file.yaml 20;
    • medicaid_uses_non_filer_rules.yaml 11;
    • nm_premium_assistance_mih_eligible.yaml 11;
    • code_health/test_source_conflicts.py 103.
  • Properties 8 and 9.

    • Property 8 can't pass trivially. Its anchor, a claimable spouse with $0 of income, was excused at afdeb92, and mutant (a) fails the test.
    • Property 9 asserts that both answers occur. Its frozen reference matches main's formula for joint units nobody can claim: in 2026 main's gross_income > 0 gate never binds against a $32,200 threshold, and 2025 is main's 6012(f) rule.
  • Mutants, each in its own process on a scratch copy, with git status clean after each:

    Mutant tax_unit_is_required_to_file.yaml test_filing_requirement_invariants.py
    (a) exclusion removed 4 failed 2 failed (both property tests)
    (b) exclusion only for a claimable head 3 failed test_joint_return_properties failed
    (c) (C)(ii) removed 1 failed passed
    (d) combined gross income 1 failed passed
    (e) exclusion only for a claimable spouse 1 failed test_filing_requirement_properties failed
    (f), mine: >= replaced by > in the own-income comparison 2 failed not completed

    The dependents file passed under every mutant. Every mutant is killed.

  • Readers at baseline, all monotone, since the change only adds required units:

    • tax_unit_is_filer can only rise.
    • files_return_for_aca_ptc can only rise, so aca_ptc can rise.
    • nm_premium_assistance_mih_eligible can only rise.
    • Federal EITC (eitc.py:25) and the state EITC helper (eitc_filing_requirement_met: DC, CO, IL, WA) can only rise, for units that take up.
    • medicaid_uses_non_filer_rules can only fall for affected people (more filers).
    • medicaid_tax_dependent_exception_living_with_both_parents can only rise, since parents_file_jointly reads tax_unit_is_filer.
    • So Medicaid eligibility can move either way for the affected households.
  • Medicaid's own rule. medicaid_person_is_required_to_file.py doesn't read tax_unit_is_required_to_file, and this PR doesn't change it. Its Table 2 logic now differs from the tax-unit rule from 2026: $5 against any amount under (C)(ii), and no (iv). This PR doesn't make any Medicaid result through that variable worse. I couldn't fetch 42 CFR 435.603 to say whether it points at 6012(a)(1) (see below).

  • Saver's credit.

    • ABLE is back in the 2026 list.
    • 2018 to 2025 is unchanged.
    • From 2027 the list is ABLE only, with a $2,100 cap, and the distributions parameter switches to the new input.
    • YAML: 2026, $1,000 IRA + $500 ABLE gives $1,500. 2027 gives $800, unaffected by $5,000 of retirement distributions. 2027, $800 − $300 gives $500. Credit at 50% of min(3,000, 2,100) is $1,050.
    • Before 2018 the list extrapolates the 2018 list, ABLE included; that is unchanged from main.
  • Count only elective deferrals of self-employed plan contributions in the saver's credit #10129 (e3f84254): merging it conflicts in qualified_retirement_savings_contributions.yaml, savers_credit_person.yaml and savers_credit_qualified_contributions.yaml. Count only elective deferrals of self-employed plan contributions in the saver's credit #10129 renames self_employed_pension_contributions to self_employed_pension_elective_deferrals in the 2018 and 2026 lists. Whichever lands second must use the new name in the 2026 list. The 2027 ABLE-only list needs no entry for it. The body's note is accurate.

  • Ids: grep -nE "\bd[0-9]{3,6}\b" over every changed file finds nothing.

  • Scope: nothing under .github/ or tests/policy/baseline/partners/ changes. The full Pull request run at this head (41478433071) passed every job, Household API Partners included.

  • Other open work: Zero a separate filer's whole standard deduction and exclude dependent care benefits from wages #10579 (open) zeroes the whole standard deduction for a separate filer whose spouse itemizes. It doesn't edit this PR's files. Under (a)(1) this PR already uses the exemption amount in that case, so the two compose.

  • Body: every other factual sentence matches the code at this head. That includes the witnesses, the 2017 witnesses, the section 911 limit and the year-end household limit. The impact: and axiom: lines are present.

Population (for the impact pair, beyond COMMON)

tax_unit_is_required_to_file, tax_unit_is_filer, files_return_for_aca_ptc, aca_ptc, medicaid_uses_non_filer_rules, medicaid_tax_dependent_exception_living_with_both_parents, medicaid / is_medicaid_eligible, nm_premium_assistance_mih_eligible, eitc, dc_eitc, co_eitc, il_eitc, wa_working_families_tax_credit, savers_credit, savers_credit_qualified_contributions.

Counts worth reporting, 2026 and 2027:

  • JOINT units with head_is_dependent_elsewhere or spouse_is_dependent_elsewhere;
  • units with separate_filer_itemizes;
  • SEPARATE units with zero gross income;
  • 2027 units with IRA or 401(k) contributions and no ABLE contributions.

What I couldn't check

  • Statutes. The proxy here refused law.cornell.edu, govinfo.gov, uscode.house.gov, ecfr.gov, irs.gov and congress.gov. So I didn't fetch 26 U.S.C. 25B, 6012(a)(1), 63(c)(6), 151(d)(2), Pub. L. 119-21 or 42 CFR 435.603.
    • For 6012(a)(1) I relied on the hub's quote in the brief.
    • Unverified:
      • the 2027 ABLE-only scope and the $2,100 cap, including whether the cap is indexed after 2027 (a web search found only secondary sources reporting $2,100 from 2027);
      • whether 6012(a)(1)(D) sets the exemption amount to zero for a filer described in 151(d)(2) before 2018, which would change the 2017 (A) threshold for claimable filers;
      • whether 63(c)(6) can reach a head of household, which the :784 case assumes through separate_filer_itemizes;
      • the 2017 dependent amounts ($1,050 and $350, read from the 2018 parameter values).
  • Population data. I couldn't run microsimulations, so I can't say how many units the change reaches. The impact pair needs to.
  • Mutant (f) property test. Its test_filing_requirement_invariants.py run didn't finish before a background time limit. The YAML file already kills (f).
  • Start Vermont's 2017 taxable income from federal taxable income (former 32 V.S.A. § 5811(21)) #10595 dependents file. I didn't run the merged dependents file. Start Vermont's 2017 taxable income from federal taxable income (former 32 V.S.A. § 5811(21)) #10595 changes no file it reads beyond the 2017 parameters, and that file has no pre-2018 case.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

…r a claimable head

The 2017 (C)(i)(II) comments called the code's pre-2018 threshold, which
adds the exemption amount, the statute's test; the statute compares gross
income with the standard deduction alone, and 6012(a)(1)(D)(ii) zeroes the
exemption amount for a 151(d)(2) individual. No formula change.

able_contributions_person also feeds able_contributions, which Arizona and
Colorado subtract, so its documentation no longer limits it to a
beneficiary's own contributions.

Property 8 passed with the (A)(iv) exclusion applied only for a claimable
spouse; a joint anchor with the claimable adult as head, and an assertion
that such a row exists, now catch that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: e8e8a75323f734b30cd696f5970f414d256d68f6

Round 1 fix round on 374e9a93ca14226a877d43941e6912e20cacb7bb. The head hadn't moved past it; its last push (16:04Z) was the previous hub round, already handed off and reviewed, so I treated it as not someone else's work in progress.

Merge of main

origin/main merged with a merge commit (e40b96ef). No conflicts. #10297 isn't on main, so the conflicts in Finding 1 don't arise here.

Findings

  1. P2, Follow the statute where IRS forms differ: refundable CTC housing deduction, joint filing test, EITC trust royalties, former 32(c)(1)(F) #10297 overlap. The body's Overlaps paragraph now carries the requested text in place of the old Follow the statute where IRS forms differ: refundable CTC housing deduction, joint filing test, EITC trust royalties, former 32(c)(1)(F) #10297 sentences. I checked the numbers against the 2026 parameters: gov.irs.deductions.standard.dependent.amount = 1,350 and aged_or_blind.amount.SINGLE = 2,050. I also ran the suggested replacement filer at this head (2026 SINGLE, claimable, aged 65, $2,000 of interest): tax_unit_is_required_to_file = false. A 2026 joint unit (head 40 with no income, claimable spouse 65 with $2,000 of interest) returns true. I didn't touch Follow the statute where IRS forms differ: refundable CTC housing deduction, joint filing test, EITC trust royalties, former 32(c)(1)(F) #10297.
  2. P2, able_contributions_person documentation. policyengine_us/variables/household/expense/retirement/able_contributions_person.py:8-13 now has the requested text. grep -rnw turns up these readers: able_contributions (adds), az_529a_able_account_subtraction.py:20 and co_able_contribution_subtraction.py:18 (both through able_contributions), and the saver's credit list in qualified_retirement_savings_contributions.yaml. The body's matching bullet is updated too.
  3. P3, $0 in the body. The (A)(iv) paragraph now reads "From 2026 that amount is $0, so every such joint return must file." I wrote the body from a file with a quoted heredoc. In the readback, no $ is followed by anything but a digit.
  4. P3, (C)(i)(II) comments.
    • Code: policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py:157-161. One sentence now says the pre-2018 threshold adds the exemption amount, which (C)(i)(II) doesn't, and that 6012(a)(1)(D)(ii) sets the exemption amount to zero for an individual described in 151(d)(2). I didn't re-fetch (D)(ii); this relies on the hub's 2026-11-02 reading.
    • YAML: tax_unit_is_required_to_file.yaml:723 reads "$3,000 does not exceed the $3,350 standard deduction." Lines 750 and 779 read "$5,000 does not exceed the $5,350 standard deduction."
    • The formula is unchanged.
  5. P3, property 8 claimable head. In policyengine_us/tests/core/test_filing_requirement_invariants.py:478, a new JOINT_ANCHORS row has the claimable adult as head: adult(20, False, True), adult(30, False). I checked the argument order first: adult(age, blind, claimed=False, **income), and tax_unit makes the first adult head. Lines 513-515 add head_claimable, and line 527 asserts that a claimable-head row with gross income under $1,350 exists. The docstring for property 8 (lines 70-72) says so.
  6. P3, 2027 saver's credit. Added to the saver's credit "What changed": "From 2027 savers_credit counts ABLE contributions only, so it falls to 0 for units that contribute only to retirement plans or IRAs. PE-US doesn't model the Saver's Match that replaces it." grep -rniE "6433|saver.?s.?match" policyengine_us finds only numeric matches in CSV and parameter data, nothing that models section 6433. The 2027 list in qualified_retirement_savings_contributions.yaml holds able_contributions_person only.

Mutant (e)

Mutant (e) deletes the is_tax_unit_spouse & head_can_be_claimed clause from loses_joint_exception, so the exclusion applies only when the spouse can be claimed. I ran it in its own process and restored the file afterwards (git diff clean).

  • The old test (copy of HEAD~1), -k joint_return: 1 passed. The mutant survived, as the reviewer found.
  • The new test, -k joint_return: 1 failed, AssertionError: 2026: excused a joint return with a claimable spouse at the property 8 assertion. The mutant is killed.

Tests at e8e8a75 (one file per process)

File Result
tax_unit_is_required_to_file.yaml 47 passed
tax_unit_is_required_to_file_dependents.yaml 56 passed
savers_credit_person.yaml 18 passed
savers_credit_qualified_contributions.yaml 6 passed
az_529a_able_account_subtraction.yaml 11 passed
co_able_contribution_subtraction.yaml 4 passed
test_filing_requirement_invariants.py 3 passed
test_filing_requirement_dependent_invariants.py 5 passed
test_disability_filing_invariants.py 1 passed
code_health/test_source_conflicts.py 103 passed

ruff format left all three changed Python files unchanged, and ruff check passed. I ran no full suites and no microsimulation.

CI

Pull request run 41522520991 at e8e8a753 was still in_progress at 21:48Z, 60 minutes after the push. By then 32 jobs had succeeded, none had failed, and two were still running: Contrib tests / Full Suite - Contrib (other-shard-1) and (other-shard-3). It has no conclusion yet, so it doesn't count as passing.

Judgment calls

  • Closing line. The body keeps impact: and axiom:. The proxy rewrites the closing line "🤖 Generated with Claude Code" to its own footer "Generated by Claude Code" on every PATCH. I tried twice and got the same readback. Apart from that footer, the readback matches my file line for line.
  • Tests list. In the body's "Tests and invariants updated" line, I noted that test_joint_return_properties's anchors include a claimable head.

Proposed follow-up

  • Saver's credit input for a beneficiary's own ABLE contributions. able_contributions_person feeds both the saver's credit (own account as designated beneficiary only, 25B(d)(1)) and the Arizona and Colorado subtractions (ABLE contributions in general). A separate person input for the beneficiary's own contributions would let the saver's credit count only those, while the states keep reading able_contributions.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

25B(d)(2)(A) reaches distributions from any entity that can take (d)(1)
contributions, and ABLE accounts take them through 2026 too, so the 2018
distribution list now holds the relevant ABLE distribution input beside
retirement_distributions. New 2023, 2026 and 2027 cases pin the list on
each side of the 2027 change.

Also correct review text without moving expected values: the (C)(ii)
case now says it pins the model's separate_filer_itemizes rule rather
than law for a head of household; the 2017 (iv) comments name (D)(ii);
the ABLE input documentation says the saver's credit counts the whole
amount; stale "open" and "stay clear" sentences on the 2026 rule are
updated; and the source-conflict candidates match the 2026 code, with a
row for the statute-over-Chart-B ruling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FzZjERFUaWqE3b64t
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 3849cf4a249b7e281a9fe332707e73db475af192 (one commit on top of e8e8a75323f734b30cd696f5970f414d256d68f6; no merge of main, and GitHub reports no conflict).

Findings

P2-1 — applied.

  • Witness first (2026, SINGLE, savers_credit_eligible_person: true, AGI $10,000, able_contributions_person: 500):
    • At e8e8a75 with $1,000 of savers_credit_relevant_able_distributions: qualified contributions 500, savers_credit_person 250. With $300: 500 and 250. Reproduced.
    • On origin/main, which has no such input: 0 and 0. Main's 2026 contributions list leaves out able_contributions_person, and main's formula subtracts retirement_distributions alone.
    • At 3849cf4: $1,000 gives 0 and 0; $300 gives 200 and 100.
  • distributions.yaml:2-12: the 2018 list now holds retirement_distributions and savers_credit_relevant_able_distributions, and the 2027 list holds the ABLE input alone. Comments as the review wrote them.
  • savers_credit_relevant_able_distributions.py:8: documentation replaced as written.
  • savers_credit_qualified_contributions.py:20-21: code comment replaced as written.
  • Changelog: first sentence replaced as written.
  • savers_credit_qualified_contributions.yaml:
    • :79: the review's 2026 case, 800 − 300 = 500.
    • New :90, 2023: $1,000 IRA + $500 ABLE − $400 retirement − $300 ABLE distributions = 800.
    • New :104, 2027: $800 ABLE − $300 ABLE distributions = 500. The $400 of retirement distributions doesn't count.
  • Mutants, each run in its own process, with the parameter file restored after each:
    • A, the review's: drop the ABLE input from the 2018 list. It fails "In 2026 relevant ABLE distributions reduce ABLE contributions too" and "In 2023 retirement-plan and relevant ABLE distributions both reduce contributions".
    • B, mine: add retirement_distributions back to the 2027 list. It fails "In 2027 only relevant ABLE distributions reduce contributions" (the new 2027 case) and "From 2027 only ABLE contributions count".

P3-1 — applied as written.

  • I kept tax_unit_is_required_to_file.py:113.
  • tax_unit_is_required_to_file.yaml:790: case renamed and comment replaced. The expected values stay basic_standard_deduction: 0 and true.
  • I read basic_standard_deduction.py:40-46: separate_filer_itemizes gives 0 with no filing-status gate, as the new comment says.
  • Body witness replaced.

P3-2 — applied as written.

P3-3 — applied. test_filing_requirement_dependent_invariants.py:29-31 and tax_unit_is_required_to_file_dependents.yaml:7-11, both as written. The 2026 section the new sentence points to starts at :544.

P3-4 — applied. YAML :779-780 ("once (iv) is gone, (A) compares only the head's own") and the 2017 body witness.

P3-5 — applied.

  • Code comment at tax_unit_is_required_to_file.py:158-163. I read the three uses it names: threshold (:78-82), basic_threshold (:103) and the own gross income test (:150-151).
  • YAML :722-724, :750-754 and :782-785 now use the review's (D)(ii) form with $3,350, $11,450, $5,350 and $13,450. No expected value changes.

P3-6 — applied. able_contributions_person.py:8-15, as written.

P3-7 — applied.

  • Docstring :24-28, with the tax-unit tests and the (A)(iv) own-income exception.
  • Comment :132-133.
  • YAML :694.
  • Body: the savers_credit_person $1,050 witness and the four active-voice rewrites.

Body. Applied with the REST PATCH and read back; it matches apart from the footer, which the server rewrote. The impact: and axiom: lines are unchanged. I also added savers_credit_qualified_contributions.yaml to the test list, and a clause to the ABLE bullet: "so its saver's credit counts the whole amount", which matches P3-6.

Tests (one file per process, at the final tree)

  • tax_unit_is_required_to_file.yaml: 47 passed.
  • tax_unit_is_required_to_file_dependents.yaml: 56 passed.
  • savers_credit_qualified_contributions.yaml: 9 passed.
  • savers_credit_person.yaml: 18 passed.
  • test_source_conflicts.py: 103 passed.
  • test_filing_requirement_invariants.py: 3 passed.
  • test_filing_requirement_dependent_invariants.py: 5 passed.
  • test_disability_filing_invariants.py: 1 passed.

The three Python test files first failed to collect because hypothesis was missing. I ran uv sync --extra dev and reran them; uv.lock didn't change.

ruff format and ruff check are clean on the changed Python files. I ran no microsimulation.

CI

Pull request run 41547259603 at 3849cf4: completed, success. No job ended other than success or skipped.

Judgment calls

  • 6-hour rule. The last push (20:26 UTC) came from the previous fix round. That round handed off at 21:48, and the item names its head, so I didn't treat that push as someone else's active work.
  • Sources not verified. The session couldn't reach law.cornell.edu, govinfo.gov, irs.gov or uscode.house.gov. So I didn't re-read 25B(d), 6012, 63(c)(6), Form 8880 or Pub 501 myself. Every legal statement I added is the review's wording, applied as the item directs. The only code claims I add rest on code I read.
  • My own cases. I wrote the 2023 and 2027 cases and the hand computations above; their comments describe only the model's list.
  • Extra pushes. The session's own branch got a push of the same commit; it has no PR and starts no workflow (push workflows run only on main).

Follow-ups (proposed, not opened)

  • Gate separate_filer_itemizes on SEPARATE in the standard deduction. basic_standard_deduction zeroes the deduction for any filing status when the flag is true. The fix flips the (C)(ii) case at tax_unit_is_required_to_file.yaml:790 to false. The review already raises this for the hub.
  • Pre-2018 (D)(ii) zero exemption amount for claimable filers. The 2017 formula uses the full exemption amount in three places. The review's witness, 2017 JOINT with both claimable and $1,000 of head wages, returns false where the statute requires a return.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hub impact pair, progress

The base side of the 2026 impact pair finished (8 household chunks through calculate_chunked from #10561). The head side runs next.

  • base 4cf0a98e4755a18f05c9ed91045c5f2f7bb2a38f, the merge base of the head with main
  • head to run next: 3849cf4a249b7e281a9fe332707e73db475af192. The PR head moved past e8e8a75323f734b30cd696f5970f414d256d68f6, the head named for this pair, so the pair measures the new head.
  • dataset hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909; the local file's SHA-256 is 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84, which matches DEFAULT_DATASET_SHA256
  • the side imported policyengine_us from the base/policyengine_us worktree
  • year 2026, chunks 8
  • wall time and memory: 10:14.48 elapsed (GNU time; 9.7 min inside calculate_chunked), maximum resident set size 3,771,400 KiB (3.60 GiB, 3.86 GB), exit status 0
Variable Entity Base weighted sum
tax_unit_is_required_to_file tax_unit 130,057,413.48
tax_unit_is_filer tax_unit 158,866,492.47
files_return_for_aca_ptc tax_unit 158,889,422.77
aca_ptc tax_unit 145,325,338,755.36
medicaid_uses_non_filer_rules person 12,847,271.83
medicaid_tax_dependent_exception_living_with_both_parents person 3,891,661.18
is_medicaid_eligible person 75,836,368.60
nm_premium_assistance_mih_eligible tax_unit 91,671.82
eitc tax_unit 70,026,636,968.69
dc_eitc tax_unit 140,731,752.09
co_eitc tax_unit 225,946,351.17
il_eitc tax_unit 567,438,717.50
wa_working_families_tax_credit tax_unit 261,674,950.18
savers_credit tax_unit 1,741,989,573.43
savers_credit_person person 3,276,957,263.21
savers_credit_qualified_contributions person 388,783,089,780.14
income_tax tax_unit 2,595,105,852,713.51
state_income_tax tax_unit 578,044,599,346.72
household_net_income household 17,161,428,162,125.83
household_benefits household 2,059,793,926,050.13

Sums use the simulation weights the helper returns. A boolean's sum is its weighted count.


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hub impact pair, progress

The base side of the 2027 impact pair has finished. The head side is running now.

  • base 4cf0a98e4755a18f05c9ed91045c5f2f7bb2a38f: the merge base of the head with main.
  • head 3849cf4a249b7e281a9fe332707e73db475af192. The brief named e8e8a753, but the PR head has moved to 3849cf4a (one more commit, "Reduce saver's credit contributions by ABLE distributions in every year"), so the pair measures 3849cf4a.
  • Year 2027, 8 chunks, calculate_chunked from Add household-chunked microsimulation calculations #10561 at c545c478.
  • Dataset hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909, SHA-256 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84 (matches DEFAULT_DATASET_SHA256).
  • Base side: wall time 10.0 min (602 s) inside the helper call (10:35.41 for the whole process); maximum resident set size 3.67 GiB (3.94 GB, 3,849,292 kB); exit status 0.
Variable Base weighted sum
tax_unit_is_required_to_file 131,805,258
tax_unit_is_filer 159,918,465
files_return_for_aca_ptc 159,941,525
aca_ptc 154,102,749,654
medicaid_uses_non_filer_rules 12,743,751
medicaid_tax_dependent_exception_living_with_both_parents 3,913,721
is_medicaid_eligible 74,446,702
nm_premium_assistance_mih_eligible 95,545
eitc 71,319,674,153
dc_eitc 143,554,936
co_eitc 228,979,876
il_eitc 578,970,754
wa_working_families_tax_credit 264,993,365
savers_credit 1,565,450,457
savers_credit_person 3,027,919,323
savers_credit_qualified_contributions 405,100,024,440
income_tax 2,697,246,360,169
state_income_tax 599,217,979,109
household_net_income 17,800,447,840,669
household_benefits 2,174,453,840,253
base summary.json
{
 "chunks": 8,
 "wall_seconds": 601.653116966,
 "weighted_sums": {
  "aca_ptc": 154102749653.55307,
  "co_eitc": 228979875.96351033,
  "dc_eitc": 143554935.8597897,
  "eitc": 71319674152.99246,
  "files_return_for_aca_ptc": 159941525.40665016,
  "household_benefits": 2174453840252.6685,
  "household_net_income": 17800447840668.535,
  "il_eitc": 578970754.2758116,
  "income_tax": 2697246360169.452,
  "is_medicaid_eligible": 74446701.98530921,
  "medicaid_tax_dependent_exception_living_with_both_parents": 3913720.6553683374,
  "medicaid_uses_non_filer_rules": 12743750.783184625,
  "nm_premium_assistance_mih_eligible": 95544.8073750851,
  "savers_credit": 1565450456.6430407,
  "savers_credit_person": 3027919322.875762,
  "savers_credit_qualified_contributions": 405100024440.4967,
  "state_income_tax": 599217979108.53,
  "tax_unit_is_filer": 159918465.19784972,
  "tax_unit_is_required_to_file": 131805258.28610556,
  "wa_working_families_tax_credit": 264993364.9692274
 },
 "year": 2027
}

Generated by Claude Code

Main follows section 6012(f)(2)(D) for a claimable spouse on a 2018-2025
joint return, and this branch follows section 6012(a)(1) from 2026. Git
merged the code without conflict. In the comments, docstrings and the
register, keep main's text on the joint test and add this branch's text
on 2026.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Hub review in progress

Reviewing head 3eca9772aea2f2d6327d81961602818b49645194 (unchanged). Findings rechecked at this head; the final review follows as a separate comment.

  • P1 (firm): tax_unit_is_required_to_file_dependents.yaml:528, main's 2026 joint (C)(i)(I) case, expects false; (A)(iv) and the 2026 zero-income ruling give true. CI run 41580731442 fails on it (Quick Feedback; Baseline irs). Fix: move the case to 2017 and add a 2026 case with the same inputs pinning true (tested on a scratch copy: 62 passed).
  • P3s: docstring at tax_unit_is_required_to_file.py:13-16 overstates the zero-income rule for pre-2018 years; the us:separate-filer-zero-income-threshold note doesn't record the 2026 ruling; duplicate joint at :234; body overlap paragraph is stale.
  • Mutants: A, B, E (savers distributions) and C, D, F, G (filing) all rerun; D is killed only by the 2017 replacement case.
  • Medicaid trace: reproduced the non-filer → filer switch for a $0 separate filer; consistent with 42 CFR 435.603(f)(1) and (f)(4) as the code cites them.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hub impact pair, progress

The base side of the 2027 impact pair has finished. The head side is running now.

  • base 369240bfecddd3c4d27ae710bd8e834332315f43: the merge base of the head with main.
  • head 3eca9772aea2f2d6327d81961602818b49645194. The brief named e8e8a753. The PR head first moved to 3849cf4a, and the first pair measured that commit against 4cf0a98e. After that pair finished, the head moved to 3eca9772, which merges main and moves the merge base to 369240bf, so this second pair measures 3eca9772 against 369240bf.
  • Year 2027, 8 chunks, calculate_chunked from Add household-chunked microsimulation calculations #10561 at c545c478.
  • Dataset hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909, SHA-256 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84 (matches DEFAULT_DATASET_SHA256).
  • Base side: wall time 10.0 min (601 s) inside the helper call (10:35.49 for the whole process); maximum resident set size 3.70 GiB (3.98 GB, 3,884,652 kB); exit status 0.
Variable Base weighted sum
tax_unit_is_required_to_file 131,805,258
tax_unit_is_filer 159,918,465
files_return_for_aca_ptc 159,941,525
aca_ptc 154,102,749,654
medicaid_uses_non_filer_rules 12,743,751
medicaid_tax_dependent_exception_living_with_both_parents 3,913,721
is_medicaid_eligible 74,446,702
nm_premium_assistance_mih_eligible 95,545
eitc 71,319,674,153
dc_eitc 143,554,936
co_eitc 228,979,876
il_eitc 578,970,754
wa_working_families_tax_credit 264,993,365
savers_credit 1,565,450,457
savers_credit_person 3,027,919,323
savers_credit_qualified_contributions 405,100,024,440
income_tax 2,697,246,488,087
state_income_tax 599,217,979,124
household_net_income 17,800,447,712,707
household_benefits 2,174,453,840,253
base summary.json
{
 "chunks": 8,
 "wall_seconds": 600.7407190680005,
 "weighted_sums": {
  "aca_ptc": 154102749653.55307,
  "co_eitc": 228979875.96351033,
  "dc_eitc": 143554935.8597897,
  "eitc": 71319674152.99246,
  "files_return_for_aca_ptc": 159941525.40665016,
  "household_benefits": 2174453840252.6685,
  "household_net_income": 17800447712706.562,
  "il_eitc": 578970754.2758116,
  "income_tax": 2697246488087.418,
  "is_medicaid_eligible": 74446701.98530921,
  "medicaid_tax_dependent_exception_living_with_both_parents": 3913720.6553683374,
  "medicaid_uses_non_filer_rules": 12743750.783184625,
  "nm_premium_assistance_mih_eligible": 95544.8073750851,
  "savers_credit": 1565450456.9936996,
  "savers_credit_person": 3027919322.875762,
  "savers_credit_qualified_contributions": 405100024440.4967,
  "state_income_tax": 599217979123.6259,
  "tax_unit_is_filer": 159918465.19784972,
  "tax_unit_is_required_to_file": 131805258.28610556,
  "wa_working_families_tax_credit": 264993364.9692274
 },
 "year": 2027
}

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hub impact pair, progress

The PR head moved again while I analyzed the first pair, whose base side is in the progress comment above. It moved from 3849cf4a249b7e281a9fe332707e73db475af192 to 3eca9772aea2f2d6327d81961602818b49645194, which merges main, so I'm running the pair again for the new head. Its base side finished; the head side runs next.

  • base 369240bfecddd3c4d27ae710bd8e834332315f43, the merge base of 3eca9772aea2f2d6327d81961602818b49645194 with main
  • dataset hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909; the local file's SHA-256 is 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84, which matches DEFAULT_DATASET_SHA256, unchanged in both trees
  • the side imported policyengine_us from the base worktree at 369240bf
  • year 2026, chunks 8, the same calculate_chunked file from Add household-chunked microsimulation calculations #10561 at c545c47877c70b367a6fab2322037e453364997e
  • wall time and memory: 11:20.81 elapsed (GNU time; 10.7 min inside calculate_chunked), maximum resident set size 3,782,808 KiB (3.61 GiB, 3.87 GB), exit status 0
Variable Entity Base weighted sum
tax_unit_is_required_to_file tax_unit 130,057,413.48
tax_unit_is_filer tax_unit 158,866,492.47
files_return_for_aca_ptc tax_unit 158,889,422.77
aca_ptc tax_unit 145,325,338,755.36
medicaid_uses_non_filer_rules person 12,847,271.83
medicaid_tax_dependent_exception_living_with_both_parents person 3,891,661.18
is_medicaid_eligible person 75,836,368.60
nm_premium_assistance_mih_eligible tax_unit 91,671.82
eitc tax_unit 70,026,636,968.69
dc_eitc tax_unit 140,731,752.09
co_eitc tax_unit 225,946,351.17
il_eitc tax_unit 567,438,717.50
wa_working_families_tax_credit tax_unit 261,674,950.18
savers_credit tax_unit 1,741,989,574.33
savers_credit_person person 3,276,957,263.21
savers_credit_qualified_contributions person 388,783,089,780.14
income_tax tax_unit 2,595,105,975,379.62
state_income_tax tax_unit 578,044,599,361.57
household_net_income household 17,161,428,039,502.38
household_benefits household 2,059,793,926,050.13

Sums use the simulation weights the helper returns. A boolean's sum is its weighted count.


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
IMPACT PAIR: differs
head 3eca977
base 369240b

The brief named head e8e8a753. The PR head first moved to 3849cf4a, and a first pair measured that commit against 4cf0a98e. After that pair finished, the head moved to 3eca9772, which merges main into the branch and moves the merge base to 369240bf. This comment reports a second pair, run from scratch at 3eca9772 against 369240bf. The first pair moved the same 11 variables in the same rows, and each variable's weighted change agrees with this pair's to within $0.36 (only its base side was posted). main's changes shift levels on both sides (income_tax by $127,918 on each).

  • Dataset: hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909, Microsimulation.default_dataset in both trees (system.py is identical in base and head). The downloaded file's SHA-256 is 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84, which matches DEFAULT_DATASET_SHA256.
  • Year 2027; 8 chunks on both sides; calculate_chunked from Add household-chunked microsimulation calculations #10561 at c545c478, loaded by path on both sides.
  • One environment (the head's uv sync) serves both sides: pyproject.toml and uv.lock don't differ between base and head. PYTHONPATH picks the tree, and each side's summary.json names the tree it imported.
Variable Entity Base sum Head sum Change Rows that differ Weighted rows up Weighted rows down Largest abs. difference
tax_unit_is_required_to_file tax_unit 131,805,258 131,902,074 +96,816 55 96,816 0 1
tax_unit_is_filer tax_unit 159,918,465 159,956,220 +37,754 28 37,754 0 1
files_return_for_aca_ptc tax_unit 159,941,525 159,979,280 +37,754 28 37,754 0 1
aca_ptc tax_unit 154,102,749,654 154,102,749,654 0 0 0 0 0
medicaid_uses_non_filer_rules person 12,743,751 12,705,996 −37,754 28 0 37,754 1
medicaid_tax_dependent_exception_living_with_both_parents person 3,913,721 3,913,721 0 0 0 0 0
is_medicaid_eligible person 74,446,702 74,446,696 −6.034 2 0 6.034 1
nm_premium_assistance_mih_eligible tax_unit 95,545 95,545 0 0 0 0 0
eitc tax_unit 71,319,674,153 71,319,674,153 0 0 0 0 0
dc_eitc tax_unit 143,554,936 143,554,936 0 0 0 0 0
co_eitc tax_unit 228,979,876 228,979,876 0 0 0 0 0
il_eitc tax_unit 578,970,754 578,970,754 0 0 0 0 0
wa_working_families_tax_credit tax_unit 264,993,365 264,993,365 0 0 0 0 0
savers_credit tax_unit 1,565,450,457 0 −1,565,450,457 3,409 0 7,121,034 1,292
savers_credit_person person 3,027,919,323 0 −3,027,919,323 4,158 0 9,005,623 1,000
savers_credit_qualified_contributions person 405,100,024,440 0 −405,100,024,440 26,239 0 53,073,925 40,450
income_tax tax_unit 2,697,246,488,087 2,698,535,408,950 +1,288,920,862 3,050 6,414,600 1,172 1,000
state_income_tax tax_unit 599,217,979,124 599,216,601,571 −1,377,553 141 0 267,665 50
household_net_income household 17,800,447,712,707 17,799,160,171,211 −1,287,541,495 2,962 11,851 6,246,274 1,000
household_benefits household 2,174,453,840,253 2,174,453,840,253 0 0 0 0 0

The sums use the simulation weights, so boolean variables sum to weighted counts. Rows are entity records (tax units, people or households) whose values differ by more than 1e-6.

Variables that move (11): tax_unit_is_required_to_file, tax_unit_is_filer, files_return_for_aca_ptc, medicaid_uses_non_filer_rules, is_medicaid_eligible, savers_credit, savers_credit_person, savers_credit_qualified_contributions, income_tax, state_income_tax, household_net_income.

Variables that don't move (9): aca_ptc, medicaid_tax_dependent_exception_living_with_both_parents, nm_premium_assistance_mih_eligible, eitc, dc_eitc, co_eitc, il_eitc, wa_working_families_tax_credit, household_benefits.

Why they move. I read the code on each path below, except the one step noted, and checked each step against this pair's outputs, recomputing the affected households on each side.

  • Saver's credit. From 2027 the head's qualified_retirement_savings_contributions.yaml lists only able_contributions_person, and its new distributions.yaml lists only savers_credit_relevant_able_distributions. The dataset stores neither variable and neither has a formula, so both are 0, and savers_credit_qualified_contributions (contributions less distributions, floored at 0) is 0 for every person. In 2027 the base side still counts traditional and Roth IRA, 401(k) and 403(b) contributions and self-employed pension contributions, less retirement_distributions. savers_credit_person and savers_credit follow it to 0. The PR's $2,100 contribution cap from 2027 has no effect here, since qualified contributions are 0, and the head doesn't model the Saver's Match.
  • Federal income tax. savers_credit is a nonrefundable credit (gov.irs.credits.non_refundable), so income_tax rises by $1.289 billion, $0.277 billion less than the credit lost. The difference is child tax credit. The CTC's liability limit subtracts the credits before it, the saver's credit among them (ctc_tax_liability_limit.preceding_credits), so with the saver's credit at 0 the CTC applied against tax rises by $0.507 billion and refundable CTC falls by $0.231 billion; no other credit changes. 361 of the 3,409 tax units that lose the credit pay no more tax ($0.186 billion of the difference), and units that pay part of it account for the rest. Two of the 361 pay $0.0009765625 less, one float32 step at their values (−$11,019.19 and −$8,445.48); they are the two downward rows.
  • State income tax. All 141 changes are decreases, and each runs through a state provision that starts from a federal amount: Missouri's mo_federal_income_tax_deduction (47 tax units) and Oregon's or_federal_tax_liability_subtraction (47) start from income_tax, and Alabama's al_federal_income_tax_deduction (36) from income_tax_before_refundable_credits. Oklahoma's ok_federal_ctc (11) subtracts the credits that precede the CTC, the saver's credit among them, when federal nonrefundable credits exceed the tax, as they do in all 11 units. It rises, and Oklahoma's child credit rises by 5% of the increase (prorated in one unit). In 9 of the 11 Oklahoma units, federal income tax doesn't change.
  • Household net income. Household by household, the change matches the federal rise net of the state decreases to within $0.11. household_benefits doesn't move: it counts health coverage, Medicaid included, only when gov.simulation.include_health_benefits_in_net_income is true, and that switch is false by default. The 10 households that gain (up to $50) are all in Oklahoma: their state tax falls and their federal tax doesn't change (one rises by $0.00006).
  • Filing requirement. All 55 tax units that newly have to file are married filing separately with $0 gross income. The base formula requires a return only for positive gross income; the head drops that gate, so $0 meets a separate filer's threshold, the exemption amount, which the formula sets to $0 because gov.irs.income.exemption.suspended is true. standard_deduction_rule.applies is false from 2026, so this section 6012(a)(1) path governs 2027 as well. The PR's other filing-rule changes (separate filers who itemize, joint returns with a claimable head or spouse, claimable filers) move no record in this dataset. 27 of the 55 already filed for another reason. For the other 28, tax_unit_is_filer turns true and files_return_for_aca_ptc follows it. For someone who isn't a tax dependent, medicaid_uses_non_filer_rules requires a non-filing tax unit; it turns false for one person in each of those 28 units, and 2 of those people (weighted 6.0) lose is_medicaid_eligible. I didn't trace the Medicaid code from the filing rules to eligibility. aca_ptc doesn't move because is_aca_ptc_eligible excludes separate filers, and with $0 gross income none of the 55 units has an EITC or a state EITC on either side.

Runs

  • Base: wall time 10.0 min (601 s) inside the helper call (10:35.49 for the whole process); maximum resident set size 3.70 GiB (3.98 GB, 3,884,652 kB); exit status 0.
  • Head: wall time 9.9 min (594 s) inside the helper call (10:29.51 for the whole process); maximum resident set size 3.70 GiB (3.97 GB, 3,878,468 kB); exit status 0.

Dropped variables: none. Before the pair, each tree's variable registry listed all 20 variables on the same entity with numeric or boolean types, and a smoke run of the same helper and variables on 800 households went through on both sides. Both full runs computed every variable; the helper's guard rejected none.

Commands

# Once on this VM
curl -sI https://huggingface.co/api/datasets/policyengine/populace-us | head -1
apt-get install -y time   # the VM had no GNU time

# For each pair, in a fresh parent directory
git clone --filter=blob:none -q https://github.com/PolicyEngine/policyengine-us.git pe && cd pe
git fetch -q origin pull/10590/head:pr-10590 pull/10561/head:pr-10561
HEAD_SHA=$(git rev-parse pr-10590); BASE_SHA=$(git merge-base origin/main $HEAD_SHA); echo $HEAD_SHA $BASE_SHA
git worktree add -q --detach ../head $HEAD_SHA
git worktree add -q --detach ../base $BASE_SHA
git show pr-10561:policyengine_us/tools/calculate_chunked.py > ../calculate_chunked.py
git diff --stat $BASE_SHA $HEAD_SHA -- pyproject.toml uv.lock   # empty
cd ../head && pip install uv; uv sync

# The pair, one side at a time
cd ../pe; mkdir -p ../out
H=$PWD/../calculate_chunked.py; PY=$PWD/../head/.venv/bin/python
export OMP_NUM_THREADS=4 OPENBLAS_NUM_THREADS=4 MKL_NUM_THREADS=4
PYTHONPATH=$PWD/../base /usr/bin/time -v $PY ../pair_side.py $H 8 2027 ../out/base > ../out/base.log 2> ../out/base.err
PYTHONPATH=$PWD/../head /usr/bin/time -v $PY ../pair_side.py $H 8 2027 ../out/head > ../out/head.log 2> ../out/head.err
$PY ../compare.py ../out/base ../out/head ../out/compare.json

# Dataset hash (the file hf_hub_download resolved, Hugging Face snapshot 9a814a3b)
sha256sum "$(PYTHONPATH=$PWD/../head $PY -c 'from policyengine_us.system import DEFAULT_DATASET, _resolve_dataset_path; print(_resolve_dataset_path(DEFAULT_DATASET))')"

pair_side.py and compare.py are the brief's scripts, unchanged. The variable-registry check, the 800-household smoke runs and the per-household diagnostics behind the explanation used extra scripts that this list leaves out.

compare.json
{
 "household_net_income": {
  "entity": "household",
  "base_sum": 17800447712706.562,
  "head_sum": 17799160171211.234,
  "change": -1287541495.328125,
  "records_changed": 2962,
  "weighted_changed": 6258125.229058085,
  "weighted_up": 11851.369354016846,
  "weighted_down": 6246273.859704067,
  "largest_abs": 1000.0,
  "rows": [
   {
    "id": 1064412,
    "base": 101196.2734375,
    "head": 100196.2734375
   },
   {
    "id": 1067372,
    "base": 162091.15625,
    "head": 161091.15625
   },
   {
    "id": 77286,
    "base": 43634.9765625,
    "head": 42634.9765625
   },
   {
    "id": 136609,
    "base": 45015.140625,
    "head": 44015.140625
   },
   {
    "id": 1152710,
    "base": 71609.4609375,
    "head": 70609.4609375
   }
  ]
 },
 "household_benefits": {
  "entity": "household",
  "base_sum": 2174453840252.6685,
  "head_sum": 2174453840252.6685,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "medicaid_uses_non_filer_rules": {
  "entity": "person",
  "base_sum": 12743750.783184625,
  "head_sum": 12705996.406638522,
  "change": -37754.37654610351,
  "records_changed": 28,
  "weighted_changed": 37754.376546102925,
  "weighted_up": 0.0,
  "weighted_down": 37754.376546102925,
  "largest_abs": 1.0,
  "rows": [
   {
    "id": 1431758,
    "base": 1.0,
    "head": 0.0
   },
   {
    "id": 1427691,
    "base": 1.0,
    "head": 0.0
   },
   {
    "id": 1370625,
    "base": 1.0,
    "head": 0.0
   },
   {
    "id": 1350741,
    "base": 1.0,
    "head": 0.0
   },
   {
    "id": 1232721,
    "base": 1.0,
    "head": 0.0
   }
  ]
 },
 "medicaid_tax_dependent_exception_living_with_both_parents": {
  "entity": "person",
  "base_sum": 3913720.6553683374,
  "head_sum": 3913720.6553683374,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "is_medicaid_eligible": {
  "entity": "person",
  "base_sum": 74446701.98530921,
  "head_sum": 74446695.9511239,
  "change": -6.034185320138931,
  "records_changed": 2,
  "weighted_changed": 6.034185331547633,
  "weighted_up": 0.0,
  "weighted_down": 6.034185331547633,
  "largest_abs": 1.0,
  "rows": [
   {
    "id": 320144,
    "base": 1.0,
    "head": 0.0
   },
   {
    "id": 10045,
    "base": 1.0,
    "head": 0.0
   }
  ]
 },
 "savers_credit_person": {
  "entity": "person",
  "base_sum": 3027919322.875762,
  "head_sum": 0.0,
  "change": -3027919322.875762,
  "records_changed": 4158,
  "weighted_changed": 9005622.821015269,
  "weighted_up": 0.0,
  "weighted_down": 9005622.821015269,
  "largest_abs": 1000.0,
  "rows": [
   {
    "id": 1335172,
    "base": 1000.0,
    "head": 0.0
   },
   {
    "id": 1129611,
    "base": 1000.0,
    "head": 0.0
   },
   {
    "id": 421378,
    "base": 1000.0,
    "head": 0.0
   },
   {
    "id": 197970,
    "base": 1000.0,
    "head": 0.0
   },
   {
    "id": 197071,
    "base": 1000.0,
    "head": 0.0
   }
  ]
 },
 "savers_credit_qualified_contributions": {
  "entity": "person",
  "base_sum": 405100024440.4967,
  "head_sum": 0.0,
  "change": -405100024440.4967,
  "records_changed": 26239,
  "weighted_changed": 53073924.54519052,
  "weighted_up": 0.0,
  "weighted_down": 53073924.54519052,
  "largest_abs": 40450.49609375,
  "rows": [
   {
    "id": 8266,
    "base": 40450.49609375,
    "head": 0.0
   },
   {
    "id": 1301413,
    "base": 40347.88671875,
    "head": 0.0
   },
   {
    "id": 270043,
    "base": 40347.8828125,
    "head": 0.0
   },
   {
    "id": 1109993,
    "base": 40347.8828125,
    "head": 0.0
   },
   {
    "id": 1211305,
    "base": 40313.57421875,
    "head": 0.0
   }
  ]
 },
 "tax_unit_is_required_to_file": {
  "entity": "tax_unit",
  "base_sum": 131805258.28610556,
  "head_sum": 131902073.9422952,
  "change": 96815.6561896354,
  "records_changed": 55,
  "weighted_changed": 96815.65618964419,
  "weighted_up": 96815.65618964419,
  "weighted_down": 0.0,
  "largest_abs": 1.0,
  "rows": [
   {
    "id": 1230557,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1228400,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1221609,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1218439,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1215840,
    "base": 0.0,
    "head": 1.0
   }
  ]
 },
 "tax_unit_is_filer": {
  "entity": "tax_unit",
  "base_sum": 159918465.19784972,
  "head_sum": 159956219.57439584,
  "change": 37754.37654611468,
  "records_changed": 28,
  "weighted_changed": 37754.376546102925,
  "weighted_up": 37754.376546102925,
  "weighted_down": 0.0,
  "largest_abs": 1.0,
  "rows": [
   {
    "id": 1230557,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1228400,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1197825,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1187025,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1123935,
    "base": 0.0,
    "head": 1.0
   }
  ]
 },
 "files_return_for_aca_ptc": {
  "entity": "tax_unit",
  "base_sum": 159941525.40665016,
  "head_sum": 159979279.78319627,
  "change": 37754.37654611468,
  "records_changed": 28,
  "weighted_changed": 37754.376546102925,
  "weighted_up": 37754.376546102925,
  "weighted_down": 0.0,
  "largest_abs": 1.0,
  "rows": [
   {
    "id": 1230557,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1228400,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1197825,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1187025,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1123935,
    "base": 0.0,
    "head": 1.0
   }
  ]
 },
 "aca_ptc": {
  "entity": "tax_unit",
  "base_sum": 154102749653.55307,
  "head_sum": 154102749653.55307,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "nm_premium_assistance_mih_eligible": {
  "entity": "tax_unit",
  "base_sum": 95544.8073750851,
  "head_sum": 95544.8073750851,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "eitc": {
  "entity": "tax_unit",
  "base_sum": 71319674152.99246,
  "head_sum": 71319674152.99246,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "dc_eitc": {
  "entity": "tax_unit",
  "base_sum": 143554935.8597897,
  "head_sum": 143554935.8597897,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "co_eitc": {
  "entity": "tax_unit",
  "base_sum": 228979875.96351033,
  "head_sum": 228979875.96351033,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "il_eitc": {
  "entity": "tax_unit",
  "base_sum": 578970754.2758116,
  "head_sum": 578970754.2758116,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "wa_working_families_tax_credit": {
  "entity": "tax_unit",
  "base_sum": 264993364.9692274,
  "head_sum": 264993364.9692274,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "savers_credit": {
  "entity": "tax_unit",
  "base_sum": 1565450456.9936996,
  "head_sum": 0.0,
  "change": -1565450456.9936996,
  "records_changed": 3409,
  "weighted_changed": 7121033.94248995,
  "weighted_up": 0.0,
  "weighted_down": 7121033.94248995,
  "largest_abs": 1292.258544921875,
  "rows": [
   {
    "id": 9872,
    "base": 1292.258544921875,
    "head": 0.0
   },
   {
    "id": 1150431,
    "base": 1000.0,
    "head": 0.0
   },
   {
    "id": 1136621,
    "base": 1000.0,
    "head": 0.0
   },
   {
    "id": 4217,
    "base": 1000.0,
    "head": 0.0
   },
   {
    "id": 1107035,
    "base": 1000.0,
    "head": 0.0
   }
  ]
 },
 "income_tax": {
  "entity": "tax_unit",
  "base_sum": 2697246488087.418,
  "head_sum": 2698535408949.6475,
  "change": 1288920862.2294922,
  "records_changed": 3050,
  "weighted_changed": 6415771.91700862,
  "weighted_up": 6414599.8987802155,
  "weighted_down": 1172.0182284051552,
  "largest_abs": 1000.0,
  "rows": [
   {
    "id": 1086806,
    "base": 491.082763671875,
    "head": 1491.082763671875
   },
   {
    "id": 1090833,
    "base": 440.1546630859375,
    "head": 1440.1546630859375
   },
   {
    "id": 103875,
    "base": 206.931640625,
    "head": 1206.931640625
   },
   {
    "id": 1025367,
    "base": 411.102783203125,
    "head": 1411.102783203125
   },
   {
    "id": 1119012,
    "base": 18.05859375,
    "head": 1018.05859375
   }
  ]
 },
 "state_income_tax": {
  "entity": "tax_unit",
  "base_sum": 599217979123.6259,
  "head_sum": 599216601570.7352,
  "change": -1377552.890625,
  "records_changed": 141,
  "weighted_changed": 267664.9058267515,
  "weighted_up": 0.0,
  "weighted_down": 267664.9058267515,
  "largest_abs": 50.000030517578125,
  "rows": [
   {
    "id": 124461,
    "base": 431.9897155761719,
    "head": 381.98968505859375
   },
   {
    "id": 1119012,
    "base": 1690.2496337890625,
    "head": 1640.2496337890625
   },
   {
    "id": 1196731,
    "base": 1289.8282470703125,
    "head": 1254.646728515625
   },
   {
    "id": 1064491,
    "base": 581.885009765625,
    "head": 546.885009765625
   },
   {
    "id": 1064502,
    "base": 1025.355224609375,
    "head": 996.0779418945312
   }
  ]
 }
}

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hub impact pair, progress

The base side of the 2026 impact pair finished (8 household chunks through calculate_chunked from #10561). The head side runs next.

  • base 369240bfecddd3c4d27ae710bd8e834332315f43, the merge base of the head with main
  • head to run next: 3eca9772aea2f2d6327d81961602818b49645194, the PR's current head. The brief named e8e8a75323f734b30cd696f5970f414d256d68f6, but the head has moved since then, so this pair measures 3eca9772.
  • dataset hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909 (DEFAULT_DATASET, identical in both trees). The local file's SHA-256 is 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84, which matches DEFAULT_DATASET_SHA256.
  • this side imported policyengine_us from the base worktree at 369240bf
  • year 2026, 8 chunks, calculate_chunked.py from Add household-chunked microsimulation calculations #10561 at c545c47877c70b367a6fab2322037e453364997e
  • wall time and memory: 12:09.34 elapsed (GNU time; 11.6 min, 694 s, inside calculate_chunked), maximum resident set size 3,790,808 KiB (3.62 GiB, 3.88 GB), exit status 0
  • All 20 sums match, to the cent, the base side that an earlier progress comment posted for the same base.
Variable Entity Base weighted sum
tax_unit_is_required_to_file tax_unit 130,057,413.48
tax_unit_is_filer tax_unit 158,866,492.47
files_return_for_aca_ptc tax_unit 158,889,422.77
aca_ptc tax_unit 145,325,338,755.36
medicaid_uses_non_filer_rules person 12,847,271.83
medicaid_tax_dependent_exception_living_with_both_parents person 3,891,661.18
is_medicaid_eligible person 75,836,368.60
nm_premium_assistance_mih_eligible tax_unit 91,671.82
eitc tax_unit 70,026,636,968.69
dc_eitc tax_unit 140,731,752.09
co_eitc tax_unit 225,946,351.17
il_eitc tax_unit 567,438,717.50
wa_working_families_tax_credit tax_unit 261,674,950.18
savers_credit tax_unit 1,741,989,574.33
savers_credit_person person 3,276,957,263.21
savers_credit_qualified_contributions person 388,783,089,780.14
income_tax tax_unit 2,595,105,975,379.62
state_income_tax tax_unit 578,044,599,361.57
household_net_income household 17,161,428,039,502.38
household_benefits household 2,059,793,926,050.13

The sums use the simulation weights that the helper returns, so a boolean's sum is its weighted count.


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: REQUEST_CHANGES
3eca977
METHODOLOGY_CHOICE: yes

The head hasn't moved since 3eca9772ae. Every delta-review fix (P2-1 and P3-1 to P3-7) appears at the line the hand-off names. The merge kept both sides' logic. But one case from main (#10297) now contradicts this branch's (A)(iv) rule, and CI fails on it (P1). Smaller items: a docstring sentence that's false before 2018, a stale overlap paragraph in the body, the source-conflict note for the 2026 ruling, and the duplicate joint line.

This review continues the interim comment above, which another session started and which stopped partway through. I reran each of its findings at this head instead of relying on it.

Findings

P1. Main's 2026 (C)(i)(I) joint case contradicts (A)(iv), and CI fails on it

Where: policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file_dependents.yaml:528-550, "2026, section 6012(a)(1)(C)(i)(I) adds the additional standard deduction". It comes from main (#10297), and the merge left it unchanged.

Scenario: 2026 JOINT; head aged 40 with no income; spouse aged 65, claimable, with $2,000 of interest; expected false. Another taxpayer can claim the spouse, so the head loses (iv) (tax_unit_is_required_to_file.py:139-152). The head's own $0 then meets the $0 exemption amount, and the formula returns true. The hub's ruling for 2026 gives the same result: the spouse who loses (iv) must file even with no gross income. So the expected value is wrong and the code is right. The body's Overlaps paragraph saw this coming ("Whichever PR lands second must move that (C)(i)(I) check"), but the merge didn't make the move.

Runs:

  • CI run 41580731442 at this head: "Quick Feedback (Selective Tests + Coverage)" and "Baseline tests / Full Suite - Baseline (irs)" both fail with tax_unit_is_required_to_file@2026: [1.] differs from 0.0 on this case.
  • Locally: uv run python -m policyengine_core.scripts.policyengine_command test .../tax_unit_is_required_to_file_dependents.yaml -c policyengine_us gives 1 failed, 60 passed.

Replacement. Move the case to 2017, where the exemption amount is $4,050, so (iv) doesn't force a return and the (C)(i)(I) addition still shows. Then add a 2026 case with the same inputs that pins true. Replace lines 528-550 with:

- name: 2017, section 6012(a)(1)(C)(i)(I) adds the additional standard deduction
  # Under section 6012(a)(1) the test is income other than earned income "in
  # excess of the sum of the amount in effect under section 63(c)(5)(A) plus
  # the additional standard deduction (if any) to which the individual is
  # entitled", so $2,000 of interest does not require a return for the aged
  # spouse: it is not over $1,050 plus the aged addition. The head loses (iv)
  # but has $0, under the $4,050 exemption amount. From 2026 that amount is
  # zero, so the same inputs require a return (next case).
  period: 2017
  input:
    people:
      head:
        age: 40
      spouse:
        age: 65
        claimed_as_dependent_on_another_return: true
        taxable_interest_income: 2_000
    tax_units:
      tax_unit:
        members: [head, spouse]
    households:
      household:
        members: [head, spouse]
        state_code: TX
  output:
    tax_unit_is_required_to_file: false

- name: 2026, the same joint return must file because the head loses (iv)
  period: 2026
  input:
    people:
      head:
        age: 40
      spouse:
        age: 65
        claimed_as_dependent_on_another_return: true
        taxable_interest_income: 2_000
    tax_units:
      tax_unit:
        members: [head, spouse]
    households:
      household:
        members: [head, spouse]
        state_code: TX
  output:
    # The head loses (iv), and the head's $0 meets the zero exemption amount.
    tax_unit_is_required_to_file: true

I applied this on a scratch copy of the head, together with the P3-3 edit below. Results: tax_unit_is_required_to_file_dependents.yaml 62 passed, tax_unit_is_required_to_file.yaml 47 passed, test_filing_requirement_invariants.py 3 passed, test_filing_requirement_dependent_invariants.py 5 passed, test_disability_filing_invariants.py 1 passed. Then I restored the tree.

The 2017 case is needed. Mutant D below drops the year gate from statutory_joint_test, and it survives every test at this head, since the only failure is this broken case. The 2017 case is the only test that kills it.

By hand, from the formula and parameters for 2017: the head's own $0 is under $4,050. The (A) threshold is at least 2 × $4,050. Gross income of $2,000 doesn't exceed the standard deduction plus $4,050. The unearned threshold is $1,050 + $1,300 = $2,350 (the model's 2017 aged addition; see "What I couldn't check"), and $2,000 doesn't exceed it. So the result is false. Without the addition, $2,000 > $1,050 gives true.

Body: the Overlaps paragraph still describes the move as future work. Replace its #10297 sentences with: "#10297 landed first. Its 2026 joint case with a claimed spouse aged 65 and $2,000 of interest becomes required under (A)(iv), so this PR moves that (C)(i)(I) check to 2017, where the exemption amount is $4,050, and adds a 2026 case with the same inputs that pins true." In the test list, change "tax_unit_is_required_to_file_dependents.yaml (one new at-threshold case)" to "tax_unit_is_required_to_file_dependents.yaml (one new at-threshold case; #10297's 2026 joint (C)(i)(I) case moved to 2017, plus a 2026 case with its inputs)".

P3-1. The docstring says a separate filer must file with no gross income "in other years", which includes pre-2018 years

Where: policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py:13-16. "In other years, section 6012(a)(1) ... ; a married individual filing separately must file even with no gross income." "Other years" covers 2017 and earlier. There the separate threshold is the $4,050 exemption amount (:67-69, :115-117), so a 2017 separate filer with $0 need not file. The code comment at :95-97 states the condition correctly ("With an exemption amount of zero"). The ruling covers 2026 on, and the docstring should match it.

Replacement: "...and gross income equal to that amount requires a return. From 2026, with a zero exemption amount, a married individual filing separately must file even with no gross income."

P3-2. The us:separate-filer-zero-income-threshold note doesn't record the 2026 ruling

Where: source_conflicts/candidates.yaml, row us:separate-filer-zero-income-threshold, note. It ends "No completed ruling date is recorded." That was true at the head, but the hub ruled the 2026 half today. The example, follows: statute, both prs and state: open already match the code and the ruling: the row covers 2018 on, and 2018 to 2025 stays open. The code matches the ruling. From 2026 the separate threshold is the exemption amount ($0) with no positive-income gate (:114-117). A separate filer aged 40 with $0 is required in 2026 (Case 19 passes) and not in 2025. The spouse who loses (iv) (:149-152) and the separate_filer_itemizes filer (:114) share the same $0 threshold.

Replacement note:

  note: 'Ruled 2026-11-03 for the statute from 2026: section 6012(f) ends with 2025, so section 6012(a)(1)(A) governs, and a married individual filing separately with no gross income must file, since $0 equals the exemption amount, which section 151(d)(5)(A) keeps at zero. The same text governs the spouse who loses (A)(iv) and a filer whose standard deduction is zero under section 63(c)(6), and PE-US applies it to all three (tax_unit_is_required_to_file.py). For 2018 to 2025 PE-US requires a return only with positive gross income, since section 6012(f) reaches "every individual who has gross income"; that half stays open. PE-US applies the IRS $5 relief in neither period.'

P3-3. A duplicate joint assignment

Where: tax_unit_is_required_to_file.py:234. It repeats :138 exactly, and filing_status doesn't change in between. Drop the second line:

         additional_standard_deduction = aged_or_blind_count * tax_unit.project(
             p_standard.aged_or_blind.amount[filing_status]
         )
-        joint = filing_status == filing_status.possible_values.JOINT
         statutory_joint_test = (

On the scratch copy, ruff check reports no problems, and the five files above pass as listed under P1.

P3-4. The body doesn't say the 2027 saver's credit falls to $0 on the population data

The impact pair shows savers_credit falling from $1.57 billion to $0 in 2027. The code explains why. From 2027, qualified_retirement_savings_contributions.yaml lists only able_contributions_person, and distributions.yaml lists only savers_credit_relevant_able_distributions. Both are inputs with no formula, and the pair reports that the dataset stores neither. So savers_credit_qualified_contributions is 0 for every person. Dropping IRA and plan contributions in 2027 follows the reading that the earlier rounds confirmed from the sources (P.L. 119-21, Form 8880's What's New). But the true 2027 total isn't $0: ABLE beneficiaries who contribute to their own accounts still qualify, and the data can't see them. The body's line "falls to 0 for units that contribute only to retirement plans or IRAs" is accurate but reads as partial. Add: "The population data has no ABLE contributions, so on it the 2027 saver's credit is $0; the model can't measure the ABLE-only credit that remains."

P3-5. One saver's credit case duplicates a stronger one

savers_credit_qualified_contributions.yaml, "From 2027 relevant ABLE distributions reduce ABLE contributions" (800 − 300 = 500). "In 2027 only relevant ABLE distributions reduce contributions" uses the same inputs plus $400 of retirement distributions, and kills everything the first case kills (mutant B). The first case can go. This is optional.

Medicaid trace (the 2 people who lose is_medicaid_eligible)

I built a household like the pair's: 2027, NY, spouses A and B each filing SEPARATE, cohabitating_spouses: true on both units. A is 35 with $0. B is 35 with $32,000 of wages and claims child C, aged 5. Inputs are keyed to 2027.

main (3eca9772ae^2) head
A tax_unit_is_required_to_file / tax_unit_is_filer false / false true / true
A medicaid_uses_non_filer_rules true false
A medicaid_household_size 3 (A, spouse, child) 2 (A, spouse)
A medicaid_household_income 32,000 32,000
A medicaid_income_level 1.138 1.437
A is_medicaid_eligible true false

The path: tax_unit_is_filer (tax_unit_is_filer.py:40, required ⇒ files) → medicaid_uses_non_filer_rules (~is_tax_filer & ~is_tax_dependent) turns false → medicaid_household_size switches from the non-filer household (self, co-resident spouse and own children) to the tax household (tax_unit_size plus the cohabiting separate spouse) → B's income stays in, the child drops out, and 143.7% of FPL exceeds the 138% adult limit.

Does 42 CFR 435.603(f) give this? On the rule as the model's code cites it, yes. Under (f)(1), a filer's household is the taxpayer plus the people they expect to claim; A claims nobody. (f)(4) adds a spouse who lives with them. The child is claimed by B, so the child isn't in A's household. The non-filer rule in (f)(3) is what brought the child in on main. So once A counts as a filer, a household of 2 with $32,000 is the regulation's result. One caveat: (f)(1) keys on whether the individual expects to file, and the model treats every unit required to file as a filer. A separate filer with $0 who is required to file but doesn't expect to would stay under (f)(3). That's a modeling assumption that tax_unit_is_filer already makes for every filer, not a defect of this PR. I couldn't fetch the regulation text (see below), so this conclusion rests on the structure the model's references give for (f)(1), (f)(3) and (f)(4). With cohabitating_spouses: false, A comes out ineligible at $0 on both sides, for a reason I didn't trace.

Mutants (each in its own process on a scratch copy, restored after each; git status clean)

Mutant Killed by
A (hand-off): drop the ABLE input from the 2018 distributions list "In 2026 relevant ABLE distributions reduce ABLE contributions too", "In 2023 retirement-plan and relevant ABLE distributions both reduce contributions"
B (hand-off): add retirement_distributions to the 2027 list "From 2027 only ABLE contributions count", "In 2027 only relevant ABLE distributions reduce contributions"
E: ABLE distributions only from 2026 only the 2023 case
C: drop statutory_joint_test (use the additional standard deduction on every return) three main 2025 cases ("...aged 65 must file with $2,000 of interest", "a blind claimable spouse is tested at $1,350 too", "...aged 65 and blind must file...") and property 4 (test_table_2_boundary_grid). Property 10 (test_filing_requirement_properties) passes under it.
D: drop the year gate from statutory_joint_test nothing at head (the only failure is the P1 case, which fails anyway); the P1 replacement's 2017 case
F (mine): restore main's gross_income > 0 gate Case 19 and test_threshold_grid
G (mine): >= → > in the (iv) own-income test the two 2026 joint no-income cases (claimable spouse, claimable head) and test_joint_return_properties

savers_credit_person.yaml passes under A, B and E: it pins no distribution.

Checked and sound

  • Head and delta: the PR head is 3eca9772ae, unchanged. Its parents are 3849cf4a24 (the fix commit) and 369240bfec (main).
  • P2-1 witness (2026 SINGLE, eligible, AGI $10,000, $500 ABLE): with $1,000 of relevant ABLE distributions, qualified contributions are 0 and savers_credit_person is 0. With $300 they are 200 and 100. The hand values are 500 − 1,000 floored at 0, and 500 − 300 = 200 at the 50% rate. distributions.yaml holds both inputs from 2018 and the ABLE input alone from 2027. The variable documentation, code comment, changelog and body bullet carry the delta review's text.
  • New saver's credit cases, by hand: 2023: $1,000 IRA + $500 ABLE − $400 − $300 = $800. 2027: $800 − $300 = $500, with the $400 of retirement distributions left out. 2026: $800 − $300 = $500. All pass.
  • P3-1 to P3-7 of the delta review: the case at tax_unit_is_required_to_file.yaml:790 and the body witness; the three source-conflict rows; test_filing_requirement_dependent_invariants.py:33 and the dependents header; the YAML at :779; the (D)(ii) comment at :158-163 and the YAML at :722, :751, :783; the able_contributions_person documentation; and the section 911 docstring at :24-28, :132-133, :694 and the body rewrites. All present as written.
  • Merge, tax_unit_is_required_to_file.py: lines 177-245 (the unearned income test with statutory_joint_test) match main exactly. standard_deduction_rule/applies.yaml is false in 2013, true from 2018 and false from 2026, and the model's parameter reads 2017 false, 2026 false, 2027 false. So for 2018 to 2025 a claimable joint spouse is tested above $1,350 (the 2025 amount) whatever their age and blindness, and in other years the addition applies. Mutant C confirms both halves.
  • Merge, dependents YAML: the header's joint-test sentence describes the 2025 cases at :431-526. The 2026 sentence points to the exact-dollar case, which passes. The $2,950 case comment points to the next case, which is the $2,000 statute case.
  • Merge, dependent invariants: the docstring's property 4 and 10 text matches unearned_test_met (per-condition 0 for JOINT) and the grid, which now varies the spouse's age and blindness. Property 7's 2026 "every unit" matches checked = np.ones_like(...).
  • Merge, candidates.yaml: ids sort (LC_ALL=C sort -c). Main's note on us:filing-claimable-spouse-unearned-income-additions stays. Its "every return from 2026 keep[s] Table 2's addition" is true of the threshold, though (iv) makes the addition moot for 2026 joint returns. Its "add the marker once Follow the statute where IRS forms differ: refundable CTC housing deduction, joint filing test, EITC trust royalties, former 32(c)(1)(F) #10297 is on main" is now due, but that's the hub's step.
  • KNOWN_UNMARKED: the test file gives tax_unit_is_required_to_file.py a count of 1. The file has one matching line (:187, "Chart B conflicts with the statute"). test_source_conflicts.py passes (103).
  • Tests at head, one file per process: tax_unit_is_required_to_file.yaml 47 passed; tax_unit_is_required_to_file_dependents.yaml 1 failed (P1), 60 passed; savers_credit_qualified_contributions.yaml 9; savers_credit_person.yaml 18; test_source_conflicts.py 103; test_filing_requirement_invariants.py 3; test_filing_requirement_dependent_invariants.py 5; test_disability_filing_invariants.py 1.
  • Readers: tax_unit_is_required_to_file is read by tax_unit_is_filer, eitc and tools/state_eitc_helpers.py. tax_unit_is_filer is read by five other variable files, Medicaid's non-filer rule among them (traced above). The saver's credit variables are read by savers_credit_person, savers_credit, two reforms (afa_other_dependent_credit, non_refundable_ss_credit) and the uprating and economic-assumption files. The two new inputs are read only by the parameter lists. I found no reader that this PR makes reachable and wrong beyond what the pair measured.
  • Body: the impact: and axiom: lines exist. The 2026 witnesses match the code and the ruling ($0 separate required; joint with a claimable spouse or head and no income required). The diff moves baseline outputs: the filing requirement, filer status, Medicaid non-filer rules and eligibility, and the 2027 saver's credit. The hub's pair measured these.
  • Scope: no file under .github/ or policyengine_us/tests/policy/baseline/partners/. Open overlap: Count only elective deferrals of self-employed plan contributions in the saver's credit #10129 (draft) edits the same contributions list, as the body says. Zero a separate filer's whole standard deduction and exclude dependent care benefits from wages #10579 edits basic_standard_deduction.py and standard_deduction.py, which this formula reads (:82, :105), and it zeroes the whole standard deduction for a separate filer who itemizes. They don't share a file, but whichever lands second should rerun tax_unit_is_required_to_file.yaml.
  • CI, run 41580731442 at this head (attempt 1): failure. "Quick Feedback (Selective Tests + Coverage)" and "Baseline tests / Full Suite - Baseline (irs)" fail. The first error in both is the P1 case, tax_unit_is_required_to_file@2026: [1.] differs from 0.0. "Contrib tests" was skipped. Every other job succeeded: PR validation, Household API Partners, Package and compatibility, Microsimulation, Python shards 1-7, and the Baseline states 1-4, household, ssa-usda, rest-a, rest-b, contrib-hhs and reform shards.

What I couldn't check

  • Sources: this session couldn't reach law.cornell.edu, govinfo.gov, uscode.house.gov or ecfr.gov (proxy refusal). I confirmed no statute, regulation or Public Law text myself. The legal readings above (6012(a)(1), (f), 151(d)(5)(A), 25B(d), 42 CFR 435.603(f)) rest on the hub's ruling, the earlier rounds' fetched quotations and the code's own citations.
  • The 2017 aged addition: the model gives $1,300 joint and $1,600 single for 2017, because aged_or_blind/amount.yaml starts in 2018 and carries that value back. I couldn't read Rev. Proc. 2016-55 to confirm the 2017 amounts. The P1 replacement case gives false for any addition of $950 or more. The pre-2018 values belong with the hub's pre-2018 follow-up.
  • Population counts: I ran no microsimulation, so I rely on the hub's pair for the 55, 28 and 2 counts. My household reproduces the mechanism, not those records.
  • Why A is ineligible with cohabitating_spouses: false: at $0 on both sides, untraced.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
IMPACT PAIR: differs
head 3eca977
base 369240b

The brief named head e8e8a753. The PR head has since moved to 3eca9772, which merges main, so this pair measures 3eca9772 against its merge base 369240bf. That's the head and base of the 2027 pair. I ran both sides from scratch. All 20 base sums match, to the cent, the 2026 base side that an earlier progress comment posted for 369240bf. This run's base side is in this progress comment. The first 2026 progress comment measured a different base, 4cf0a98e, so its sums don't apply here.

  • Dataset: hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909, which is Microsimulation.default_dataset in both trees. DEFAULT_DATASET and DEFAULT_DATASET_SHA256 are identical, and system.py doesn't differ between base and head. The downloaded file (Hugging Face snapshot 9a814a3b) has SHA-256 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84, which matches DEFAULT_DATASET_SHA256. I downloaded it before the base side, so neither side's time includes the download.
  • Year 2026; 8 chunks on both sides. Both sides load calculate_chunked.py from Add household-chunked microsimulation calculations #10561 at c545c478 by path.
  • One environment (the head's uv sync, Python 3.14) serves both sides, since pyproject.toml and uv.lock don't differ between base and head. PYTHONPATH picks the tree, and each side's summary.json names the tree it imported. Microsimulation accepted dataset_end_year on both sides.
Variable Entity Base sum Head sum Change Rows that differ Weighted rows up Weighted rows down Largest abs. difference
tax_unit_is_required_to_file tax_unit 130,057,413.48 130,153,683.44 +96,269.96 55 96,269.96 0 1
tax_unit_is_filer tax_unit 158,866,492.47 158,904,034.04 +37,541.58 28 37,541.58 0 1
files_return_for_aca_ptc tax_unit 158,889,422.77 158,926,964.34 +37,541.58 28 37,541.58 0 1
aca_ptc tax_unit 145,325,338,755.36 145,325,338,755.36 0 0 0 0 0
medicaid_uses_non_filer_rules person 12,847,271.83 12,809,730.25 −37,541.58 28 0 37,541.58 1
medicaid_tax_dependent_exception_living_with_both_parents person 3,891,661.18 3,891,661.18 0 0 0 0 0
is_medicaid_eligible person 75,836,368.60 75,836,368.59 −0.002873 1 0 0.002873 1
nm_premium_assistance_mih_eligible tax_unit 91,671.82 91,671.82 0 0 0 0 0
eitc tax_unit 70,026,636,968.69 70,026,636,968.69 0 0 0 0 0
dc_eitc tax_unit 140,731,752.09 140,731,752.09 0 0 0 0 0
co_eitc tax_unit 225,946,351.17 225,946,351.17 0 0 0 0 0
il_eitc tax_unit 567,438,717.50 567,438,717.50 0 0 0 0 0
wa_working_families_tax_credit tax_unit 261,674,950.18 261,674,950.18 0 0 0 0 0
savers_credit tax_unit 1,741,989,574.33 1,741,989,574.33 0 0 0 0 0
savers_credit_person person 3,276,957,263.21 3,276,957,263.21 0 0 0 0 0
savers_credit_qualified_contributions person 388,783,089,780.14 388,783,089,780.14 0 0 0 0 0
income_tax tax_unit 2,595,105,975,379.62 2,595,105,975,379.62 0 0 0 0 0
state_income_tax tax_unit 578,044,599,361.57 578,044,599,361.57 0 0 0 0 0
household_net_income household 17,161,428,039,502.38 17,161,428,039,502.38 0 0 0 0 0
household_benefits household 2,059,793,926,050.13 2,059,793,926,050.13 0 0 0 0 0

The sums use the simulation weights, so boolean variables sum to weighted counts. Rows are entity records (tax units, people or households) whose values differ by more than 1e-6.

Variables that move (5): tax_unit_is_required_to_file, tax_unit_is_filer, files_return_for_aca_ptc, medicaid_uses_non_filer_rules, is_medicaid_eligible.

Variables that don't move (15): aca_ptc, medicaid_tax_dependent_exception_living_with_both_parents, nm_premium_assistance_mih_eligible, eitc, dc_eitc, co_eitc, il_eitc, wa_working_families_tax_credit, savers_credit, savers_credit_person, savers_credit_qualified_contributions, income_tax, state_income_tax, household_net_income, household_benefits.

2026 filing-requirement breakdown. For this breakdown I ran a diagnostic on each tree for the 54 households that hold a moved filing or Medicaid record. It builds them with the helper's own chunk code and guard. On each side it reproduces exactly all 3,273 of the pair's values for those households.

  • Separate filers with $0 gross income: all 55. Each of the 55 tax units that newly must file (weighted 96,269.96) is married filing separately, and its gross income is exactly $0. That's the formula's own measure: irs_gross_income summed over the unit, plus the section 911 gross exclusion. No tax unit stops having to file. None of the 55 has a head or spouse who can be claimed as a dependent, separate_filer_itemizes or a section 911 exclusion. So the PR's other filing changes move no record in 2026, as in 2027. Those are the (A)(iv) rule for joint returns, the (C)(ii) rule for separate_filer_itemizes, and the tests for filers who can be claimed.
  • Already filed: 27 of the 55 (weighted 58,728.39). Each of them files because would_file_taxes_voluntarily is true; none files to claim a refundable credit. For the other 28 (weighted 37,541.58), tax_unit_is_filer turns true and files_return_for_aca_ptc follows it. Those 28 are the only rows where either variable moves.
  • Medicaid outputs that move: medicaid_uses_non_filer_rules (28 people, weighted −37,541.58) and is_medicaid_eligible (1 person, weighted −0.0029). medicaid_tax_dependent_exception_living_with_both_parents and nm_premium_assistance_mih_eligible don't move. The 28 people are the heads of the 28 tax units that start to file, one per unit, and none is a tax dependent. 14 of them are eligible for Medicaid on both sides and 13 on neither. Person 10045 (Connecticut, aged 62, weight 0.0029) loses eligibility. Their Medicaid household falls from 3 people to 1 while household income stays $23,604.81, so medicaid_income_level rises from 0.864 to 1.479. That's above Connecticut's 2026 adult limit of 1.38 (about 1.3805 for one person once the monthly limit is rounded up to a whole dollar), and medicaid_category falls from ADULT to NONE. Person 320144 (Michigan) was the 2027 pair's other eligibility loss. In 2026, when they're aged 63, their household also falls from 3 people to 1, but their income level rises only from 0.789 to 1.351. That's under Michigan's limit of 1.38 (about 1.3805), so they stay eligible.

The counts match the 2027 pair's: 55 tax units newly required, 27 of them already filing, 28 starting to file, and 28 people leaving the non-filer rules. 2027 has two eligibility losses; 2026 has one.

Why they move. I read the code at each step below.

  • Filing requirement. In 2026 standard_deduction_rule.applies is false, so the formula takes its section 6012(a)(1) path. gov.irs.income.exemption.suspended is true, so the formula's exemption amount is $0. At the base, income_at_least_basic_threshold is (gross_income > 0) & (gross_income >= where(separate, exemption_amount, basic_threshold)). The head drops the gross_income > 0 gate. It sets no_exception to separate or separate_filer_itemizes, then tests gross_income >= where(no_exception, exemption_amount, basic_threshold). So $0 of gross income now meets a separate filer's $0 threshold.
  • Filing. tax_unit_is_filer is true for every unit that's required to file, so the 28 units that weren't already filing for another reason start to file. files_return_for_aca_ptc is true for a filer, so it follows.
  • Medicaid. medicaid_uses_non_filer_rules is true for a person who isn't a tax dependent and whose tax unit doesn't file, or who meets a fallback or an exception. It turns false for the 28 heads. medicaid_household_size then counts the tax household instead of the non-filer household. medicaid_income_level divides household income by the poverty guideline for that size. is_adult_for_medicaid_fc compares that level with the state's adult limit, after medicaid_income_eligible rounds the monthly dollar limit up to a whole dollar. With no category, is_medicaid_eligible is false for person 10045. I didn't trace why medicaid_household_income is the same on both sides; the figures above are computed values.
  • Saver's credit (doesn't move). For 2026 the head adds able_contributions_person to the qualified contributions and savers_credit_relevant_able_distributions to the distributions that reduce them. Both are inputs without formulas, and none of the dataset's six entity tables stores either, so both are $0 and the 2026 credit doesn't change. The $2,000 cap holds through 2026, and the head raises it to $2,100 only from 2027.

Runs

  • Base: 12:09.34 for the whole process (GNU time), and 694 s (11.6 min) inside the helper call. Maximum resident set size 3,790,808 KiB (3.62 GiB, 3.88 GB); exit status 0.
  • Head: 11:46.89 for the whole process, and 672 s (11.2 min) inside the helper call. Maximum resident set size 3,790,896 KiB (3.62 GiB, 3.88 GB); exit status 0.
  • The sides ran one at a time, with nothing else running. Neither came near 14 GB, so I didn't rerun with 16 chunks. After the pair, the breakdown diagnostic took 1:02.06 (base) and 1:00.86 (head), each with a maximum resident set size of 1.89 GiB.

Dropped variables: none. Before the pair, each tree's variable registry listed all 20 variables on the same entity, each a bool or float. Both full runs computed every variable, and the helper's guard rejected none.

Commands

# Once on this VM
curl -sI https://huggingface.co/api/datasets/policyengine/populace-us | head -1
apt-get install -y time   # the VM had no GNU time

# In a fresh parent directory
git clone --filter=blob:none -q https://github.com/PolicyEngine/policyengine-us.git pe && cd pe
git fetch -q origin pull/10590/head:pr-10590 pull/10561/head:pr-10561
HEAD_SHA=$(git rev-parse pr-10590); BASE_SHA=$(git merge-base origin/main $HEAD_SHA); echo $HEAD_SHA $BASE_SHA
# at run time: 3eca9772aea2f2d6327d81961602818b49645194 369240bfecddd3c4d27ae710bd8e834332315f43
git worktree add -q --detach ../head $HEAD_SHA
git worktree add -q --detach ../base $BASE_SHA
git show pr-10561:policyengine_us/tools/calculate_chunked.py > ../calculate_chunked.py   # pr-10561 was c545c47877c70b367a6fab2322037e453364997e
git diff --stat $BASE_SHA $HEAD_SHA -- pyproject.toml uv.lock   # empty
cd ../head && uv sync

# Checks before the pair: the variable registry, then the dataset download and hash
cd ../pe; PY=$PWD/../head/.venv/bin/python
for side in base head; do PYTHONPATH=$PWD/../$side $PY ../registry_check.py > ../registry_$side.json; done
sha256sum "$(PYTHONPATH=$PWD/../head $PY -c 'from policyengine_us.system import DEFAULT_DATASET, _resolve_dataset_path; print(_resolve_dataset_path(DEFAULT_DATASET))')"

# The pair, one side at a time
mkdir -p ../out
H=$PWD/../calculate_chunked.py; PY=$PWD/../head/.venv/bin/python
export OMP_NUM_THREADS=4 OPENBLAS_NUM_THREADS=4 MKL_NUM_THREADS=4
PYTHONPATH=$PWD/../base /usr/bin/time -v $PY ../pair_side.py $H 8 2026 ../out/base > ../out/base.log 2> ../out/base.err
PYTHONPATH=$PWD/../head /usr/bin/time -v $PY ../pair_side.py $H 8 2026 ../out/head > ../out/head.log 2> ../out/head.err
$PY ../compare.py ../out/base ../out/head ../out/compare.json

# The breakdown, after the pair, one side at a time
mkdir -p ../diag
for side in base head; do PYTHONPATH=$PWD/../$side /usr/bin/time -v $PY ../breakdown_side.py $H 2026 ../out $side ../diag/$side > ../diag/$side.log 2> ../diag/$side.err; done
$PY ../breakdown_report.py ../out ../diag ../diag/report.json

# The dataset's tables hold no ABLE contribution or distribution input (each table prints [])
$PY -I -c '
import sys, pandas as pd
names = {"able_contributions_person", "able_contributions", "savers_credit_relevant_able_distributions"}
with pd.HDFStore(sys.argv[1], mode="r") as store:
    for key in ("/person", "/tax_unit", "/marital_unit", "/family", "/spm_unit", "/household"):
        print(key, sorted(names & set(store[key].columns)))
' "$(PYTHONPATH=$PWD/../head $PY -c 'from policyengine_us.system import DEFAULT_DATASET, _resolve_dataset_path; print(_resolve_dataset_path(DEFAULT_DATASET))')"

pair_side.py and compare.py are the brief's scripts, unchanged. The block below holds the registry check and the two breakdown scripts.

Registry check and breakdown scripts

registry_check.py

"""Print each variable's entity and value type in the tree on PYTHONPATH."""
import json
import sys

import policyengine_us
from policyengine_us import CountryTaxBenefitSystem, Microsimulation
from policyengine_us.system import DEFAULT_DATASET, DEFAULT_DATASET_SHA256

VARIABLES = (
    "tax_unit_is_required_to_file",
    "tax_unit_is_filer",
    "files_return_for_aca_ptc",
    "aca_ptc",
    "medicaid_uses_non_filer_rules",
    "medicaid_tax_dependent_exception_living_with_both_parents",
    "is_medicaid_eligible",
    "nm_premium_assistance_mih_eligible",
    "eitc",
    "dc_eitc",
    "co_eitc",
    "il_eitc",
    "wa_working_families_tax_credit",
    "savers_credit",
    "savers_credit_person",
    "savers_credit_qualified_contributions",
    "income_tax",
    "state_income_tax",
    "household_net_income",
    "household_benefits",
)
system = CountryTaxBenefitSystem()
out = {
    "package": policyengine_us.__file__,
    "DEFAULT_DATASET": DEFAULT_DATASET,
    "DEFAULT_DATASET_SHA256": DEFAULT_DATASET_SHA256,
    "Microsimulation.default_dataset": Microsimulation.default_dataset,
    "variables": {},
}
for name in VARIABLES:
    variable = system.variables.get(name)
    out["variables"][name] = (
        None
        if variable is None
        else [variable.entity.key, variable.value_type.__name__]
    )
json.dump(out, sys.stdout, indent=1)
print()

breakdown_side.py

"""Diagnose the moved filing and Medicaid records on one side.

python breakdown_side.py <helper.py> <year> <pair_out_dir> <side> <out_dir>

Selects every household that holds a tax unit or person whose filing or
Medicaid output differs between the pair's two sides, builds one chunk of
those households with the helper's own chunk code, guards the simulation the
way calculate_chunked does, and records diagnostics for each record. It also
recomputes the pair's 20 variables for the selected records and checks them
against <pair_out_dir>/<side>, so a mismatch shows if the subset isn't exact.
The tree on PYTHONPATH supplies the formulas.
"""

import importlib.util
import json
import sys
from pathlib import Path

import numpy as np
import pandas as pd

helper, year, pair, side, out = (
    sys.argv[1],
    int(sys.argv[2]),
    Path(sys.argv[3]),
    sys.argv[4],
    Path(sys.argv[5]),
)
spec = importlib.util.spec_from_file_location("hub_calculate_chunked", helper)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

import policyengine_us  # noqa: E402
from policyengine_core.periods import period as make_period  # noqa: E402
from policyengine_us import Microsimulation  # noqa: E402
from policyengine_us.tools.section_911 import section_911_gross_exclusion  # noqa: E402

PAIR_VARIABLES = {
    "tax_unit": (
        "tax_unit_is_required_to_file",
        "tax_unit_is_filer",
        "files_return_for_aca_ptc",
        "aca_ptc",
        "nm_premium_assistance_mih_eligible",
        "eitc",
        "dc_eitc",
        "co_eitc",
        "il_eitc",
        "wa_working_families_tax_credit",
        "savers_credit",
        "income_tax",
        "state_income_tax",
    ),
    "person": (
        "medicaid_uses_non_filer_rules",
        "medicaid_tax_dependent_exception_living_with_both_parents",
        "is_medicaid_eligible",
        "savers_credit_person",
        "savers_credit_qualified_contributions",
    ),
    "household": ("household_net_income", "household_benefits"),
}
# Records whose values move in these variables pick the households.
SCREEN = {
    "tax_unit": (
        "tax_unit_is_required_to_file",
        "tax_unit_is_filer",
        "files_return_for_aca_ptc",
        "nm_premium_assistance_mih_eligible",
    ),
    "person": (
        "medicaid_uses_non_filer_rules",
        "medicaid_tax_dependent_exception_living_with_both_parents",
        "is_medicaid_eligible",
    ),
}
TAX_UNIT_DIAGNOSTICS = (
    "head_is_dependent_elsewhere",
    "spouse_is_dependent_elsewhere",
    "separate_filer_itemizes",
    "eligible_for_refundable_credits",
    "would_file_if_eligible_for_refundable_credit",
    "would_file_taxes_voluntarily",
    "tax_unit_size",
    "head_spouse_count",
    "age_head",
    "cohabitating_spouses",
)
PERSON_DIAGNOSTICS = (
    "age",
    "is_tax_unit_head",
    "is_tax_unit_spouse",
    "is_tax_unit_dependent",
    "medicaid_is_tax_dependent",
    "irs_gross_income",
    "medicaid_household_size",
    "medicaid_household_income",
    "medicaid_income_level",
)

pair_tables = {
    name: {
        entity: pd.read_pickle(pair / name / f"{entity}.pkl")
        for entity in PAIR_VARIABLES
    }
    for name in ("base", "head")
}

dataset = module._load_dataset(Microsimulation.default_dataset)
source = module._EntityTables(dataset)
positions = source.positions
selected = np.zeros(source.household_count, dtype=bool)
screened = {}
for entity, names in SCREEN.items():
    base, head = pair_tables["base"][entity], pair_tables["head"][entity]
    ids = source.ids(entity)
    for table in (base, head):
        if not np.array_equal(table[f"{entity}_id"].to_numpy(), ids):
            sys.exit(f"{entity}: the pair tables don't follow the dataset's order")
    moved = np.zeros(len(ids), dtype=bool)
    for name in names:
        moved |= (
            np.abs(head[name].to_numpy(dtype=float) - base[name].to_numpy(dtype=float))
            > 1e-6
        )
    screened[entity] = int(moved.sum())
    selected[positions[entity][moved]] = True
masks = {entity: selected[owners] for entity, owners in positions.items()}
if masks["marital_unit"].sum() == 1 < len(masks["marital_unit"]):
    sys.exit("The subset holds exactly one marital unit; the helper rejects that.")

chunk = source.chunk(masks)
sim = Microsimulation(dataset=chunk, dataset_end_year=year)
module._guard(sim)
period = make_period(year)


def calc(name):
    return np.asarray(sim.calculate(name, year, use_weights=False))


tax_unit = sim.populations["tax_unit"]
household = sim.populations["household"]
person_tax_unit_id = tax_unit.ids[tax_unit.members_entity_id]
person_household_id = household.ids[household.members_entity_id]
tax_unit_household_id = (
    pd.Series(person_household_id).groupby(person_tax_unit_id).first()
)

result = {}
frame = pd.DataFrame({"tax_unit_id": tax_unit.ids})
frame["household_id"] = tax_unit_household_id.reindex(tax_unit.ids).to_numpy()
frame["filing_status"] = calc("filing_status")
frame["irs_gross_income_sum"] = tax_unit.sum(calc("irs_gross_income"))
frame["section_911_gross_exclusion"] = section_911_gross_exclusion(tax_unit, period)
frame["filing_gross_income"] = (
    frame.irs_gross_income_sum + frame.section_911_gross_exclusion
)
for name in TAX_UNIT_DIAGNOSTICS + PAIR_VARIABLES["tax_unit"]:
    frame[name] = calc(name)
result["tax_unit"] = frame

frame = pd.DataFrame({"person_id": sim.populations["person"].ids})
frame["tax_unit_id"] = person_tax_unit_id
frame["household_id"] = person_household_id
frame["medicaid_category"] = calc("medicaid_category")
for name in PERSON_DIAGNOSTICS + PAIR_VARIABLES["person"]:
    frame[name] = calc(name)
result["person"] = frame

frame = pd.DataFrame({"household_id": household.ids})
frame["state_code_str"] = calc("state_code_str")
for name in PAIR_VARIABLES["household"]:
    frame[name] = calc(name)
result["household"] = frame

# The subset must reproduce the pair's values for the same records.
mismatches = {}
checked = 0
for entity, names in PAIR_VARIABLES.items():
    pair_table = pair_tables[side][entity].set_index(f"{entity}_id")
    mine = result[entity].set_index(f"{entity}_id")
    expected = pair_table.loc[mine.index]
    for name in names:
        a = mine[name].to_numpy(dtype=float)
        b = expected[name].to_numpy(dtype=float)
        checked += len(a)
        bad = int((a != b).sum())
        if bad:
            mismatches[name] = bad

out.mkdir(parents=True, exist_ok=True)
for entity, frame in result.items():
    frame.to_pickle(out / f"{entity}.pkl")
summary = {
    "package": str(Path(policyengine_us.__file__).parent),
    "side": side,
    "year": year,
    "screened_records": screened,
    "subset_counts": {entity: int(mask.sum()) for entity, mask in masks.items()},
    "values_checked": checked,
    "mismatches": mismatches,
}
(out / "summary.json").write_text(json.dumps(summary, indent=1, sort_keys=True))
print(json.dumps(summary, sort_keys=True), flush=True)

breakdown_report.py

"""Summarize the moved filing and Medicaid records.

python breakdown_report.py <pair_out_dir> <diag_dir> <out.json>

<pair_out_dir> holds the pair's base/ and head/ tables; <diag_dir> holds the
base/ and head/ outputs of breakdown_side.py.
"""

import json
import sys
from pathlib import Path

import numpy as np
import pandas as pd

pair, diag, out = Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3])


def pair_table(side, entity):
    return pd.read_pickle(pair / side / f"{entity}.pkl").set_index(f"{entity}_id")


def diag_table(side, entity):
    return pd.read_pickle(diag / side / f"{entity}.pkl").set_index(f"{entity}_id")


def moved(entity, name):
    base, head = pair_table("base", entity), pair_table("head", entity)
    difference = head[name].to_numpy(dtype=float) - base[name].to_numpy(dtype=float)
    mask = np.abs(difference) > 1e-6
    return base.index[mask], base, head


def count(ids, weights):
    return {"records": int(len(ids)), "weighted": float(weights.loc[ids].sum())}


report = {}
tu_weight = pair_table("base", "tax_unit")["weight"]
person_weight = pair_table("base", "person")["weight"]
base_tu, head_tu = diag_table("base", "tax_unit"), diag_table("head", "tax_unit")
base_p, head_p = diag_table("base", "person"), diag_table("head", "person")
households = diag_table("head", "household")

# Inputs to the breakdown must agree across the sides.
same = {}
for name in ("filing_status", "filing_gross_income", "irs_gross_income_sum"):
    same[f"tax_unit.{name}"] = bool(base_tu[name].equals(head_tu[name]))
for name in ("medicaid_is_tax_dependent", "is_tax_unit_head", "is_tax_unit_spouse"):
    same[f"person.{name}"] = bool(base_p[name].equals(head_p[name]))
report["inputs_identical_across_sides"] = same

required_ids, base_pair, head_pair = moved("tax_unit", "tax_unit_is_required_to_file")
direction = (
    head_pair.loc[required_ids, "tax_unit_is_required_to_file"].astype(int)
    - base_pair.loc[required_ids, "tax_unit_is_required_to_file"].astype(int)
)
newly = direction.index[direction == 1]
report["required_to_file_moved"] = count(required_ids, tu_weight)
report["newly_required"] = count(newly, tu_weight)
report["no_longer_required"] = count(direction.index[direction == -1], tu_weight)

units = head_tu.loc[newly]
report["newly_required_by_filing_status"] = {
    status: count(group.index, tu_weight)
    for status, group in units.groupby("filing_status")
}
zero_income = units.index[units.filing_gross_income == 0]
separate_zero = units.index[
    (units.filing_status == "SEPARATE") & (units.filing_gross_income == 0)
]
report["newly_required_with_zero_gross_income"] = count(zero_income, tu_weight)
report["newly_required_separate_with_zero_gross_income"] = count(
    separate_zero, tu_weight
)
report["newly_required_gross_income_max"] = float(units.filing_gross_income.max())
report["newly_required_other_paths"] = {
    "head_is_dependent_elsewhere": int(units.head_is_dependent_elsewhere.sum()),
    "spouse_is_dependent_elsewhere": int(units.spouse_is_dependent_elsewhere.sum()),
    "separate_filer_itemizes": int(units.separate_filer_itemizes.sum()),
    "section_911_gross_exclusion_positive": int(
        (units.section_911_gross_exclusion > 0).sum()
    ),
}

already = newly[base_pair.loc[newly, "tax_unit_is_filer"].astype(bool).to_numpy()]
not_yet = newly.difference(already)
report["newly_required_already_filing"] = count(already, tu_weight)
report["newly_required_not_yet_filing"] = count(not_yet, tu_weight)
reasons = base_tu.loc[already]
report["already_filing_reasons"] = {
    "would_file_taxes_voluntarily": int(reasons.would_file_taxes_voluntarily.sum()),
    "eligible_and_would_file_for_refundable_credit": int(
        (
            reasons.eligible_for_refundable_credits
            & reasons.would_file_if_eligible_for_refundable_credit
        ).sum()
    ),
}
report["not_yet_filing_turn_filer_on_head"] = int(
    head_pair.loc[not_yet, "tax_unit_is_filer"].astype(bool).sum()
)
filer_ids, _, _ = moved("tax_unit", "tax_unit_is_filer")
report["tax_unit_is_filer_moved"] = count(filer_ids, tu_weight)
report["filer_moves_are_newly_required_not_yet_filing"] = bool(
    set(filer_ids) == set(not_yet)
)
aca_ids, _, _ = moved("tax_unit", "files_return_for_aca_ptc")
report["files_return_for_aca_ptc_moves_match_filer_moves"] = bool(
    set(aca_ids) == set(filer_ids)
)

# Medicaid outputs.
medicaid = {}
for entity, name in (
    ("person", "medicaid_uses_non_filer_rules"),
    ("person", "medicaid_tax_dependent_exception_living_with_both_parents"),
    ("person", "is_medicaid_eligible"),
    ("tax_unit", "nm_premium_assistance_mih_eligible"),
):
    ids, base_table, head_table = moved(entity, name)
    weights = person_weight if entity == "person" else tu_weight
    entry = count(ids, weights)
    if len(ids):
        change = (
            head_table.loc[ids, name].astype(int) - base_table.loc[ids, name].astype(int)
        )
        entry["up"] = int((change > 0).sum())
        entry["down"] = int((change < 0).sum())
    medicaid[name] = entry
report["medicaid"] = medicaid

non_filer_ids, _, _ = moved("person", "medicaid_uses_non_filer_rules")
people = head_p.loc[non_filer_ids]
report["non_filer_rule_people"] = {
    "in_newly_filing_units": int(people.tax_unit_id.isin(not_yet).sum()),
    "is_tax_unit_head": int(people.is_tax_unit_head.sum()),
    "is_tax_unit_spouse": int(people.is_tax_unit_spouse.sum()),
    "medicaid_is_tax_dependent": int(people.medicaid_is_tax_dependent.sum()),
    "per_unit_max": int(people.groupby("tax_unit_id").size().max())
    if len(people)
    else 0,
    "units": int(people.tax_unit_id.nunique()),
}

eligible_ids, _, _ = moved("person", "is_medicaid_eligible")
rows = []
for person_id in eligible_ids:
    b, h = base_p.loc[person_id], head_p.loc[person_id]
    rows.append(
        {
            "person_id": int(person_id),
            "weight": float(person_weight.loc[person_id]),
            "tax_unit_id": int(h.tax_unit_id),
            "in_newly_filing_unit": bool(h.tax_unit_id in set(not_yet)),
            "state": str(households.loc[h.household_id, "state_code_str"]),
            "age": float(h.age),
            "head": bool(h.is_tax_unit_head),
            "spouse": bool(h.is_tax_unit_spouse),
            "filing_status": str(head_tu.loc[h.tax_unit_id, "filing_status"]),
            "non_filer_rules": [
                bool(b.medicaid_uses_non_filer_rules),
                bool(h.medicaid_uses_non_filer_rules),
            ],
            "household_size": [int(b.medicaid_household_size), int(h.medicaid_household_size)],
            "household_income": [
                float(b.medicaid_household_income),
                float(h.medicaid_household_income),
            ],
            "income_level": [float(b.medicaid_income_level), float(h.medicaid_income_level)],
            "category": [str(b.medicaid_category), str(h.medicaid_category)],
            "eligible": [bool(b.is_medicaid_eligible), bool(h.is_medicaid_eligible)],
        }
    )
report["medicaid_eligibility_changes"] = rows

out.write_text(json.dumps(report, indent=1))
print(json.dumps(report, indent=1))
compare.json
{
 "household_net_income": {
  "entity": "household",
  "base_sum": 17161428039502.375,
  "head_sum": 17161428039502.375,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "household_benefits": {
  "entity": "household",
  "base_sum": 2059793926050.1262,
  "head_sum": 2059793926050.1262,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "medicaid_uses_non_filer_rules": {
  "entity": "person",
  "base_sum": 12847271.82582817,
  "head_sum": 12809730.250348527,
  "change": -37541.57547964342,
  "records_changed": 28,
  "weighted_changed": 37541.57547964394,
  "weighted_up": 0.0,
  "weighted_down": 37541.57547964394,
  "largest_abs": 1.0,
  "rows": [
   {
    "id": 1431758,
    "base": 1.0,
    "head": 0.0
   },
   {
    "id": 1427691,
    "base": 1.0,
    "head": 0.0
   },
   {
    "id": 1370625,
    "base": 1.0,
    "head": 0.0
   },
   {
    "id": 1350741,
    "base": 1.0,
    "head": 0.0
   },
   {
    "id": 1232721,
    "base": 1.0,
    "head": 0.0
   }
  ]
 },
 "medicaid_tax_dependent_exception_living_with_both_parents": {
  "entity": "person",
  "base_sum": 3891661.178732761,
  "head_sum": 3891661.178732761,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "is_medicaid_eligible": {
  "entity": "person",
  "base_sum": 75836368.59673956,
  "head_sum": 75836368.59386677,
  "change": -0.0028727948665618896,
  "records_changed": 1,
  "weighted_changed": 0.0028727944009006023,
  "weighted_up": 0.0,
  "weighted_down": 0.0028727944009006023,
  "largest_abs": 1.0,
  "rows": [
   {
    "id": 10045,
    "base": 1.0,
    "head": 0.0
   }
  ]
 },
 "savers_credit_person": {
  "entity": "person",
  "base_sum": 3276957263.2134867,
  "head_sum": 3276957263.2134867,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "savers_credit_qualified_contributions": {
  "entity": "person",
  "base_sum": 388783089780.1381,
  "head_sum": 388783089780.1381,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "tax_unit_is_required_to_file": {
  "entity": "tax_unit",
  "base_sum": 130057413.4841103,
  "head_sum": 130153683.44483973,
  "change": 96269.96072943509,
  "records_changed": 55,
  "weighted_changed": 96269.96072944469,
  "weighted_up": 96269.96072944469,
  "weighted_down": 0.0,
  "largest_abs": 1.0,
  "rows": [
   {
    "id": 1230557,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1228400,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1221609,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1218439,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1215840,
    "base": 0.0,
    "head": 1.0
   }
  ]
 },
 "tax_unit_is_filer": {
  "entity": "tax_unit",
  "base_sum": 158866492.4678716,
  "head_sum": 158904034.04335123,
  "change": 37541.575479626656,
  "records_changed": 28,
  "weighted_changed": 37541.57547964394,
  "weighted_up": 37541.57547964394,
  "weighted_down": 0.0,
  "largest_abs": 1.0,
  "rows": [
   {
    "id": 1230557,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1228400,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1197825,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1187025,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1123935,
    "base": 0.0,
    "head": 1.0
   }
  ]
 },
 "files_return_for_aca_ptc": {
  "entity": "tax_unit",
  "base_sum": 158889422.76599342,
  "head_sum": 158926964.34147304,
  "change": 37541.575479626656,
  "records_changed": 28,
  "weighted_changed": 37541.57547964394,
  "weighted_up": 37541.57547964394,
  "weighted_down": 0.0,
  "largest_abs": 1.0,
  "rows": [
   {
    "id": 1230557,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1228400,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1197825,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1187025,
    "base": 0.0,
    "head": 1.0
   },
   {
    "id": 1123935,
    "base": 0.0,
    "head": 1.0
   }
  ]
 },
 "aca_ptc": {
  "entity": "tax_unit",
  "base_sum": 145325338755.36423,
  "head_sum": 145325338755.36423,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "nm_premium_assistance_mih_eligible": {
  "entity": "tax_unit",
  "base_sum": 91671.81996917567,
  "head_sum": 91671.81996917567,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "eitc": {
  "entity": "tax_unit",
  "base_sum": 70026636968.69415,
  "head_sum": 70026636968.69415,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "dc_eitc": {
  "entity": "tax_unit",
  "base_sum": 140731752.08650315,
  "head_sum": 140731752.08650315,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "co_eitc": {
  "entity": "tax_unit",
  "base_sum": 225946351.16696692,
  "head_sum": 225946351.16696692,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "il_eitc": {
  "entity": "tax_unit",
  "base_sum": 567438717.4972099,
  "head_sum": 567438717.4972099,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "wa_working_families_tax_credit": {
  "entity": "tax_unit",
  "base_sum": 261674950.1818561,
  "head_sum": 261674950.1818561,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "savers_credit": {
  "entity": "tax_unit",
  "base_sum": 1741989574.3300624,
  "head_sum": 1741989574.3300624,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "income_tax": {
  "entity": "tax_unit",
  "base_sum": 2595105975379.617,
  "head_sum": 2595105975379.617,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "state_income_tax": {
  "entity": "tax_unit",
  "base_sum": 578044599361.5671,
  "head_sum": 578044599361.5671,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 }
}

Generated by Claude Code

MaxGhenis and others added 3 commits November 3, 2026 03:04
#10297's 2026 joint case, a claimed spouse aged 65 with $2,000 of
interest, expected no return. Under (A)(iv) the head must file in 2026,
since the head's own $0 equals the zero exemption amount. Run the case in
2017, with its $4,050 exemption amount, and add a 2026 case with the same
inputs that expects a return.

Limit the docstring's zero-income rule for separate filers to 2026 on,
record the 2026 ruling in the source-conflict note, and drop a repeated
`joint` assignment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit 3fb8e8f into main Nov 3, 2026
35 checks passed
@MaxGhenis
MaxGhenis deleted the statute-bugs-savers-filing-2026 branch November 3, 2026 09:26
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#10590 at 4d7d7cbfe44a8e524eca4a199a8c1ef7eed6146e (squash): Keep ABLE contributions in the saver's credit and apply 6012(a)(1) to every 2026 filer.

MaxGhenis pushed a commit that referenced this pull request Nov 3, 2026
…-wages

Brings in #10590, whose filing-test case pins the unconditional
separate_filer_itemizes rule that this PR gates on SEPARATE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Nov 3, 2026
#10598 merged as 56e0a3a. Conflicts:
- copay/rate/first_child.yaml, additional_child.yaml, maximum.yaml and
  tests/.../tx_ccs.yaml: main's versions equal #10598's head, so this
  branch's versions (#10598's text plus the Board-scale clauses, the
  State Plan reference and Cases 7 and 8) stand.
- source_conflicts/candidates.yaml: three-way merge against #10598's
  head; keeps main's new entry from #10590 and this PR's four Texas
  entries. Only those four differ from main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Nov 5, 2026
…t care benefits from wages (#10579)

* Zero a separate filer's whole standard deduction and exclude DCAP from wages

IRC 63(c)(6)(A) makes the standard deduction zero for a married individual
filing separately whose spouse itemizes. The model zeroed only the basic
amount, so the 63(f) aged and blind amount still applied.

Employer dependent care benefits (W-2 box 10) never reduced income tax or
payroll wages. IRC 129(a)(1) excludes them from gross income and
3121(a)(18) and 3306(b)(13) from FICA and FUTA wages. They now leave
box 1 and FICA wages, and the part above the filer's own exclusion
returns as Form 1040 line 1e income.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Count dependent care benefits inside employment_income in older DCAP tests

This PR treats dependent_care_employer_benefits as a pre-tax payroll
deduction inside employment_income, like pre_tax_health_insurance_premiums.
Four tests from #10078 entered box 1 wages as employment_income, so their
box 1 wages fell by the benefits. Raise employment_income by the benefits so
box 1 wages and every expected value stay as they were, and correct CA and
NY comments that still stated AGI before the exclusion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Merge main to pick up the net-of-benefits 129(b) earned income limit (#10520)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Count line 1e as EITC earned income, gate 63(c)(6) on separate status, pin the 129(b) limit

- eitc_earned_income adds taxable_dependent_care_benefits: the excess over
  the IRC 129 exclusion is includible in gross income, so it is earned
  income under 32(c)(2)(A)(i). No cycle runs through this path.
- standard_deduction and basic_standard_deduction zero the deduction only
  for a SEPARATE unit whose spouse itemizes, as 63(c)(6)(A) says.
- The joint YAML case now expects the net-of-benefits 129(b) limit that
  main uses since #10520; add the part-time regression and EITC cases.
- The wage property now asserts exclusion <= wages - benefits, which the
  wage identity alone cannot detect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Measure the 129(b) limit from box 1 wages in the exclusion property

The property from main drew employment_income as the earned income limit.
With line 12 benefits inside employment_income and #10520's taxable-wage
measure, Form 2441 line 18 earned income is employment_income less the
benefits, so the fixtures now draw box 1 wages and add the benefits on top.
California's FTB 3506 measure still reads ca_taxable_wages, which keeps
the benefits; the oracle follows that and California law is unchecked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Merge origin/main into fix-separate-filer-standard-deduction-and-dcap-wages

Brings in #10587's Alabama cases and later main commits before the
round-3 fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Count line 1e as earned income for claimable filers

Line 1e is part of Form 1040 line 1z, which the 2025 instructions use as
earned income on the dependent standard deduction worksheet. Leaving it
off the dependent earned income list made the filing tests treat it as
unearned income and cut the 63(c)(5) deduction. The new witness covers
all three readers. The existing cases now carry care expenses at least
equal to their benefits, so they stay true once the Form 2441 line 16-17
limit lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Count line 1e as IRA compensation

Treas. Reg. 1.219-1(c)(1) counts includible pay for services, so the
taxable part of dependent care benefits is compensation. A helper keeps
the dependent's own IRA limit without line 1e, which would need the
filer's filing status and close a cycle through dependent_gross_income.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bound the exclusion from below and fix stale docs

The wage property checked only upper bounds, so a zero exclusion passed
it. adjusted_earnings now says it is the 129(b) measure, not 32(c)(2)
earned income. Test comments now show the net-of-benefit earned income,
and duplicate references share one entry per source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add California's addition for the federal exclusion above its cap

California kept its $5,000 cap when the federal cap rose for 2021 and
from 2026, so federal AGI left out more than California allows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* List the dependent care addition in ca_additions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Merge origin/main into fix-separate-filer-standard-deduction-and-dcap-wages

Brings in #10590, whose filing-test case pins the unconditional
separate_filer_itemizes rule that this PR gates on SEPARATE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep ira_compensation's formula self-contained and honor supplied input

The IRA differential test runs the formula body on its own, so the
formula repeats the sum with line 1e instead of calling a module helper.
The dependent path now uses a supplied ira_compensation as given, as it
did before, and otherwise leaves out the dependent's own line 1e.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pin the dependent IRA helper's sum and set care expenses in the EITC cases

The helper's copy of the IRC 219(f)(1) sum had no test: dropping the SE
tax deduction or disability pay from it alone passed every IRA file. A
dependent case with disability pay and SE income now pins it. The helper
always left line 1e out, so it drops the dead include_line_1e branch and
says so in its name.

The three EITC cases this PR adds set benefits with no care expenses, so
the Form 2441 line 16-17 limit would move them. Each now sets $5,000 of
expenses, which moves no expected value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Merge main (0315452) into fix-separate-filer-standard-deduction-and-dcap-wages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Lower the stack-depth floor in test_guard_adds_no_frames_per_dependency to 200

This PR's changes shorten the deepest stack that computing income tax
reaches. On CI, merged with main, the test measured an unguarded depth of
282 and failed its floor of 300, while main passes it. The floor checks that
the measurement reaches the dependency chain, where each variable costs
several frames. On a 200-frame chain, a guard that adds one frame per
variable still adds more than the 3 frames the test allows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pin the dependent IRA helper to the formula on every term

ira_compensation_without_line_1e repeats the formula's sum without Form 1040
line 1e. Add a test that compiles both from source and compares them on each
of the nine terms with line 1e at zero, so dropping alimony or the
self-employed pension subtraction from either copy now fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants