Repository navigation
Halve the married-filing-separately SALT cap after the phase-down, not before - #9979
Merged
Merged
Conversation
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>
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.
🤖 Generated with Claude Code |
Contributor
Author
|
US + core hub merge audit, us#9979 at
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
salt_capapplied 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):
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:
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 newphase_out.separate_fraction. TheSEPARATEcap 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 nowmin(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.gov.irs.deductions.itemized.salt_and_real_estate.phase_out.separate_fraction= 0.5 from 2025 (164(b)(6)(B)). It mirrors Axiom'sus:statutes/26/164#married_separate_applicable_limitation_fraction.phase_out/floor/amount.yaml: adds statute and worksheet (lines 9–10) references.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.pychecks the following with Hypothesis, plus a deterministic grid across all five filing statuses on $5,000 MAGI steps:salt_capequals the worksheet computed from the statute's dollar amounts, not the model's parameters, for 2024, 2025, 2026, 2027 (101% of 2026) and 2030.salt_cap≤ cap for the filing status.salt_deduction= min(salt_cap, SALT paid limited to AGI less exemptions).salt_capequals 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 insalt_deduction.yamlencoded the old order and is corrected.salt_cap)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 heade6fe75c, the October 11, 2026 rerun passed all 10salt_cap.yamlcases, 8salt_deduction.yamlcases 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 formatalso passed with no formatting changes.Hub microsimulation impact (current head)
A real microsimulation on the default dataset, main at the merge base
fdd7062905against this PR's heade6fe75cf8e, for 2026. The runs went one at a time under the shared heavy-job lock. Sums are weighted.salt_capsalt_deductionitemized_taxable_income_deductionsstate_income_taxincome_taxhousehold_net_incomehousehold_benefitsComparison 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_deductionhas 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 pasteablereview_findingand companion tests from the same ground truth as these YAML cases. The statute-level parameters, includingmarried_separate_applicable_limitation_fraction, are already encoded correctly inus/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)coregroup (142 s before this PR, per the Makefile's timing notes).Notes
states-shard-3failure 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 head4d23cd5cde9a62ac7b52610d3523f00424b6c4f5on October 8, 2026, including Household API Partners. CI verification at the latest merged head is pending with the hub.🤖 Generated with Claude Code