Skip to content

Count a filer's gains without netted losses and Social Security once in Montana's elderly credit income - #10139

Open
MaxGhenis wants to merge 7 commits into
mainfrom
fix-mt-ehrc-filer-gains-ss-roles
Open

MaxGhenis wants to merge 7 commits into
mainfrom
fix-mt-ehrc-filer-gains-ss-roles

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR fixes two gaps in Montana's elderly homeowner/renter credit gross household income, both found in the reviews of #9882. #9882's changes landed on main in #9915, and this PR fixes what remained.

  1. A filer's capital losses netted on their own return were never added back (DTrim99, S1).
    • MCA 15-30-2337(9)(a) defines income as "federal adjusted gross income, without regard to loss". 2024 Schedule 2EC line 7 (instructions PDF p. 47) says: "do not include any capital losses".
    • The loss add-back restores loss_ald, which holds only the capital loss deducted in AGI. A short-term loss netted against the same person's long-term gain never reaches loss_ald. So a household member on their own return with a $10,000 long-term gain and a $4,000 short-term loss counted $6,000, but $10,000 as a dependent (dependent_gross_income counts gains without losses). The claimant's own return had the same gap.
    • The prior-year carryover had the same gap (round-1 review of this PR). long_term_capital_gains already includes long_term_capital_loss_carryover as a loss, as Schedule D line 15 does. A $10,000 gain this year with a $4,000 carryover counted $6,000, on both the filer and the dependent path.
    • Fix: each person's gains without losses come from the long-term and short-term parts whenever either is nonzero: max(0, LTCG + carryover) + max(0, STCG). Without parts, capital_gains is a net amount supplied directly, and the gain is max(0, net + carryover).
      • The parts take precedence because capital_gains can disagree with them. Once it is an input for anyone in a simulation, everyone without one holds its default of zero, not the sum of their parts (review round 3).
      • The carryover is never counted as a gain the parts did not show (review round 2).
      • Adding the carryover back follows az_property_tax_credit_capital_gains.
      • The formula then replaces what the person's income path counted (irs_gross_income floors the net; dependent_gross_income floors each part) with these gains. Only the gain a loss hid comes back, and an unused loss never counts as income.
      • Cross-spouse netting on a joint return still goes through loss_ald, so it is not counted twice.
  2. Social Security dropped for a person input as both a dependent and the head (GPT-6.1 Sol, second review of Count household members' income in Montana's elderly homeowner/renter credit #9882, P2).
    • When every adult in a tax unit is input as a dependent, head_or_spouse_candidates keeps the adults, so one becomes the head while staying a dependent. Then neither irs_gross_income (people who are not dependents) nor dependent_gross_income (people who are not head or spouse) counts their taxable Social Security.
    • The add-back still subtracted taxable_social_security + dependent_taxable_social_security. In the test case that left $57,500 of $60,000, against §2337(9)(a)(viii), which counts "all payments received under federal social security".
    • Fix: subtract only the taxable part an income path actually counted.

The trailing comment in gross_income_sources.yaml now describes the three items the formula adds outside the list.

Example (2024 Schedule 2EC)

The claimant is 70, with a $20,000 pension and $2,000 of property tax billed. Their 30-year-old household member files their own return and has a $10,000 long-term gain and a $4,000 short-term loss.

Line Form Main This PR
17 Other members' income (gain without losses) 10,000 6,000 10,000
18 Gross household income 30,000 26,000 30,000
22 Net household income: (18 − 12,600) × 0.05 870 670 870
30 Credit: min(2,000 − 22, 1,150) × 100% 1,130 1,150 1,130

Tests

24 new YAML cases, appended to the existing files. 13 fail on main and pass here; I reverted the formula in place and confirmed the revert before each run.

Case Expected Main
Member's own netted gain [20_000, 10_000] [20_000, 6_000]
Claimant's own netted gain 30,000 26,000
Social Security for a person input as dependent and head [20_000, 60_000] [20_000, 57_500]
Credit-level netting case: net household income [870, 0] [670, 0]
This year's 10,000 gain with a 4,000 carryover (LTCG 6,000) 30,000 26,000
This year's 2,000 gain with a 10,000 carryover 22,000 20,000
A dependent's 10,000 gain with a 4,000 carryover [20_000, 10_000] [20_000, 6_000]
Long-term loss 4,000 against a short-term gain 10,000 30,000 26,000
Losses 10,000 against a short-term gain 4,000 24,000 20,000
Social Security for a head and spouse both input as dependents [20_000, 60_000, 60_000] [20_000, 34_500, 34_500]
Credit with a carryover 1,130 1,150
A 6,000 net supplied directly with a 4,000 carryover 30,000 26,000
Parts showing a 10,000 gain and a 4,000 loss, with an inconsistent 20,000 net 30,000 40,000

11 cases pass on main and guard behavior that a future change could break:

  • A dependent's 10,000 long-term gain, when the claimant's capital_gains is supplied (so the dependent's defaults to 0): [20_000, 10_000], and a credit of 1,130. The previous head of this PR gave [20_000, 0] and 1,150.
  • A carryover with no gain this year adds nothing (20,000).
  • A carryover against a net loss supplied directly adds no gain, for a filer (30,000), a dependent ([20_000, 10_000]) and at the credit level (1,130). The previous head of this PR gave 35,000 and 352.
  • The dependent gate for a minor alone in a tax unit with is_tax_unit_dependent: false, at the gross household income level ([20_000, 10_000]) and the credit level (1,130). DTrim99's S2 asked for this; only the randomized property test reached it before. Counting the minor twice would give a credit of 189.
  • A business loss on another member's return, added back on that return.
  • Cross-spouse capital-loss netting on a joint return, counted once.
  • A three-generation household: the daughter's federal EITC, refundable CTC and Montana EITC count, and the grandchild, a dependent in her tax unit, counts $3,000 of wages. Gross household income is [15_000, 35_502.81, 0, 3_000].

Every expectation is worked line by line in the YAML comments.

Invariant test. policyengine_us/tests/test_mt_elderly_credit_capital_gains.py is a new file.

  • What it builds. One deterministic simulation over a grid. It reaches every sign region of the long-term amount plus carryover, the short-term amount and a net supplied directly, with and without distributions: 258 cases in all.
  • Input styles. Parts with their net; parts with the net left to default, which reproduces round 3's trap; and a net alone.
  • Placements. Each case appears three ways: as the claimant's dependent, on the member's own return, and as the claimant. That makes 774 households in one simulation.
  • What it checks. Gross household income must equal the pension plus the gains without losses plus distributions. The reference is written from Schedule 2EC line 7, independently of the model. The claimant's credit must be the same however the member files, and two grid cases are checked by hand.
  • Why Python. YAML can only pin examples, and three review rounds found edge combinations the examples missed. The test fails on main and on each of the three previous heads of this PR.
  • Cost. Measured locally on macOS, not on CI's Linux runner, and twice back to back at host load 210–290, so wall time is noise:
    • A baseline file that imports the model and builds a one-person simulation used 13.9–14.4 s of user CPU, at a 1.236–1.238 GB peak footprint.
    • This file used 13.4–14.1 s of user CPU, at a 1.247 GB peak. Its own test time is 3.1 s, of which setup is 2.2 s.
    • So the addition is about 3 s and 10 MB on top of an already-loaded model. The Linux increment is an estimate from these numbers; I have not measured it on CI.
    • The file lands in the Rest job's remaining group. In run 38184073570 that group took 28:32.67 end to end (26:25 JUnit time) for 2,248 tests, and the round-3 review read a 13.96 GiB peak from that run's resource report.
  • Why a new file. Limit the Montana elderly homeowner/renter credit to one claimant per household (MCA 15-30-2341(1)) #9900 is rewriting test_mt_elderly_credit_household_members.py, so this file stays separate.

Local runs:

  • At head ca9c11bc41, every file in the elderly credit test folder passes (81/81), and so do test_mt_elderly_credit_household_members.py (2/2) and the new invariant test (3/3).

The heavy-lock sweep at 88e09fef0d passes: the whole mt/ folder (664 cases), contrib/states/mt (75) and the state rollups gov/states/tax/income plus two other Montana tax files (118).

Invariants

  • Each person's capital gains enter gross household income once, without losses, including a prior-year carryover. That holds whether they file their own return, are a dependent, or are a spouse on a joint return, and whether their gains are supplied as parts or as a net.
  • Each person's Social Security enters once and in full, whatever their recorded roles.
  • Gross household income does not depend on how household members are split among tax units, as far as capital gains and Social Security go. Above-the-line deductions follow the federal AGI reading on both paths; Count refundable credits received in Montana's elderly homeowner/renter credit income #9915 subtracts dependents' own deductions.

Not changed

axiom: TheAxiomFoundation/rulespec-us#1512 queued (output 6 and its addendum cover gains without losses and all Social Security once; companion cases added in this comment)

🤖 Generated with Claude Code

Hub microsimulation impact (current head)

A real microsimulation on the default dataset, main at the merge base de3815f4cc against this PR's head c6ee13d99a, for 2024 and 2026. The runs went one at a time under the shared heavy-job lock. Sums are weighted.

Output 2024 change 2026 change 2024 records changed (weighted) 2026 records changed (weighted)
mt_elderly_homeowner_or_renter_credit +0 +0 0 (0) 0 (0)
mt_elderly_homeowner_or_renter_credit_gross_household_income +0 +0 0 (0) 0 (0)
mt_income_tax +0 +0 0 (0) 0 (0)
state_income_tax +0 0 (0)
income_tax +0 0 (0)
household_net_income +0 0 (0)
household_benefits +0 0 (0)

Re-measured after the main merge with #9900's one-claimant rule. No record in the default dataset changes in either year, including the credit's gross household income.

Comparison files are in fixes/10139-impact/ on the hub host.

MaxGhenis and others added 2 commits October 12, 2026 05:15
…in Montana elderly credit income

Gross household income is federal AGI "without regard to loss" (MCA
15-30-2337(9)(a)); 2024 Schedule 2EC line 7 says "do not include any
capital losses". The loss add-back restores only loss_ald, which holds the
capital loss deducted in AGI. A filer's short-term loss netted against
their own long-term gain never reaches it, so a member on their own return
with a $10,000 long-term gain and a $4,000 short-term loss counted $6,000;
as a dependent the same person counted $10,000. Restore the netted loss for
everyone who is not a tax unit dependent, as dependent_gross_income does.

The untaxed Social Security add-back subtracted both taxable_social_security
and dependent_taxable_social_security. A person input as a dependent who is
also the head (every adult in the unit input as a dependent) has neither
counted, so part of their benefits dropped out. Subtract only the taxable
part that irs_gross_income or dependent_gross_income actually counted.

Review findings S1 (DTrim99) and P2 (GPT-6.1 Sol) on
#9882, whose changes landed in #9915.

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

Four cases fail on main (a filer's and a claimant's netted capital loss,
Social Security for a person input as both dependent and head, and the
credit-level netting case); five guard behavior main already has (the
dependent gate for a minor alone in a tax unit, a business loss on another
member's return, cross-spouse netting on a joint return, and a dependent in
another member's tax unit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 4 commits October 12, 2026 06:24
… elderly credit income

long_term_capital_gains already includes the prior-year long-term capital
loss carryover as a loss (Schedule D line 15; long_term_capital_loss_carryover
is a memo). A carryover is a loss, so it must not reduce gross household
income (MCA 15-30-2337(9)(a); 2024 Schedule 2EC line 7, "do not include any
capital losses"). Add it back to get the year's own long-term gain, as
az_property_tax_credit_capital_gains does, and restore only the gain it hid,
on both income paths. Restoration applies where an income path counts the
person, so a person input as both a dependent and the head stays excluded
from both, as before.

New cases: a carryover smaller than, larger than and equal to the year's
gain; a dependent's carryover; a long-term loss netted against a short-term
gain; losses exceeding the gain; and Social Security for a spouse also input
as a dependent. Review finding P2 (GPT-6.1 Sol) on #10139.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… shows; add capital gains property tests

When capital_gains is supplied directly as a net amount, the long-term and
short-term parts default to zero, so adding the carryover to the long-term
part invented a gain: a direct -5,000 net with a 5,000 carryover counted
5,000 (review round 2 on #10139). Compute gains without losses as the year's
net, max(0, capital_gains + carryover), plus the netting between the
year's long-term and short-term amounts, max(0, a) + max(0, b) - max(0, a + b).
With parts supplied this is the floored parts; with a direct net it is the
net alone.

test_mt_elderly_credit_capital_gains.py checks, for random long-term and
short-term amounts or a direct net, a carryover and distributions, that a
household member's gains count without losses whether they are a dependent,
file their own return or are the claimant, against a reference written from
Schedule 2EC line 7, and that the credit is the same either way. It is
derandomized (20 examples, 11 s of test time locally); a 300-example random
local run passed, and it fails on main and on the previous head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gains; test every sign region in one simulation

Once capital_gains is an input for anyone in a simulation, everyone without
one holds its default of zero, not the sum of their parts. The previous
identity started from capital_gains, so a dependent with only a 10,000
long-term gain, beside a claimant whose capital_gains was supplied as 0,
counted nothing (review round 3 on #10139). The parts now decide the gains
whenever either is nonzero, max(0, long-term + carryover) + max(0,
short-term); without parts, capital_gains is a net supplied directly and
counts as max(0, net + carryover). The restoration replaces whatever the
income path counted, so an inconsistent capital_gains input yields to the
parts.

test_mt_elderly_credit_capital_gains.py now builds one simulation from a
grid covering every sign region of the long-term amount plus carryover, the
short-term amount and a direct net, with and without distributions, in three
input styles (parts with their net, parts with the net left to default, a
net alone) and three placements (dependent, own return, claimant). It
replaces 20 Hypothesis-built simulations: 3.1 s of test time locally, peak
footprint 1.25 GB against 1.24 GB for importing the model and building a
one-person simulation, so about 8 MB for the test itself. It fails on main
and on the previous two heads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 12, 2026 12:53
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hand-off to the US + core hub. Head ca9c11bc4181efff7d2d544dd21ea5344af9399d, base main, no PR dependencies.

  • Review. The independent review (GPT-6.1 Sol, Subfleet --task review --tier hard) gave APPROVE at ca9c11bc41 in round 4: ~/reviews/us-hub/mt-ehrc-household/review/review-10139-r4.md. Rounds 1–3 (review-10139-r1.md, -r2.md, -r3.md) requested changes; all are addressed in this PR. They covered the carryover, a net supplied directly, sparse inputs, and the CI cost. Round 4's one P3, the elapsed-time cost report, is in the body.
  • Passed locally at head.
    • Every file in the elderly credit test folder: 81/81.
    • test_mt_elderly_credit_household_members.py (2/2) and the new test_mt_elderly_credit_capital_gains.py (3/3).
    • 13 of the 24 new YAML cases fail on main. The new invariant test fails on main and on each previous head.
    • The heavy-lock sweep (whole mt/ folder, contrib/states/mt, state rollups) passed at 88e09fef0d, before the capital-gains rounds. CI covers the final head.
  • Pending.
    • CI: the full suite started when the PR was marked ready.
    • The hub impact run: impact_queue.sh with spec ~/reviews/us-hub/mt-ehrc-household/impact-spec.txt (10139, 2024 and 2026). The driver is pid 17584, launched with launch_impact.sh; its log is ~/reviews/us-hub/mt-ehrc-household/impact-10139.log, and it writes to fixes/10139-impact/. When it is DONE, run impact_body.py 10139 2024 2026 --apply. That replaces the body's "Pending" section.
  • Related.
  • Axiom. rulespec-us#1512 queued; this PR's companion cases are in three comments there.
  • Follow-ups. SSI, TANF and workers' compensation (Schedule 2EC line 16) are a chip (task_224dc08d).
  • Max decisions. None. This is a bug fix to the law and the form, so it needs no approval under the merge rules.

Artifacts: ~/reviews/us-hub/mt-ehrc-household/.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant