Repository navigation
Keep ABLE contributions in the saver's credit and apply 6012(a)(1) to every 2026 filer - #10590
Conversation
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
|
Ready for hub review Head: Per-item status
Ownership checks: neither branch existed; no open PR on the same change. Tests (failing first, then passing; one file per process)
Hand computations
Mutants (each in its own process, code restored after)
Process note: my first F2 run was invalid. CIDraft, not run. Judgment calls
Proposed follow-up
Generated by Claude Code |
|
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
|
Ready for hub review Head: What the item asked, and how it's resolved
CIThe Judgment calls
Proposed follow-up issues
Generated by Claude Code |
|
Ready for hub review Independent review, round 1. The head hasn't moved past 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. FindingsP2-1. Merging #10297 changes one of #10297's expected values; the body predicts only a textual conflictWhere: the PR body, "Overlaps" ("Expect a textual conflict in that test file"), and #10297's 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 Run: I merged Replacement (body): "#10297 edits the same variable and two of this PR's test files. Merging them conflicts in comments in P2-2.
|
| 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_filercan only rise.files_return_for_aca_ptccan only rise, soaca_ptccan rise.nm_premium_assistance_mih_eligiblecan 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_rulescan only fall for affected people (more filers).medicaid_tax_dependent_exception_living_with_both_parentscan only rise, sinceparents_file_jointlyreadstax_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_elsewhereorspouse_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
:784case assumes throughseparate_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.pyrun 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
|
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>
|
Ready for hub review Head: Round 1 fix round on Merge of main
Findings
Mutant (e)Mutant (e) deletes the
Tests at e8e8a75 (one file per process)
CIPull request run 41522520991 at Judgment calls
Proposed follow-up
Generated by Claude Code |
|
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
|
Ready for hub review Head: FindingsP2-1 — applied.
P3-1 — applied as written.
P3-2 — applied as written.
P3-3 — applied. P3-4 — applied. YAML P3-5 — applied.
P3-6 — applied. P3-7 — applied.
Body. Applied with the REST PATCH and read back; it matches apart from the footer, which the server rewrote. The Tests (one file per process, at the final tree)
The three Python test files first failed to collect because
CIPull request run 41547259603 at 3849cf4: completed, success. No job ended other than success or skipped. Judgment calls
Follow-ups (proposed, not opened)
Generated by Claude Code |
|
Hub impact pair, progress The base side of the 2026 impact pair finished (8 household chunks through
Sums use the simulation weights the helper returns. A boolean's sum is its weighted count. Generated by Claude Code |
|
Hub impact pair, progress The base side of the 2027 impact pair has finished. The head side is running now.
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>
|
Hub review in progress Reviewing head
Generated by Claude Code |
|
Hub impact pair, progress The base side of the 2027 impact pair has finished. The head side is running now.
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 |
|
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
Sums use the simulation weights the helper returns. A boolean's sum is its weighted count. Generated by Claude Code |
|
Ready for hub review The brief named head
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): Variables that don't move (9): 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.
Runs
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))')"
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 |
|
Hub impact pair, progress The base side of the 2026 impact pair finished (8 household chunks through
The sums use the simulation weights that the helper returns, so a boolean's sum is its weighted count. Generated by Claude Code |
|
Ready for hub review The head hasn't moved since 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. FindingsP1. Main's 2026 (C)(i)(I) joint case contradicts (A)(iv), and CI fails on itWhere: Scenario: 2026 JOINT; head aged 40 with no income; spouse aged 65, claimable, with $2,000 of interest; expected Runs:
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 - 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: trueI applied this on a scratch copy of the head, together with the P3-3 edit below. Results: The 2017 case is needed. Mutant D below drops the year gate from 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 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 P3-1. The docstring says a separate filer must file with no gross income "in other years", which includes pre-2018 yearsWhere: 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
|
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 are3849cf4a24(the fix commit) and369240bfec(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_personis 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.yamlholds 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:790and the body witness; the three source-conflict rows;test_filing_requirement_dependent_invariants.py:33and the dependents header; the YAML at:779; the (D)(ii) comment at:158-163and the YAML at:722,:751,:783; theable_contributions_persondocumentation; and the section 911 docstring at:24-28,:132-133,:694and the body rewrites. All present as written. - Merge,
tax_unit_is_required_to_file.py: lines 177-245 (the unearned income test withstatutory_joint_test) match main exactly.standard_deduction_rule/applies.yamlis 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,950case 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" matcheschecked = np.ones_like(...). - Merge,
candidates.yaml: ids sort (LC_ALL=C sort -c). Main's note onus:filing-claimable-spouse-unearned-income-additionsstays. 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 givestax_unit_is_required_to_file.pya count of 1. The file has one matching line (:187, "Chart B conflicts with the statute").test_source_conflicts.pypasses (103).- Tests at head, one file per process:
tax_unit_is_required_to_file.yaml47 passed;tax_unit_is_required_to_file_dependents.yaml1 failed (P1), 60 passed;savers_credit_qualified_contributions.yaml9;savers_credit_person.yaml18;test_source_conflicts.py103;test_filing_requirement_invariants.py3;test_filing_requirement_dependent_invariants.py5;test_disability_filing_invariants.py1. - Readers:
tax_unit_is_required_to_fileis read bytax_unit_is_filer,eitcandtools/state_eitc_helpers.py.tax_unit_is_fileris read by five other variable files, Medicaid's non-filer rule among them (traced above). The saver's credit variables are read bysavers_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:andaxiom: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/orpolicyengine_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 editsbasic_standard_deduction.pyandstandard_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 reruntax_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.yamlstarts in 2018 and carries that value back. I couldn't read Rev. Proc. 2016-55 to confirm the 2017 amounts. The P1 replacement case givesfalsefor 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
|
Ready for hub review The brief named head
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): Variables that don't move (15): 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.
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.
Runs
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))')"
Registry check and breakdown scripts
"""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()
"""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)
"""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 |
#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>
|
US + core hub merge audit, us#10590 at
|
…-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>
#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>
…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>
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 restoreable_contributions_personto the 2026 list, which main dropped, and add a 2027 list that holdsable_contributions_persononly.contributions_cap.yaml: $2,100 from 2027; $2,000 through 2026.distributions.yaml: the distributions that reduce qualified contributions. Through 2026,retirement_distributionsand 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 fromretirement_distributions.able_contributions_persondocumentation: 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 throughable_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.savers_creditcounts 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.yamllist (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_filewhenstandard_deduction_rule.appliesis false:basic_standard_deductionalready holds the 63(c)(5) limited amount.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-levelirs_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.separate_filer_itemizesis 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.Limits.
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.irs_gross_incomealone.policyengine_us/tools/section_911.pygives 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_itemizeson 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 changesbasic_standard_deduction.pyandstandard_deduction.py, which this formula reads, so we reruntax_unit_is_required_to_file.yamlon 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 newtest_joint_return_propertiesincludes anchors with a claimable head as well as a claimable spouse.test_filing_requirement_dependent_invariants.pyandtest_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:
axiom: TheAxiomFoundation/rulespec-us#1696 and TheAxiomFoundation/rulespec-us#2011 queued
🤖 Generated with Claude Code