Repository navigation
Count only the head's and spouse's income in the EITC investment income test (26 U.S.C. 32(i)) - #9883
Conversation
…come Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…line Start from net_capital_gains, take out dependents' gains and losses, and add the filers' floored capital gain distributions, so a tax unit amount supplied for net_capital_gains still counts, as it did before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… supplied net_capital_gains is stored in float32, so subtracting a dependent's very large gain from it could move the filers' capital gain line by a few dollars and flip eligibility at the limit (independent review finding, reproduced in the model: a child's 20,000,000 gain turned 11,951 into 11,950). Use the head's and spouse's person-level gains, and read net_capital_gains only where it differs from the members' own gains, that is, where a tax unit amount was supplied. Also widen the property test to dependents' gains of 16 to 64 million, add the two boundary cases, correct the Form 8814 explanation (an election also reaches Worksheet 1 lines 3 and 5) and mention the distributions floor in the changelog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comparing values alone read an abolished or stale net_capital_gains as a supplied aggregate and subtracted dependents' gains from it (Opus delta review S1; with gov.abolitions.net_capital_gains a child's 5,000 gain made the parent eligible). Check core's input record (is_input, next to the existing _input_keys in tools/period_branch.py) as well. Add a test for that branch and a guard that the formula's gain list matches net_capital_gains.adds; fix a stale test comment and a long YAML header line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ow invariant 4 A supplied net_capital_gains that equals the members' own gains and losses in float32 adds nothing to them, so the filers' gains are summed exactly; two YAML cases pin both rounding directions as intended (Sol delta review). Invariant 4 holds for filer gains in the drawn range; past about 16.8 million the previous formula's float32 total could be off by a few dollars, which the direct sum corrects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ital_gains Return to the capital gain line the task specified: the head's and spouse's long- and short-term gains (tax_unit_non_dep_add) plus their floored capital gain distributions. Reading a supplied tax unit net_capital_gains needs to know which tax units supplied it, which core does not record, and every value test for it let a dependent's gain move the filers' result somewhere (four review rounds: float32 collisions, abolished, stale and default-filled values). A supplied tax unit total is now not read, as in irs_gross_income and the NIIT base; no PolicyEngine repo or the microdata supplies it. Drop the is_input helper, the cases that pinned the aggregate reading and the input-record test; pin instead that a supplied total is not read. Add two downstream pins from a 504-household sweep (session bbf31ca6): a dependent's income no longer removes MN child and working family credits or the NM EITC. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ad; clarify the capital gain comment Docs only, from the bafe4fe reviews: the changelog now says the test reads the head's and spouse's long- and short-term gains and no longer a supplied tax unit net_capital_gains; the variable documentation also says a capital_gains amount set directly on a person is not read; the code comment gives the actual reason the tax unit total is not read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Handing this PR to the US + core hub at head Head: Reviews. Everything is in
Tests run locally, one file at a time, all passing at
Sol also reran these independently. Microsimulation: 2026, main versus Pending: CI (32 checks queued behind the Actions backlog; 3 pass so far, none failed). Depends on: nothing. #9889 (items 2 and 3 of #9635) is stacked on this PR and must merge after it. Holds or decisions: none for this PR. On the Axiom side, the encoder dispatch for TheAxiomFoundation/rulespec-us#1416 is Max's d439, which is parked. It doesn't block the merge ( Follow-ups owed, queued as session chips (outside this PR):
🤖 Generated with Claude Code |
# Conflicts: # policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/credits/mn_child_and_working_families_credits.yaml
|
US + core hub sweep audit (2026-10-07): squash-merged #9883 as Reviewed head: |
Summary
eitc_relevant_investment_incomesummed every tax unit member's interest, dividends, capital gains, rents and passive income withadd. A tax unit dependent's income is on the dependent's own return, so it could deny the parent's EITC, and a dependent's loss could offset the parent's own income and grant it. The variable now sums the head and spouse only (tax_unit_non_dep_add). The capital gain line is the head and spouse's long- and short-term gains and losses plus their capital gain distributions, each floored at zero. The capital gain line is floored at zero, as Worksheet 1 line 5 requires. The tax unit'snet_capital_gainsis not read, so an amount supplied there no longer counts (below). Publication 596 Worksheet 1's separate baskets are unchanged: portfolio income is not floored (worksheet lines 1–3 are not negative), and the capital gain line and the netted passive line are each floored on their own.This fixes item 1 of #9635. Items 2 and 3 (passive income that is also partnership self-employment earnings, and farm rent) are not in this PR, so #9635 stays open.
Every number below comes from running
main(4c900b6d3d) and this branch. 2025, TX, head of household aged 30 with $20,000 of wages and one qualifying child aged 8:eitc_relevant_investment_incomemain → brancheitcmain → branchAGI is the same on main and the branch in every row (20,000 in the first four rows; 33,000, 33,000, 33,000, 32,000 and 31,000 after that).
irs_gross_incomealready leaves dependents out, and #9647 did the same for their losses. The 2025 investment income limit is $11,950.Household sweep (another session, bbf31ca6):
46d22d861fmerged onto mainb79705b4bd, compared with main, over 504 households. TY2021–2026 × TX/MN/NM/WI × heads aged 35, 70 and 72 with Social Security, each with a child aged 10 holding $50,000 of taxable interest, tax-exempt interest, qualified dividends, LTCG, STCG or rent, plus no-income controls.eitc,eitc_relevant_investment_income,eitc_investment_income_eligible,mn_child_and_working_families_credits,nm_eitc,wi_earned_income_creditor AGI in any row. The controls match main.~/reviews/mo-capital-gains-line-7a-2026-10-06/eitc-9883-check/.The fix cuts both ways. Filers whose only disqualified income was a dependent's regain the credit. Filers whose dependents' losses had pulled their own investment income under the limit lose it.
Law and forms
non_sch_d_capital_gainsinput cannot be a Form 1099-DIV box 2a amount. As inirs_gross_income,loss_limited_net_capital_gainsandfiler_loss_limited_net_capital_gains, each filer's distributions are floored at zero. Before, the input was added unfloored and could cancel part of a real gain.Code
policyengine_us/variables/gov/irs/credits/earned_income/eitc_relevant_investment_income.py:tax_unit_non_dep_addover taxable interest, tax-exempt interest and dividends.max_(0, the filers' LT and ST gains + Σ filers' max(0, non_sch_d_capital_gains)). This is the filers' Schedule D line 16 with distributions on line 13, and Form 1040 line 7a when positive.tax_unit_non_dep_addoverrental_incomeandpassive_partnership_s_corp_income, netted across the head and spouse before the zero floor.The tax unit's
net_capital_gainsis not read. The capital gain line sums the head's and spouse's person-levellong_term_capital_gainsandshort_term_capital_gains, as the NIIT base (filer_loss_limited_net_capital_gains, #9784) already does. AGI also never reads the tax-unit total:irs_gross_incomecounts the person-levelcapital_gains(on the differences, see the follow-ups below).Behavior change. On main, a
net_capital_gainsamount supplied for the tax unit counted here. Now it doesn't. A YAML case pins this: supplying 12,000 alone gave 12,000 on main and gives 0 here, with AGI 0 on both.Nothing supplies it today. The pinned microdata (
populace_us_2024.h5) has nonet_capital_gainscolumn; its capital gain inputs are person-level. Agh search codeacross PolicyEngine repos finds no input use (the only hits read it as a calibration target).Why not keep it. Earlier heads tried: subtract dependents' person-level gains from the supplied total (the convention of Use the filers' Schedule D for the tax on net capital gain #9855's
filer_schedule_d_linesline 16), and detect "supplied" by value, then by core's input record. Two problems made that unworkable:Four review rounds each found a case where a dependent's gain still moved the filer: float32 collisions (including a supplied total that moves with the child's gain), abolished values, stale cached values, and default-filled units in mixed situations. The person-level sum is the route the task specified while Use the filers' Schedule D for the tax on net capital gain #9855 is unmerged, and it makes the dependent invariant hold for every input. If aggregate support is wanted, the clean route is a per-unit interface or a filer-only
net_capital_gains. That would be a separate PR.Float32. Summing the filers' own gains is exact whatever a dependent has. Two YAML boundary cases with a $20,000,000 child pin it.
Downstream. Every state credit computed from the federal EITC moves with it. These read this variable, or its eligibility flag
eitc_investment_income_eligible, directly or through the sharedcalculate_eitc_like_amounthelper:eitc_eligibleca_eitc_eligible), and through it the young child tax credit, the foster youth tax credit and San Francisco's WFTCin_eitc_eligible)federal_eitc_without_age_minimum)me_pro_forma_childless_eitc)mn_wfc_eligible)mo_wftc_eligible)nj_eitc_income_eligible)nm_eitc_eligible)ok_federal_eitc_eligible,ok_federal_eitc_investment_income_eligible)States with their own investment income limits can change where the federal one doesn't. Example, run on main and this branch: CA's 2025 limit is $4,814. A head of household aged 30 with $20,000 of wages, whose child aged 3 has $5,000 of interest, goes from CalEITC 0 → 388.21 and the young child tax credit 0 → 1,189. The federal EITC is 4,328 on both.
Invariants
These hold for every input with nonnegative interest and dividends, and the property test checks them:
eitc_relevant_investment_income,eitc_investment_income_eligible,eitc,filer_loss_limited_net_capital_gainsor AGI. That includes dependents' gains up to $64 million, where float32 rounding of a tax-unit total would show.eitc_relevant_investment_incomeequals an independent numpy computation of Worksheet 1 over the head and spouse: interest, tax-exempt interest and dividends, plus max(0, their LT + ST gains + per-filer max(0, distributions)), plus max(0, their rental + passive income). Eligibility is exactly that amount ≤ the limit, andeitcis 0 whenever it exceeds the limit.net_capital_gainsalso changes, by design, because that total is no longer read.Two intended exceptions:
Other routes by which a dependent's income still reaches the filer. These are outside this formula and present on main too. The bafe4fe reviews reproduced them, and each is queued as separate follow-up work (not in this PR, no GitHub issue yet):
tax_unit_is_required_to_file: a child's wages can turn off the filer's required filing, and with it the EITC.taxable_unemployment_insurance: the 2020 exclusion pools a child's unemployment compensation, and a negative child amount changes the filer's.tax_unit_social_security_for_taxability: subtracting dependents from a float32 total.capital_gainsset directly, as the capital-gains marginal-rate branch does, reaches AGI but not this test or the NIIT base. So that marginal rate misses the EITC investment-income cliff.Tests
YAML.
policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_relevant_investment_income.yamlhas 12 cases, each with hand arithmetic in comments:e993a95bc1, which subtracted from the float32 total.net_capital_gainsis not read. It fails on main by design.Downstream pins from session bbf31ca6's sweep, with hand arithmetic. Both fail on main and pass here:
mn_child_and_working_families_credits.yaml, 2025: a child's $50,000 of interest no longer removes MN's credits, 2,129.20 = 1,750 CTC + 4% × 9,480.nm_eitc.yaml, 2025: a child's $50,000 LTCG no longer removes NM's EITC, 1,082 = 25% × 4,328.Python.
policyengine_us/tests/core/test_dependent_eitc_investment_income_invariants.pychecks invariants 1–4 in 3 tests:derandomize=True, 5 examples, each a batch of 10–40 tax units: single, head of household, joint, and joint with dependents. Dependents' capital, rental and passive inputs take either sign, including negative distributions. Their gains sometimes reach $64 million in multiples of 16, which float32 holds exactly. Interest and dividends are drawn nonnegative, and the passive share equals the drawnpartnership_s_corp_income.Each batch runs as one vectorized simulation three times: as drawn, with the dependents' inputs zeroed, and with $1,000 more of the head's interest. YAML can't state a property over all inputs, which is why this one is in Python.
It fails on main, where all 3 tests fail. With the current seeded population (which includes dependents' gains up to $64 million), 103 of 200 units differ from main, by up to $117,211,907; Sol's final review measured this. The 107-of-200 and $65,768 figures came from the earlier population, without the large gains. It also fails on
e993a95bc1, the float32 subtraction, where all 3 fail: one Hypothesis batch is off by $1, and the seeded run has 19 of 200 units off by up to $5.Measured locally on macOS (Apple silicon, Python 3.13, host heavily loaded), not on Linux CI: the three tests' calls take 2.0 s (1.45 + 0.34 + 0.25 s), 30.5 s wall including import and system build, 1.13 GB peak RSS. It lands in the
coregroup ofmake test-other-python-rest, which the Makefile times at 142 s.Existing files that read this variable, run locally one file at a time. All pass.
e1d780bc96, whose capital gain line is the same as this head's:max(0, filer_loss_limited_net_capital_gains), which equals the filers' gains plus floored distributions when positive. None of the 17 suppliesnet_capital_gains(checked with grep).bafe4feeb2I re-ran these, and all pass:eitc_investment_income_baskets(3), the partner file (15),mn_child_and_working_families_credits(18),nm_eitc(4),nj_eitc_investment_income_baskets(6), and this PR's YAML (12) and Python tests (3). At257fe3444aI also rannj_eitc_income_eligible(3).The 17 files:
eitc_investment_income_baskets,eitc_investment_income_eligible,earned_income_tax_credit;ca_eitc_eligible,co_eitc,dc_eitc,dc_eitc_with_qualifying_child,dc_eitc_without_qualifying_children,il_eitc,in_eitc_eligible,mo_wftc_eligible,mo_wftc,ok_federal_eitc_investment_income_eligible,wa_working_families_tax_credit,wi_earned_income_credit;mt_hb268;partners/analytics_coverage/edge_cases/federal/tax_credits/eitc/federal.yaml. Its cases put the capital gain on the head, so nothing changes; no partner file is edited.Reviews
Two independent reviews ran on the same brief at
e993a95bc1. Both are in~/reviews/eitc-dependent-investment-income-2026-10-06/.Opus 5.5 (Subfleet
--tier standard),review-opus.md: APPROVE, with three should-fixes. All three are fixed: the downstream list is now complete, the NJ files are run, and the changelog mentions the distributions floor. Its nits on the docstring and the Form 8814 wording are fixed. Its nit on the hard-coded gain components stands. The formula sums long- and short-term gains, the same components asnet_capital_gains.adds, andirs_gross_income'scapital_gains.GPT-6.1 Sol (Subfleet
--tier hard),review-sol.md: REQUEST_CHANGES.Opus delta review of
e993a95bc1→46d22d861f,review-delta-opus.md: APPROVE.net_capital_gains. I reproduced it and fixed it in069db9a27awith the input record.INTEREST_SHIFTcomment, the long YAML header line, a note that a supplied amount is stored in float32, and a test guarding theaddslist.Sol delta review of
e993a95bc1→46d22d861f,review-delta-sol.md: REQUEST_CHANGES.f4c4e66406).INTEREST_SHIFTcomment was already fixed in069db9a27a.069db9a27a, which came after the head it reviewed.Final round at
f4c4e66406:review-final-opus.md): APPROVE. It agreed with the reply on Sol's collision point. Its should-fix: a value supplied for one tax unit gives the others a recorded default of 0.review-final-sol.md): REQUEST_CHANGES. Its blocking case was a supplied total that moves with the child's gain (+2 each) yet switches authority at the float32 collision and flips eligibility. It also found a deleted-but-recorded input and abolition together with a supplied total.Both are right that value tests can't establish which units supplied the aggregate. The resolution is in
bafe4feeb2: stop reading the tax unit'snet_capital_gains(Code section above). That makes every one of those cases moot. Theis_inputhelper and the aggregate-specific tests are removed.Review of
bafe4feeb2: Sol (review-final2-sol.md) and Opus (review-final2-opus.md) both APPROVE, with no blocking findings.net_capital_gainsis not read.capital_gainsis not read.capital_gainsmarginal-rate gap are listed above as follow-ups, since they exist on main and lie outside this formula.6dae716b9c.Review of
6dae716b9c(the docs-only delta), by an independent in-session Opus 5.5 agent: APPROVE.default_arrayfill.Microsimulation impact
One 2026 run of each, from
lockrun.shon the populace-us 2024 dataset (populace_us_2024.h5@populace-us-2024-spm-20260909). Main (4c900b6d3d) is compared with this branch (bafe4feeb2), record by record (compare.py).eitc_relevant_investment_incomeeitc_investment_income_eligibleeitc,income_tax,state_income_tax,household_net_incomeca_eitc,wi_earned_income_credit,mo_wftc,ok_eitc,in_eitc,wa_working_families_tax_creditfiler_loss_limited_net_capital_gainsWhat the numbers show:
Raw output:
~/reviews/eitc-dependent-investment-income-2026-10-06/impact-9883-2026.json. Per-record arrays:~/reviews/pe-us-merge-backlog-2026-10-02/impact/out/{eitcdep-base,eitcdep-pr}.2026.*.Axiom
axiom: TheAxiomFoundation/rulespec-us#1416 queued
rulespec-us encodes only 32(i)(1):
us/statutes/26/32.yamlruleeitc_investment_income_eligiblecompares an opaqueeitc_relevant_investment_incomeinput with the limit. 32(i)(2) is queued in TheAxiomFoundation/rulespec-us#1416 (pe-parity, dispatch-ready). Itsreview_findingalready requires "Every amount is the taxpayer's own (both spouses on a joint return), not a dependent's". This comment adds the sources for that scope, a pasteablereview_findingaddendum and six hand-computed TY2026 companion cases (J1–J6) matching the YAML cases here. The signed-encoder dispatch is decision d439, which Max has parked. The one route by which a child's interest, dividends and distributions do reach a parent's Worksheet 1 is the §1(g)(7) Form 8814 election, which PE doesn't model. It is queued separately in TheAxiomFoundation/rulespec-us#1424.🤖 Generated with Claude Code