Skip to content

Halve the married-filing-separately SALT cap after the phase-down, not before - #9979

Merged
MaxGhenis merged 4 commits into
mainfrom
fix-salt-cap-mfs-halving
Oct 12, 2026
Merged

MaxGhenis merged 4 commits into
mainfrom
fix-salt-cap-mfs-halving

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

salt_cap applied the 30% SALT phase-down to the already-halved married-filing-separately (MFS) cap. Separate filers therefore lost their cap twice as fast as the law allows. For 2025 MFS modified AGI of $300,000, with taxes paid above the cap, the law gives $12,500; the model gave $5,000.

This PR applies the halving after the phase-down and the $10,000 floor, as the statute and the 2025 Schedule A worksheet do. It also stops the floor from raising a reformed cap that starts below it.

The law and the form

P.L. 119-21 sec. 70120 (139 Stat. 169–170) rewrote 26 U.S.C. 164(b)(6)–(7):

  • 164(b)(6)(B): the SALT deduction may not exceed "the applicable limitation amount (half the applicable limitation amount in the case of a married individual filing a separate return)".
  • 164(b)(7)(B)(i): "the applicable limitation amount shall be reduced by 30 percent of the excess (if any) of the taxpayer's modified adjusted gross income over the threshold amount (half the threshold amount in the case of a married individual filing a separate return)".
  • 164(b)(7)(B)(iii): "The reduction under clause (i) shall not result in the applicable limitation amount being less than $10,000."

The applicable limitation amount is the single $40,000 (2025) figure, so it is reduced and floored first and only then halved for a separate filer. Only the threshold is halved before the phase-down.

The 2025 Schedule A State and Local Tax Deduction Worksheet follows the same order:

Line Instruction MFS treatment
1 Enter $40,000 not halved
5 Enter $500,000 ($250,000 if married filing separately) halved before the phase-down
6–8 Line 1 minus 30% of (line 4 − line 5) full 30% rate
9 Larger of line 8 or $10,000 not halved
10 Smaller of line 9 (half the amount on line 9 if married filing separately) or line 5d halved last

Worked case, 2025 MFS, MAGI $300,000: line 6 = $50,000, line 7 = $15,000, line 8 = $25,000, line 9 = $25,000, line 10 = $12,500. The old model computed $20,000 − $15,000 = $5,000.

For a separate filer with MAGI M in 2025, the cap is therefore max($40,000 − 0.3·max(0, M − $250,000), $10,000) / 2. Before this PR, the model computed max($20,000 − 0.3·max(0, M − $250,000), $5,000). That is the same expression with a 0.6 rate inside the halving, so the cap reached the $5,000 floor at $300,000 rather than at $350,000.

Changes

  • salt_cap.py: a separate filer's reduction is multiplied by the new phase_out.separate_fraction. The SEPARATE cap and floor parameters already hold half of $40,000 and half of $10,000, so halving the reduction gives exactly ½·max(L − r·e, F) = max(L/2 − (r/2)·e, F/2). The Schedule A worksheet computes the same quantity. The floor is now min(cap, floor): 164(b)(7)(B)(iii) limits the reduction and never lifts the cap. This matters only under reforms. On main, setting the cap to $0 for 2025–2029 still allowed $10,000.
  • New parameter gov.irs.deductions.itemized.salt_and_real_estate.phase_out.separate_fraction = 0.5 from 2025 (164(b)(6)(B)). It mirrors Axiom's us:statutes/26/164#married_separate_applicable_limitation_fraction.
  • phase_out/floor/amount.yaml: adds statute and worksheet (lines 9–10) references.
  • Builds on Add back income excluded under sections 911, 931 and 933 in modified AGIs (student loan interest and 11 siblings) #9817, which is merged in: the reduction still uses its 164(b)(7)(B)(iv) modified AGI (agi_plus_section_911_931_933_exclusions).

Invariants (stated and executed)

Write L for the applicable limitation amount, T for the threshold, F = $10,000 and e(M, t) = max(0, M − t). The statute gives C(M) = max(L − 0.3·e(M, T), F) for other filers and S(M) = max(L − 0.3·e(M, T/2), F)/2 for a separate filer. tests/core/test_salt_cap_invariants.py checks the following with Hypothesis, plus a deterministic grid across all five filing statuses on $5,000 MAGI steps:

  1. Differential: salt_cap equals the worksheet computed from the statute's dollar amounts, not the model's parameters, for 2024, 2025, 2026, 2027 (101% of 2026) and 2030.
  2. Shift identity: S(M) = C(M + T/2)/2. The identity the statute implies is a MAGI shift, not "half the joint cap at twice the MAGI". C(2M)/2 = max(L − 0.6·e(M, T/2), F)/2 is exactly the old, wrong model. A test built on that identity would have certified the bug.
  3. Split inequality: 2·S(M) ≥ C(2M), strictly while the phase-down runs, 0 < e(M, T/2) < (L − F)/0.3. Two equal-MAGI separate returns keep more cap than one joint return at their combined MAGI. The old model had equality everywhere.
  4. Bounds: min(cap, floor) ≤ salt_cap ≤ cap for the filing status.
  5. Monotone and Lipschitz: raising MAGI by d lowers the cap by between 0 and 0.3·d (0.15·d for a separate filer). The old MFS slope was 0.3.
  6. Accounting: salt_deduction = min(salt_cap, SALT paid limited to AGI less exemptions).
  7. Reform floor: with caps reformed below the floor ($0 joint, $6,000 single, $3,000 MFS in 2026), salt_cap equals the reformed cap at every MAGI.

