Skip to content

Fix Montana's 2021–2023 SALT caps and tax elections - #10002

Open
MaxGhenis wants to merge 7 commits into
mainfrom
fix-mt-salt-cap-joint-return
Open

MaxGhenis wants to merge 7 commits into
mainfrom
fix-mt-salt-cap-joint-return

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

For 2021–2023, we cap Montana's state and local tax deduction at $10,000 per joint return and $5,000 per separate column. Previously, spouses used the joint cap in both calculations.

We pool eligible taxes per return in mt_salt_deduction and assign the deduction to the head. We calculate separate columns with mt_salt_deduction_indiv, reusing each person's city tax and retaining tax-unit sums.

Law

Montana incorporated federal itemized deductions while excluding state income taxes under former MCA 15-30-2131(1)(a).

Taxpayers choose general sales taxes or local income taxes under 26 USC 164(b)(5)(A). We follow that election over cumulative lines 5a and 5b on 2021, 2022 and 2023 Form 2.

Spouses filing separately on the same form each have a return under the 2023 allocation instructions, pages 12–13. We record the conflict as us-mt:salt-sales-income-election with official excerpts.

Invariants

  • We calculate each return's or column's deduction from real estate taxes plus the larger sales tax or local income tax amount, subject to its filing-status cap.
  • For nonnegative tax inputs, each deduction stays between zero and its cap. Raising real estate taxes never lowers a deduction.
  • Swapping spouses preserves the joint total and swaps their columns.
  • Spouses can deduct more through separate columns when they choose different tax categories.
  • From 2024, we preserve main's per-person formula and set separate deductions to zero.

Methodology choices

  • Split tax-unit sales taxes and residual local income taxes equally between spouses. Assign city taxes to their payers.
  • Pool dependents' taxes on the joint return; give dependents no separate column.
  • Retain reported real estate taxes without optimizing jointly paid taxes between spouses.
  • Choose the larger eligible tax category after allocation. Leave value-based personal property taxes unmodeled because we lack an input.
  • The schedule parameter starts in 2021 and applies to earlier years. We test 2021–2023.

Microsimulation impact

To be measured before merge.

Tests

CI passed 36 jobs, 214 cases across 21 required files, and 103 source-conflict checks.

Cached local checks passed 44 unique cases across five required files: joint SALT 14, separate columns 17, Kansas City 1, St. Louis 1 and employee taxes 11. The 17 separate-column cases also passed uncached. 51 source-conflict candidate checks passed.

We retain seven property tests, up to six examples per Hypothesis test and 150 seeded tax units, including residual taxes and surviving spouses. When any unit has residual local tax, we set all units' local tax inputs.

The full suite runs this file once in Rest Python's policy group. All seven passed among 1,873 CI cases in 3.356 seconds, excluding shared startup. The group took 19:08.70 wall with 11.92 GiB peak RSS.

The local property attempt timed out after 900.8 seconds (903.471 seconds wall overall), reporting no assertion results; peak capped memory charge was 1,174.782 MiB. We did not complete local validation for 16 required files within 6 hours. No runtime mutant verdict is available.

axiom: former MCA 15-30-2131(1)(a) (Itemized Deductions Schedule line 5) TheAxiomFoundation/rulespec-us#1520 queued

🤖 Generated with Claude Code

…5,000 per separate column

Form 2, Itemized Deductions Schedule, line 5 (2021-2023) adds lines 5a
through 5d and caps the total at $10,000 (single, head of household,
married filing jointly) or $5,000 (married filing separately).

- mt_salt_deduction: a joint return's taxes are pooled and capped once,
  on the head. It had capped each spouse separately, so a couple with
  $8,000 each deducted $16,000.
- mt_salt_deduction_indiv (new): each column of a return filed
  separately on the same form (status 2a) is capped at $5,000. The
  separate-columns path had used the tax unit's joint cap ($10,000).
- Line 5a: add the tax unit's general sales taxes (state_sales_tax,
  local_sales_tax), split equally between a couple's columns.
- From 2024 (schedule discontinued) the per-person amounts are as
  before; the 2024 Worksheet A change is separate.

The 2021-2023 switch is gov.states.mt.tax.income.deductions.itemized.
state_specific_deduction_applies, byte-identical to the file in #9903
and #9947.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 7, 2026 17:11
…ction

