Skip to content

Let Utah military retirees claim the military retirement credit when no retirement credit applies - #10702

Open
MaxGhenis wants to merge 4 commits into
mainfrom
fix-ut-military-retirement-credit-choice
Open

MaxGhenis wants to merge 4 commits into
mainfrom
fix-ut-military-retirement-credit-choice

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

What was wrong

  1. ut_claims_retirement_credit returned True whenever the retirement credit (Utah Code 59-10-1019) was at least the Social Security benefits credit (59-10-1042), including when both were zero. ut_military_retirement_credit_eligible requires ~ut_claims_retirement_credit, so every military retiree born after 1952 who had no Social Security credit lost the military retirement credit (59-10-1043). Existing military-credit tests passed because they set ut_claims_retirement_credit as an input.
  2. The credit had its own rate parameter, military_retirement/rate.yaml, which copied the income tax rate through 2025 and stopped there, so PolicyEngine-US applied 2025's 4.5% in 2026. 59-10-1043(2)(a) uses "the percentage listed in Subsection 59-10-104(2)", which SB 60 (2026) sets at 4.45%.
  3. That parameter's first value (2021) also extended backward, so PolicyEngine-US gave the credit in 2020 and earlier. The 2020 TC-40 instructions have no military retirement credit; the 2021 instructions introduce it (code AJ, at 4.95%).

A married filer, age 45, with $30,000 of military retired pay and $60,000 of wages, TY2026 (PolicyEngine/veterans-day-2026, scripts/trace_state.py UT working_45):

ut_claims_retirement_credit ut_military_retirement_credit Utah income tax
Before (2.105.0) true 0 2,759
After (this branch) false 1,335 (4.45% × 30,000) 1,424

With a $30,000 private pension in place of the military pay, the same household owes 2,759 on both versions. A retired couple, ages 67 and 65, with $45,000 of military retired pay and $30,000 of Social Security (retired_67) owes the same on both: it has a Social Security credit, so the tie never arose, and its liability caps the military credit below the amount at either rate.

The law

  • 59-10-1019(3)(b): a claimant may not claim the retirement credit "if a tax credit under Section 59-10-1042 or 59-10-1043 is claimed on the claimant's return for the same taxable year."
  • 59-10-1042(3)(b) and 59-10-1043(3)(b): a claimant who claims a 59-10-1019 credit may claim neither. Nothing bars claiming 1042 and 1043 together.
  • 2025 TC-40 instructions, codes 18, AH and AJ (p. 21, p. 23), say the same.
  • 59-10-1043(2): the credit equals "the product of: (a) the percentage listed in Subsection 59-10-104(2); and (b) the amount of military retirement pay that is included in adjusted gross income on the claimant's federal income tax return for the taxable year."

Change

  • New ut_military_retirement_credit_max: the 59-10-1043(2) amount, gov.states.ut.tax.income.rate times the filer's and spouse's military retirement income. Only the new in_effect parameter gates it, so the choice can read it without a cycle. ut_military_retirement_credit_potential now adds it under the existing eligibility gate.
  • ut_claims_retirement_credit = retirement_credit_max > 0 and retirement_credit_max >= ss_benefits_credit_max + military_retirement_credit_max. A positive retirement credit still wins ties.
  • This branch deletes military_retirement/rate.yaml; the new military_retirement/in_effect.yaml reads false before 2021 and true from 2021.

Invariants (for every input):

  • the claimed set is the larger of {1019} and {1042, 1043};
  • a retirement credit of $0 never blocks 1042 or 1043;
  • from 2021 the 1043 amount equals the 59-10-104(2) rate times the filer's and spouse's military retirement income, and before 2021 it is 0;
  • with no military retirement income, the choice matches the old one except when both credits are 0.

The YAML tests cover each boundary. I added no Hypothesis test, following this repo's YAML-first test policy.