These properties apply to every input in the tested years. The tests omit 2028–2029, whose inherited cap and threshold rounding remains unchanged; review r1 found that the rounded 2028 parameters can differ from the exact shift identity by $0.075.

Tests: fail before, pass after

YAML cases are in itemizing/salt_cap.yaml, the variable-named file. One existing case in salt_deduction.yaml encoded the old order and is corrected.

Case Main This PR
MFS 2025, MAGI $300,000 (review case) 5,000 ✗ 12,500
MFS 2025, MAGI $340,000: above the old $300,000 floor point, below the statutory $350,000 point 5,000 ✗ 6,500
MFS 2026, MAGI $320,000 5,000 ✗ 10,075
MFS 2025, MAGI $290,000 (existing case, corrected: salt_cap) 8,000 ✗ 14,000
MFS 2025, MAGI $350,000: statutory floor kink (control) 5,000 5,000
MFS 2025, MAGI $400,000: floor (control) 5,000 5,000
MFS 2025, MAGI $250,000: no excess (control) 20,000 20,000
Joint 2025, MAGI $550,000 (control) 25,000 25,000
Single 2026, MAGI $555,000 (control) 25,400 25,400
Head of household 2025, MAGI $700,000: floor (control) 10,000 10,000
MFS 2030, no phase-down (control) 5,000 5,000

The controls pass on both sides by design. Under baseline law, only MFS returns above the halved threshold change. Reformed caps below the floor also change for other filing statuses.

On main (f33a4b3, the first merge base), the YAML cases gave 4 failed and 13 passed, with the four ✗ rows above failing. The property tests gave 5 failed and 2 passed: Hypothesis, the 2025–2027 grids and the reform floor failed, and the 2024 and 2030 grids passed. Hypothesis minimized to the bug itself. At MFS MAGI $250,001 the model gives $19,999.70 where the law gives $19,999.85, and its slope is 0.30 per dollar where the law's is 0.15. On this branch: at merged head e6fe75c, the October 11, 2026 rerun passed all 10 salt_cap.yaml cases, 8 salt_deduction.yaml cases and 7 Python invariant tests (25 passes). Each file ran in its own foreground process, sequentially. The current YAML total is 18 rather than the historical 17 because it includes the retained #9817 modified-AGI regression. make format also passed with no formatting changes.

Hub microsimulation impact (current head)

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

Output 2026 change 2026 records changed (weighted)
salt_cap +691 8 (0)
salt_deduction +631 5 (0)
itemized_taxable_income_deductions +631 5 (0)
state_income_tax +0 1 (0)
income_tax -2 3 (0)
household_net_income +2 3 (0)
household_benefits +0 0 (0)

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

Axiom parity

axiom: TheAxiomFoundation/rulespec-us#1655 queued

us:policies/income_tax/salt_deduction_pipeline#federal_salt_deduction has the same defect: its MFS branch halves the 2026 cap and floor before the phase-down, and its tests expect 5,950 and 5,000 where the law gives 13,075 and 11,575. The hub filed rulespec-us#1655, with the verbatim law, the required change, a pasteable review_finding and companion tests from the same ground truth as these YAML cases. The statute-level parameters, including married_separate_applicable_limitation_fraction, are already encoded correctly in us/statutes/26/164.yaml.

Cost of the new Python test