Review r1 found that local_income_tax is not always zero for a Montana
household: PolicyEngine levies the Kansas City and St. Louis earnings
taxes and the Philadelphia wage tax on earnings in those cities without a
residence test. Line 5 adds "any local income taxes that you paid".

- mt_salt_deduction: the joint return's line 5 adds local_income_tax.
- mt_salt_deduction_indiv: each column takes the tax unit's local income
  tax in proportion to the person's own earnings taxed by those cities
  ("Deductions that are attributable to only one spouse must be claimed
  by that spouse"); without such earnings it is divided like the sales
  taxes.
- YAML: Kansas City cases for a single return, a joint return at the
  cap, and two separate-column cases. The Hypothesis file adds Kansas
  City earnings to its draws and reference calculation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r2: dividing the tax unit's local income tax by each spouse's
share of taxable city earnings is exact only when everyone pays the same
rate with no credit. PolicyEngine sets Philadelphia's rate per person and
subtracts St. Louis credits per person.

- New Person variables mo_kansas_city_earnings_tax_person,
  mo_st_louis_earnings_tax_person and pa_philadelphia_wage_tax_person
  hold each person's own tax, with the existing formulas. The tax-unit
  variables now add them up, so their values do not change.
- mt_salt_deduction_indiv: each column takes its own spouse's city tax.
  Any other local income tax of the tax unit (none for a Montana
  household) is divided like the sales taxes.
- YAML: first tests for the three city taxes; Montana cases for a
  Philadelphia reduced rate, a St. Louis credit, two cities with one
  column at the cap, and a joint return with two cities.
- Hypothesis: St. Louis earnings and per-person credits in the draws.

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

DTrim99 commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Cap Montana's 2021–2023 state and local tax deduction once per joint return and at $5,000 per separate column

Summary

The fix is right in law. I checked the 2021, 2022 and 2023 Form 2 Itemized Deductions Schedule, line 5 (PDF p. 7 each year). It reads: "Add lines 5a through 5d, enter the total here, but not more than $10,000 if your status is single, head of household or married filing jointly; or $5,000 if you are married filing separately". The filing-separately rules say a status 2a return is split "between Columns A and B, with each column constituting its own return" (2021 instructions PDF p. 10, 2022 p. 11, 2023 p. 12).

  • A joint return now caps the couple's combined line 5 once, at $10,000. Main capped each spouse separately.
  • Each 2a column is now capped at $5,000. Main capped each column at the joint $10,000.
  • Lines 5a (general sales taxes) and 5b (local income taxes) are now included.
  • Per year and filing status: single, head of household and joint get $10,000; separate forms (PE SEPARATE) and each 2a column get $5,000; a federal qualifying surviving spouse, who files Montana status 1, gets $10,000.
  • 2024+ is unchanged.

I hand-checked 23 expectations and the PR body's household table.

The branch is 729 commits behind main and conflicts with it in two YAML test files. I resolved them the obvious way (keep both renames) and ran a merged tree locally:

  • the PR's and the affected Montana YAML files: 100 cases pass;
  • the source-conflict and other relevant code-health guards: pass;
  • main's newer Montana property tests: pass;
  • the PR's own Hypothesis file: passes.

Two points remain open, both SHOULD:

  1. A new term in mt_salt_deduction_indiv is not pinned by any test.
  2. One reference tuple lists the same PDF twice, which the CLAUDE.md reference rules don't allow.

Critical (must fix)

None.

Should address

  1. mt_salt_deduction_indiv.py:61 and :64: the other_local_tax term is not pinned.

    • The term is other_local_tax = max_(0, local_income_tax - person.tax_unit.sum(own_local_tax)). Each married column then adds sales_tax_share * other_local_tax.
    • No YAML case and no Hypothesis draw ever makes local_income_tax larger than the sum of the three person-level city taxes. The differential in test_mt_salt_deduction_invariants.py doesn't model this term either. So deleting it (using sales_tax_share * sales_tax) fails no test.
    • It is reachable: local_income_tax can be entered directly, and the NYC, Indiana county and Wilmington taxes it adds have no person-level split here.
    • Fix: add a mt_salt_deduction_indiv.yaml case for 2021–2023. Example: an MT couple with tax-unit input local_income_tax: 2_000, no city earnings and real estate taxes of 4,500 and 0. Expected columns: [min(4,500 + 1,000, 5,000), 1,000] = [5,000, 1,000].
    • Alternatively, drop the term. For a Montana household PolicyEngine levies no other local income tax, as the PR body says.
    • Side note: the instructions say deductions "attributable to only one spouse must be claimed by that spouse" (2023 instructions PDF p. 13). An equal split of another city's income tax is a convention, not that rule, so say so in the comment if you keep the term.
  2. mt_salt_deduction_indiv.py:20–23: the same document is listed twice.

    • The 2023 Form 2 instructions appear at #page=12 and again at #page=37.
    • CLAUDE.md: "One reference entry per source document; URLs that differ only by #page= are one source". Also: "Variable reference tuples have no title: put a # PDF pages 61-62, 67 comment above a multi-page href."
    • The code comments also quote PDF p. 13 ("Deductions that are attributable to only one spouse must be claimed by that spouse"), which no reference covers.
    • Fix: one entry, ...2023_..._Instructions.pdf#page=12, with # PDF pages 12-13, 37 above it. Optionally, add the 2021 (PDF p. 10) and 2022 (PDF p. 11) instructions too, since the variable applies to 2021–2023. Their allocation text is identical.

Suggestions

  1. Inert mt_salt_deduction: 0 inputs once merged with main.

    • Main's new separate-column cases set mt_salt_deduction: 0 to isolate other lines: the split-mortgage case and both premium cases. That is 6 inputs in mt_itemized_deductions_indiv.yaml and 6 in federal_itemization/mt_itemized_deductions_for_federal_itemization_indiv.yaml.
    • After this PR those inputs no longer reach the columns. The cases still pass only because the computed mt_salt_deduction_indiv is 0 there.
    • Rename them to mt_salt_deduction_indiv when resolving the merge. Add mt_salt_deduction_indiv to ZERO_PERSON_COMPONENTS in main's tests/core/test_mt_mortgage_interest_federal_limit_properties.py for the same reason. It passes on the merged tree today.
  2. Reuse the existing separate-filing status.

    • cap = where(married, p.cap.SEPARATE, p.cap[filing_status]) rebuilds the mapping that state_filing_status_if_married_filing_separately_on_same_return already provides (JOINT to SEPARATE, others unchanged).
    • Montana's other column variables use that variable (mt_standard_deduction_indiv, mt_federal_income_tax_deduction_indiv, mt_regular_income_tax_indiv). p.cap[state_filing_status_if_married_filing_separately_on_same_return] would match them.
  3. Dependents are treated asymmetrically.

    • On a joint return, line 5 now sums every tax-unit member's real estate taxes and, through local_income_tax, their city taxes. A 2a column drops dependents' amounts entirely.
    • Main's joint itemized total already excludes dependents' mortgage and investment interest ("A joint return deducts the interest either spouse paid, excluding dependents").
    • Line 5c is real estate taxes "that you paid ... on real estate that you own" (2023 instructions PDF p. 37). Consider counting only the head and spouse on the joint return too, or record this as a methodology note. It matches the federal salt convention, and the PR body documents it, so this is not a finding.
  4. Sales taxes and local income taxes together (optional).

    • Under 26 USC 164(b)(5)(A), electing general sales taxes applies 164(a) "without regard to the reference to State and local income taxes". Former MCA 15-30-2131(1)(a) incorporates the section 161 items.
    • Form 2 nonetheless lists 5a and 5b as separate lines with no election, and the PR follows the form.
    • The overlap needs an entered sales tax for a Montana household (PE's table amount is 0) plus a Kansas City, St. Louis or Philadelphia tax, so the reach is negligible. If you read the statute as importing the election, record a candidate. Otherwise a one-line comment noting that the form allows both would help the next reader.
  5. Joint payments allocated freely (follow-up). The instructions let spouses allocate jointly paid taxes "in any proportional amount" (2023 instructions PDF p. 13). With per-person real_estate_taxes, a 2a couple with 8,000/2,000 deducts 7,000. Allocating jointly paid taxes up to each column's $5,000 would give 10,000. This is documented as a methodology choice in the body and the axiom issue; just flagging it as a follow-up.

  6. PR body housekeeping.

  7. Axiom issue visibility. Use a reform instead of a parameter to simulate taxsim's incorrectnesses #1520's title and body cover only line 1 and the standard deduction. Line 5 is in an extension comment. Consider adding "line 5" to the title or a pointer in the body, so the dispatcher sees it.

Verified correct

Law

  • 2021–2023 Form 2 page 7, line 5, gives the cap text quoted above (identical each year).
  • Line 5 instructions are at 2021 PDF p. 32, 2022 p. 34 and 2023 p. 37: "State and Local Taxes Limited to $10,000 ... excluding state income taxes". 5a, 5b, 5c and 5d are cumulative lines. Line 6 (vehicle registration fees) and line 7 (livestock fees) are outside the cap and untouched.
  • Former MCA 15-30-2131(1)(a) (MCA 2021) allows the IRC section 161/211 items except "(ii) state income tax paid". The section is repealed in MCA 2023 (Secs. 65, 70(1), Ch. 503, L. 2021). The $10,000/$5,000 amounts are 26 USC 164(b)(6) for 2018–2024.
  • 2023 instructions PDF p. 12: status 2a "allocation is done between Columns A and B, with each column constituting its own return". PDF p. 13 has the attributable-deduction and joint-account allocation rules. The same text is in 2021 (p. 10) and 2022 (p. 11).

Formulas

  • mt_salt_deduction (2021–2023) is is_head * min_(sum over the unit of 5a + 5b + 5c, cap[filing_status]), and the joint aggregates add it over the unit. 2024+ keeps main's per-person amount.
  • mt_salt_deduction_indiv caps married columns at SEPARATE and other filers at their own status's cap. It splits sales tax by spouse_allocation_rate (0.5), gives each column its own spouse's city tax and zeroes dependents. Its defined_for eligibility ends in 2024, so it is zero from 2024.
  • Vectorized throughout; the state_specific_deduction_applies switch is a scalar parameter.
  • Entities and periods are correct: Person for the deductions and city taxes, TaxUnit sums through adds, all YEAR.
  • No new state-gated int or enum key (filing_status is not state-gated), so the NH Add New Hampshire state supplementation for 2026 #10156 pattern doesn't apply.
  • state_specific_deduction_applies.yaml is byte-identical to main's (blob 8b6d633).

Hand checks

mt_salt_deduction.yaml:

  1. HoH 2022, 12,000: min(12,000, 10,000) = 10,000.
  2. Joint 2023, 8,000 + 8,000: min(16,000, 10,000), [10,000, 0]. mt_itemized_deductions_joint = [10,000, 0].
  3. Joint 2022: 6,000 + 2,500 = 8,500.
  4. Joint 2021: 9,000 + 1,500 + 300 = 10,800, capped to 10,000.
  5. Single 2023: 2,000 + 1,800 = 3,800.
  6. Single 2023 with Kansas City tax: 2,000 + 1% × 100,000 = 3,000.
  7. Joint 2022: 9,500 + 1,000 = 10,500, capped to 10,000.
  8. Joint 2023: 4,000 + 1,000 + 3.44% × 100,000 = 8,440. The nonresident rate is 0.0344 from 2022-07-01, which is in effect at 2023-01-01.

mt_salt_deduction_indiv.yaml:
9. [min(8,000, 5,000), min(8,000, 5,000)] = [5,000, 5,000]; mt_itemized_deductions_indiv = [5,000, 5,000].
10. [5,000, 2,000].
11. 4,800 + 500 = 5,300, capped to 5,000; column B 500.
12. Dependent case: [3,000, 1,000, 0].
13. Single: min(12,000, 10,000) = 10,000; 8,000.
14. SEPARATE: min(6,000, 5,000) = 5,000.
15. 2024: [0, 0].
16. [3,000 + 1,000, 3,000] = [4,000, 3,000].
17. [min(4,500 + 1,000, 5,000), 500] = [5,000, 500].
18. [3.44% × 100,000, 1.5% × 100,000] = [3,440, 1,500].
19. [1,000 − 1,000, 1,000] = [0, 1,000].
20. [min(4,000 + 1,000, 5,000), 3,440] = [5,000, 3,440].

City tax files:
21. Kansas City 2024: [1,000, 500], total 1,500.
22. St. Louis: [max(0, 1,000 − 1,200), 1,000] = [0, 1,000], total 1,000, before credit 2,000.
23. Philadelphia 2024: 3,750 + 3,440 + 600 = 7,790.

PR body table:

  • 2023 joint itemized: 26,000 − 6,000 = 20,000. Each column falls by 3,000.
  • 2023 mt_income_tax: 6,436.61 − 6,031.61 = 405 = 6,000 × 6.75%.
  • 2021: 4,274.85 − 3,860.85 = 414 = 6,000 × 6.9%.

Other checks

  • Changelog: a top-level changelog.d/fix-mt-salt-cap-joint-return.fixed.md, type fixed.
  • Tests are in variable-named YAML files.
  • The Python file has a stated purpose (vectorized mixed units, monotonicity, head swap, 2024+ equality), an independent reference calculation with caps from the forms, and reported cost.
  • No partner-test files are touched.
  • Callers: on main mt_salt_deduction is read only by the four Montana itemized aggregates. The two separate-column ones switch to mt_salt_deduction_indiv, and the two joint ones sum the head-only amount correctly.
  • No new cycle: the city taxes depend only on their inputs.
  • The new comments add nothing that matches the source-conflict ratchet pattern.

Axiom line assessment

axiom: former MCA 15-30-2131(1)(a) (Itemized Deductions Schedule line 5) TheAxiomFoundation/rulespec-us#1520 queued: valid.

  • Use a reform instead of a parameter to simulate taxsim's incorrectnesses #1520 is OPEN and labelled pe-parity.
  • Its 2026-10-07 extension comment, last edited 2026-10-08, names this PR and head 0a4846c7b1. It gives:
    • the module path (us-mt/statutes/15-30-2131/1/a.yaml, state and local tax component, composing us/statutes/26/164.yaml);
    • the corpus pin and rows;
    • verbatim Form 2 line 5, the instructions, the MCA and P.L. 119-21 sec. 70120(a);
    • required outputs 5–9;
    • a review_finding;
    • 14 companion cases from the form's line 5 arithmetic, matching this PR's YAML (including the person-level city tax cases at this head).
  • The blocker is recorded. The TY2018–2024 text of 164(b)(6) is not in the corpus, and each encoder run needs approval. The 2026-10-19 comment shows the Form 2 and MCA 2021 rows have since been ingested.
  • See suggestion 7 on the title.

CI status

  • All 26 checks pass at 0a4846c7b1 (run 37795653696, 2026-10-08). Nothing is pending.
  • That run used the CI configuration of early October. It predates main's Rest sharding, the YAML-only batch collection and several new guards, so it does not cover today's main.
  • On a locally merged tree (main e53b426082 plus this head, with the two YAML conflicts resolved), run one process at a time:
    • YAML: 11 files, 100 cases pass. These are mt_salt_deduction, mt_salt_deduction_indiv, both separate-column and both joint itemized files, mt_tax_unit_itemizes, Montana integration.yaml and the three city tax files.
    • Code-health guards: test_source_conflicts.py, test_reference_urls.py, test_no_group_variables_in_person_adds.py and test_no_builtin_sum_over_entity_calls.py pass. One synthetic tokenizer probe in test_source_conflicts.py (test_unscannable_files_fail_closed) fails locally under Python 3.11; it reads no repository file and is unrelated to this PR.
    • Main's Montana tests: tests/core/test_mt_mortgage_interest_federal_limit_properties.py (6 pass), test_state_income_tax_accounting_invariants.py::test_formula_states_keep_the_properties_in_2023 (pass, including at least one Montana unit filing separately) and test_state_tax_head_spouse_swap_invariance.py::test_seeded_population for 2021–2023 (3 pass).
    • The PR's own file: test_mt_salt_deduction_invariants.py (7 pass).
  • Other main-side guards don't apply: Integrate modeled state unemployment insurance into income flows #9558, Count military SBP annuities in state benefit income lists #10317 and Count each Social Security benefit once in Pennsylvania CCW countable income #10408 (income source lists), test_programs_registry and the reform combo budget.

Branch status

Verdict: REQUEST_CHANGES

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: 0 critical, 2 should (the other_local_tax term is unpinned; the 2023 instructions are cited twice instead of one entry with a page-list title). The cap logic matches Form 2 line 5 for 2021-2023 and every filing status, and the merged tree passes locally. Branch note: 729 behind, with conflicts in two Montana YAML files (keep both renames). Full report in the comment above.

MaxGhenis and others added 3 commits November 13, 2026 08:02
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis changed the title Cap Montana's 2021–2023 state and local tax deduction once per joint return and at $5,000 per separate column Fix Montana's 2021–2023 SALT caps and tax elections Nov 13, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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.

2 participants