Tests

  • ut_claims_retirement_credit.yaml: test 4 (0 vs 0) now expects false; three new cases weigh the military credit (alone, combined with the Social Security credit, and outweighed by the retirement credit).
  • New ut_military_retirement_credit_max.yaml: the TY2025 amount (1,350), survivor benefits (450), no credit in 2020, and the TY2026 amount at 4.45% (1,335).
  • ut_military_retirement_credit.yaml: two integration cases with no ut_claims_retirement_credit input (a 45-year-old gets the credit; a 76-year-old with $4,000 of military pay takes the $450 retirement credit instead).
  • tests/core/test_dependent_retirement_income_state_invariants.py: its Utah comment now describes the new choice; its 4 cases pass unchanged.
  • Before (2.105.0 site-packages): 6 of the 14 cases in the two edited test files fail. After, at a3bf81b: all 581 cases under tests/policy/baseline/gov/states/ut pass.

axiom: us-ut/statute/59-10-1043 queued (TheAxiomFoundation/rulespec-us#2074)

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits November 8, 2026 00:46
…no retirement credit applies

ut_claims_retirement_credit returned True whenever the retirement credit
(59-10-1019) was at least the Social Security benefits credit (59-10-1042),
including when both were zero. Because ut_military_retirement_credit_eligible
requires ~ut_claims_retirement_credit, every military retiree born after 1952
without a Social Security credit lost the military retirement credit
(59-10-1043): a 45-year-old with $30,000 of military retired pay paid $1,350
too much Utah tax in 2025.

59-10-1019(3)(b) bars the retirement credit when a 1042 or 1043 credit is
claimed, and 1042(3)(b) and 1043(3)(b) bar those credits only alongside 1019,
so a filer chooses between the retirement credit alone and the other two
together. The choice now compares the retirement credit with the sum of the
Social Security and military retirement credits, and a retirement credit
worth nothing is never claimed. A new ut_military_retirement_credit_max holds
the unconditional amount so the comparison has no cycle. The credit rate is
zero before 2021, when 59-10-1043 took effect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59-10-1043(2)(a) sets the credit at the percentage listed in 59-10-104(2).
The separate military_retirement/rate parameter copied that rate through
2025 but not the 4.45% rate for 2026, so the 2026 credit used 4.5%. The
credit now reads gov.states.ut.tax.income.rate, as the Social Security
benefits credit already does, gated by a new in_effect parameter that
starts in 2021.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits November 8, 2026 00:55
…ry retirement credit

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit to PolicyEngine/veterans-day-2026 that referenced this pull request Nov 8, 2026
…ice-date inputs, and run on 2.105.1 plus the Utah and Montana fixes

The 45-year-old served 2001 to 2020 and the 67-year-old 1977 to 1996. In the
military scenario the script sets retired_from_federal_government and
Kentucky's months of service before 1998, and Oregon's subtraction for the
share of service before October 1, 1991. The outputs come from a local
PolicyEngine-US checkout (bfeef52eb9): 2.105.1 plus PolicyEngine/policyengine-us#10702
and #10713. The README documents the households, data coverage and limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review November 9, 2026 16:49
@MaxGhenis
MaxGhenis requested a review from DTrim99 November 9, 2026 16:50
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

David, can you check how the PR picks between Utah's retirement credits? Utah lets a filer claim either the retirement credit or the Social Security and military retirement credits together (59-10-1019(3)(b)). The PR picks the retirement credit when it's positive and at least the other two combined. It also reads the military credit's rate off the income tax rate, 4.45% in 2026 under SB 60, so I'd like your eye on that too. If you can look by Tuesday afternoon, we can merge it before the Veterans Day post (PolicyEngine/policyengine-app-v2#1523).

@codecov

codecov Bot commented Nov 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (aae7f98) to head (a3bf81b).
⚠️ Report is 93 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main    #10702   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            1         3    +2     
  Lines           37        38    +1     
=========================================
+ Hits            37        38    +1     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@DTrim99

DTrim99 commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Let Utah military retirees claim the military retirement credit when no retirement credit applies

Summary

The PR fixes three real problems with Utah's military retirement credit (Utah Code 59-10-1043):

  • The retirement-credit election blocked the military credit for retirees with neither a retirement credit nor a Social Security credit. More broadly, it blocked it whenever a positive $450 retirement credit beat the Social Security credit alone.
  • The credit had its own rate parameter, which stopped at 4.5% (2025). The statute instead uses the 59-10-104(2) rate, which SB 60 sets at 4.45% for 2026.
  • That parameter's first value extended back before 2021.

The new election claims the retirement credit only when it is positive and at least the Social Security and military credits combined. That matches 59-10-1019(3)(b), 59-10-1042(3)(b) and 59-10-1043(3)(b), and the TC-40 instructions for 2021 through 2025. Every TY2024–2026 household I traced comes out right.

One critical problem remains in TY2021–2023, years the PR claims (in_effect turns on in 2021). Before 2024, Utah's nonrefundable credit list does not include ut_military_retirement_credit, so the credit never reduces tax in those years. Because the election now weighs the military credit, these filers also lose the retirement credit they had on main. A 75-year-old with $30,000 of military retired pay owes $625.35 for TY2023 at this head. Main gives $300.35, and the law gives $0.

Critical

1. TY2021–2023: the election picks the military credit, which is never applied, so the PR raises tax for older military retirees. File: policyengine_us/parameters/gov/states/ut/tax/income/credits/non_refundable.yaml, lines 26-36.

  • The gap. The 2021-01-01 and 2022-01-01 lists omit ut_military_retirement_credit. Only the 2024-01-01 list (line 44) has it: the original encoding started the rate in 2021 but the list entry in 2024. ordered_capped_state_non_refundable_credits sums only listed credits. So at this head ut_military_retirement_credit computes (for example 625.35 in 2023), but ut_non_refundable_credits is 0.
  • The law covers 2021-2023:
    • 2021 S.B. 11 (which enacted the credit), Section 4: "This bill has retrospective operation for a taxable year beginning on or after January 1, 2021" (https://le.utah.gov/~2021/bills/sbillenr/SB0011.htm).
    • The 2021 TC-40 instructions list "Military Retirement Credit" among the line 24 apportionable nonrefundable credits (https://files.tax.utah.gov/tax/forms/2021/tc-40inst.pdf#page=11). Code "AJ Military Retirement Credit" is in the TC-40A Part 3 code list on page 19, and the worksheet is on page 22.
    • The 2022 instructions (pages 19 and 22) and the 2023 instructions (pages 20 and 23) say the same.
  • Probe, head against main. Single filer, age 75, $30,000 of military retired pay, no other income:
Year Retirement credit max Military credit max Head claims retirement credit Head UT tax Main UT tax Law
2021 325 1,485 (4.95%) false 823.77 498.77 0
2022 325 1,455 (4.85%) false 760.88 435.88 0
2023 325 1,395 (4.65%) false 625.35 300.35 0
2024 325 1,365 (4.55%) false 0 207.52 0
  • Arithmetic. The retirement credit is 450 − 0.025 × (30,000 − 25,000) = 325. Under the law the filer elects code AJ (1,395 > 325 in 2023), and AJ is capped at the 625.35 liability, so tax is 0. The 2024 row shows the PR's fix working once the credit is on the list.
  • Fix:
    • Add ut_military_retirement_credit to the 2021-01-01 and 2022-01-01 lists. Placement does not change totals, because the liability cap applies to the total.
    • Add a TY2021–2023 household case on ut_income_tax or ut_non_refundable_credits, for example the 2023 case above with ut_income_tax: 0. None of the PR's tests reach tax before 2024, so the 2020 zero case and in_effect alone cannot catch this.

Should address

1. Reference rule: one source is cited twice. ut_claims_retirement_credit.py lines 22-23 cite the 2025 TC-40 instructions twice, differing only by #page=21 and #page=23.

  • CLAUDE.md says "One reference entry per source document; URLs that differ only by #page= are one source". It also says "Variable reference tuples have no title: put a # PDF pages 61-62, 67 comment above a multi-page href."
  • Fix: keep one href, https://files.tax.utah.gov/tax/forms/2025/tc-40inst.pdf#page=21, with # PDF pages 21, 23 on the line above it.

Suggestions

  1. Cite the source of the 2021 start. in_effect.yaml line 4 says 59-10-1043 applies to taxable years beginning on or after January 1, 2021. That date comes from 2021 S.B. 11 Section 4 (retrospective operation). The cited statute page is the 2022 version and does not show it. Consider adding the enrolled bill as a reference. The cited 2021 instructions page already corroborates the value.
  2. Flag the deleted parameter. Deleting gov.states.ut.tax.income.credits.military_retirement.rate removes a parameter path that saved policies or API reforms may use. A sentence in the changelog would help, or a removed fragment next to the fixed one, saying the credit now follows gov.states.ut.tax.income.rate.
  3. Active-duty survivors (pre-existing, out of scope). The TC-40 instructions (2021 page 22, 2025 page 23) say: "Do not take this credit if you receive survivor benefits for a service member who died while on active duty or while in training. Instead, take the Military Survivor Benefits Credit (code AA)." military_retirement_pay_survivors does not separate these survivors, so they get code AJ instead. This could be a documented limitation or a follow-up issue.
  4. PR body understates main's bug. "What was wrong" item 1 stresses the case where both credits are zero. Main also blocked the military credit whenever a positive retirement credit beat the Social Security credit alone. In the 2024 row above, main gives the $325 retirement credit instead of the $1,365 military credit, capped at the liability. The changelog covers this; the body could say it too.

Verified correct

  • Election rule. 59-10-1019(3)(b) bars the retirement credit "if a tax credit under Section 59-10-1042 or 59-10-1043 is claimed on the claimant's return for the same taxable year". 59-10-1042(3)(b) and 59-10-1043(3)(b) each bar their own credit when a 59-10-1019 credit is claimed. Nothing bars claiming 1042 and 1043 together.
    • The 2025 instructions agree. Page 21: "You may not claim this credit if you (or your spouse, if filing jointly) claim either the Social Security Benefits Credit (code AH) or the Military Retirement Credit (code AJ)." Page 23: "You may not claim this credit if you (or your spouse, if filing jointly) claim the Retirement Credit (code 18)."
    • The 2021 instructions (pages 20 and 22) are the same.
    • The election is per return ("on the claimant's return"), as modeled.
  • Comparing uncapped amounts gives the best choice. Total nonrefundable credits are min(sum of credits, liability), so the larger uncapped option never yields less. A positive retirement credit wins ties, where both options are worth the same.
  • Rate.
    • 59-10-1043(2)(a) uses "the percentage listed in Subsection 59-10-104(2)". Enrolled SB 60 (2026) amends 59-10-104(2)(b) to "4.45%", retrospective to taxable years beginning on or after January 1, 2026.
    • gov.states.ut.tax.income.rate runs 4.95% (2021), 4.85%, 4.65%, 4.55%, 4.5% and 4.45% (2026). That matches the deleted parameter for 2021–2025 and the worksheets (2022 "Credit percentage - 4.85%"; 2025 "Multiply line 1 by 0.045").
    • The start date checks out: 2021 S.B. 11 created the credit, retrospective to TY2021, at "the percentage listed in Subsection 59-10-104(2)".
  • Survivor benefits and dependents. 59-10-1043(1)(a)(i) covers "retirement pay, including survivor benefits". military_retirement_income adds military_retirement_pay and military_retirement_pay_survivors. tax_unit_non_dep_add keeps dependents' pay off the filer's return. The Count military SBP annuities in state benefit income lists #10317 military-retirement readers guard passes on the merged tree (tax-side files are exempt).
  • No dependency cycle. ut_military_retirement_credit_max is gated only by state and in_effect. The election reads it, and eligibility reads the election.
  • Mutation pinning (deleting each piece fails a test):
    • the retirement_credit > 0 guard: unit test 4 and the age-45 integration case;
    • the military term in the sum: "military credit alone" (450 vs 1,350);
    • the Social Security term: the combined case (450 vs 300 + 200);
    • in_effect: the 2020 case;
    • the income tax rate: the 2026 case (1,335, not 1,350);
    • the >= tie: existing unit test 3 (500 vs 500).
  • Credit limited to Utah tax. The credit is capped in order after the taxpayer credit. ut_income_tax_before_non_refundable_credits is tax minus the taxpayer credit, matching TC-40 line 23 feeding line 24. This satisfies 59-10-1043(3)(a). The 2024+ list position (after ut_ctc) changes only how credits are attributed, not the total.
  • Housekeeping. The changelog fragment is a top-level .fixed.md. Tests sit in variable-named files. The Python test edit is comment-only, so it adds no CI cost. No partner test files are touched.

Hand checks

  1. ut_military_retirement_credit_max, 2025: 30,000 × 4.5% = 1,350 ✓
  2. Survivor benefits, 2025: 10,000 × 4.5% = 450 ✓
  3. 2020: in_effect is false, so 0 ✓
  4. 2026: 30,000 × 4.45% = 1,335 ✓
  5. Claims test 4: retirement credit 0, so false ✓
  6. 450 vs 0 + 1,350: false ✓
  7. 450 vs 300 + 200 = 500: false ✓
  8. 450 vs 100 + 225 = 325: true ✓
  9. Age 45 (born 1980, after 1952): retirement credit 0, so claims is false, eligible is true, and the credit is 1,350 against a 1,000,000 liability ✓
  10. Age 76 (born 1949), $4,000 of pay: MAGI 4,000 < 25,000, so the retirement credit is 450. Military credit = 4,000 × 4.5% = 180. Claims is true and the military credit is 0 ✓
  11. Existing 2024 cases: 30,000 × 4.55% = 1,365, and 45,000 × 4.55% = 2,047.50 ✓
  12. Probe, the PR body's working case. 2026, married couple aged 45, $30,000 military pay and $60,000 wages: 2,758.73 − 1,335 = 1,423.73 (main 2,758.73). Matches the PR's table ✓
  13. Probe, 2026 couple aged 67/65, $45,000 military pay and $30,000 Social Security:
    • born 1959/1961, so the retirement credit is 0;
    • provisional income 60,000, so taxable Social Security is 0.85 × (60,000 − 44,000) + 6,000 = 19,600;
    • Social Security credit 19,600 × 4.45% = 872.20; military credit max 2,002.50;
    • the 1,100.23 liability absorbs 872.20 + 228.03, so tax is 0, the same as main ✓
  14. Probe, 2025 single age 75, $20,000 military pay and $10,000 private pension: retirement credit 450 − 0.025 × 5,000 = 325, military credit 900. Claims is false, and the 438.23 liability goes to 0 (main 113.23). The law gives the same: the filer elects AJ ✓
  15. Probe, 2025 single age 72 (born 1953), $30,000 military pay: retirement credit 0. The military credit of 1,350 is capped at 438.23, so tax is 0 (main 438.23) ✓
  16. Probe, 2025 single age 70, $20,000 military pay and $24,000 Social Security: provisional income 32,000, so taxable Social Security is min(0.5 × 7,000, 12,000) = 3,500. Social Security credit 3,500 × 4.5% = 157.50. The 61.23 liability goes to 0 ✓
  17. Probe, 2025 single age 70, $30,000 private pension, no military pay: all three credits are 0. Claims is false and tax is 438.23, the same as main. Only the claims flag changes, with no dollar effect ✓

Axiom line assessment

axiom: us-ut/statute/59-10-1043 queued (TheAxiomFoundation/rulespec-us#2074) has a status word and names the issue, so it is acceptable.

  • rulespec-us#2074 is OPEN and labelled pe-parity.
  • It is dispatch-ready. It has:
    • the module path us-ut/statutes/59-10-1043.yaml;
    • the corpus citation with its sha256;
    • verbatim 59-10-1043 plus the 1019(3)(b) and 1042(3)(b) bars;
    • four required outputs;
    • companion tests A–D built from the statute's own arithmetic (TY2026 at 4.45%: 1,335; survivor benefits 445; 1019 claimed gives 0; no pay gives 0).
  • It matches the head's ut_military_retirement_credit_max.
  • The critical fix above changes only PolicyEngine's credit list, not the statutory module, so the issue stays accurate.

CI status

  • At head a3bf81b, 35 checks pass: all baseline shards, all Python shards, Household API Partners, PR validation and codecov. Three Contrib shards were still pending at the last check. Nothing has failed.
  • On the merged tree, I ran the Count military SBP annuities in state benefit income lists #10317 military-retirement readers guard and the Tie source-conflict markers to the register shared with Axiom #10287 source-conflict ratchet locally. Both pass, including the conflict-comment growth and KNOWN_UNMARKED checks.
    • One detector self-test failed locally. It feeds synthetic text only, so this is a local Python-version artifact unrelated to any repo file. CI's guard job passed.
  • The programs registry is unaffected: the PR does not touch programs.yaml, and no entry references the deleted parameter.

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 per the review above: 1 critical (for 2021-2023 the new election drops the retirement credit, but the military retirement credit isn't in the non-refundable list until 2024, so those filers pay more than on main and than the law requires), 1 should (duplicate same-document reference), 4 suggestions. 2024-2026 results are correct.

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