On main (failing path, including Hypothesis shrinking), pytest reported 52.90 s; the timed command took 55.64 s wall time and 1.57 GB maximum RSS on an Apple M5 Max. The 30.5 s Hypothesis test dominates; the reform test (8.7 s) builds one reformed system, and each grid year takes under 1 s. Passing path: pytest reported 140.63 s; the complete foreground command took 313.03 s wall time, with 1,542,684,672 bytes maximum RSS (1.54 GB) in the assigned sandbox. The reform-floor test took 90.44 s and Hypothesis took 13.68 s. The recorded command was python -m pytest -q policyengine_us/tests/core/test_salt_cap_invariants.py --durations=10, using the prescribed Python environment and default pytest plugins. Wall time includes interpreter startup and shutdown, so these host timings should not be treated as a direct performance comparison with the historical failing run. It runs in the Rest (Python) core group (142 s before this PR, per the Makefile's timing notes).

Notes

  • CI's states-shard-3 failure on the first push came from main: ms_health_savings_account_adjustment.yaml, fixed by Update the Mississippi HSA test for joint-column combination (main is red) #9992. This branch now includes that fix. The Pull request workflow subsequently passed at reviewed head 4d23cd5cde9a62ac7b52610d3523f00424b6c4f5 on October 8, 2026, including Household API Partners. CI verification at the latest merged head is pending with the hub.
  • No partner expected outputs are expected to change. The four explicit MFS cases have 2026 incomes at or below $42,000, below the $252,500 threshold, and no partner case uses a cap reform.

🤖 Generated with Claude Code

MaxGhenis and others added 4 commits October 7, 2026 09:15
26 USC 164(b)(6)(B), as amended by P.L. 119-21 sec. 70120(a), limits a
married individual filing separately to half the applicable limitation
amount. 164(b)(7)(B) reduces that amount by 30% of modified AGI over the
threshold (half the threshold for a separate filer) and bars the reduction
from taking it below $10,000. The 2025 Schedule A State and Local Tax
Deduction Worksheet follows the same order: line 1 is $40,000 for every
filing status, line 5 halves only the threshold, line 9 applies the $10,000
floor and line 10 halves the result.

salt_cap applied the full 30% reduction to the already-halved $20,000 MFS
cap, so separate filers phased down twice as fast as the law allows: at
$300,000 MAGI in 2025 the model gave $5,000 instead of
(40,000 - 15,000) / 2 = $12,500. The new phase_out.separate_fraction
parameter (0.5) scales a separate filer's reduction, which is equivalent
to halving after the reduction because the SEPARATE cap and floor already
hold the halves.

The floor also raised any cap below it to $10,000, so a reform setting the
cap to $0 for 2025-2029 still allowed $10,000. 164(b)(7)(B)(iii) only stops
the reduction; the floor is now min(cap, floor).

Adds variable-named YAML cases and Hypothesis invariants (worksheet
differential, the shift identity S(M) = C(M + T/2) / 2, the split
inequality 2 S(M) >= C(2M), bounds, monotonicity, a Lipschitz bound and a
reform floor case), and corrects the one existing case that encoded the
old order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve salt_cap.py by keeping #9817's 164(b)(7)(B)(iv) modified AGI and
applying the separate-filer fraction to the reduction it produces.

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

Copy link
Copy Markdown
Contributor Author

Context from the session that opened this PR, for the US + core hub, which now owns it. Nothing here asks for a change.

  • First independent review: GPT-6.1 Sol, APPROVE at 4d23cd5 with no P1 and no new P2 (~/reviews/us-hub/reviews-gen/us-9979/review-r1.md). It agrees with the hub review at e6fe75c on the statutory MFS order and the algebra: max(L/2 − 0.15·e, F/2) = max(L − 0.3·e, F)/2. It also found no IRS guidance that reads the order differently: no Notices, FAQs or Pub. 17, and nothing in the 2026 draft Schedule A or the Form 1040-SS instructions.
  • Pre-existing P2 that both reviews raise: capped_property_taxes still uses the raw cap with no phase-down, and it feeds MD, MN, NE, OK and OR itemized deductions. It is split into its own task (chip task_e5d512a9), which researches each state's conformity, fixes, tests and hands off to this hub. This PR stays unchanged on it.
  • Evidence: statute and worksheet excerpts with source checksums, the before-fix logs (main at f33a4b3: YAML 4 failed, 13 passed; property tests 5 failed, 2 passed) and the run scripts are in ~/reviews/us-hub/salt-cap-mfs/evidence/.
  • This session's own microsim never ran: it waited on the heavy lock and the session ended. The hub's queued impact run is the only one; no numbers from this session exist.
  • d1068 (file the Axiom issue) is moot now that rulespec-us#1655 is filed under d1142. It stays open until it parks on 10/14, because only Max can rule on it.

🤖 Generated with Claude Code

@MaxGhenis MaxGhenis closed this Oct 11, 2026
@MaxGhenis MaxGhenis reopened this Oct 11, 2026
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 11, 2026 23:20
@MaxGhenis
MaxGhenis merged commit 300e1cc into main Oct 12, 2026
43 of 47 checks passed
@MaxGhenis
MaxGhenis deleted the fix-salt-cap-mfs-halving branch October 12, 2026 12:04
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#9979 at e6fe75cf8e39224e5d219092e447b92df3ab531d (squash): Halve the married-filing-separately SALT cap after the phase-down, not before.

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