Skip to content

Apply Delaware's 2027-2029 military pension caps and the 60+ choice - #10716

Open
MaxGhenis wants to merge 2 commits into
mainfrom
fix-de-military-pension-cap-2027
Open

MaxGhenis wants to merge 2 commits into
mainfrom
fix-de-military-pension-cap-2027

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

What was wrong

PolicyEngine-US held Delaware's military pension exclusion at $12,500 in every year from 2022, and gave filers 60 and older only the $12,500 pension exclusion. Senate Bill 219 (85 Del. Laws c. 426, signed August 17, 2026) raises the military pension cap for 2027, 2028 and 2029. From 2027 it also lets filers 60 and older take the military pension exclusion when it is larger, and adds a domicile requirement for them.

The law

30 Del. C. § 1106(b)(3), as SB 219 and its Senate Amendment 1 left it, subtracts:

  • b. For 2022 through 2026: for persons under 60, the greater of pensions up to $2,000 or "Amounts received, not to exceed $12,500, as a United States military pension"; for persons 60 or older, pensions and eligible retirement income up to $12,500.
  • c. For 2027: for persons under 60, the greater of pensions up to $2,000 or a military pension up to $15,000; for persons 60 or older, "the greater of: A. Amounts received, not to exceed $12,500, as pensions from employers, the United States, this State, or any subdivision of this State, or as eligible retirement income; or B. Amounts received, not to exceed $15,000, as a United States military pension."
  • d. For 2028: the same, with $20,000.
  • e. "For taxable years beginning on or after January 1, 2029": the same, with $25,000.

Senate Amendment 1 added two rules to the definitions in f:

  • f.3 applies the dollar limits "individually to each spouse who is receiving a United States military pension on a joint return".
  • f.4 makes a person 60 or older "eligible for the subtraction under this paragraph (b)(3) only if" they meet a domicile test: 3 years of Delaware domicile for a person domiciled in Delaware before January 1, 2027, or 5 years for a person domiciled there from that date.

Change

  • cap/military.yaml: $15,000 from 2027, $20,000 from 2028 and $25,000 from 2029.
  • New military_retirement_exclusion_available_at_age_threshold.yaml (false through 2026, true from 2027): when true, de_pension_exclusion gives filers at or above the age threshold the greater of their capped pension exclusion and their capped military pension, as it already did for younger filers.
  • New domicile_requirement_applies.yaml (true from 2027) and a new person input, de_pension_exclusion_domicile_requirement_met, which defaults to true. When a filer at or above the age threshold fails the requirement, de_pension_exclusion gives them nothing. PolicyEngine has no data on how long people have lived in Delaware, so population results assume everyone meets it.
  • f.3 needs no change: de_pension_exclusion already works person by person.

The age-threshold switch matters even where the caps make the greater-of look automatic. A 60+ filer's pension basket can net capital losses, so it can fall below the military pension: with $10,000 of military pay and a $3,000 capital loss, the basket is $7,000. Before 2027 the statute gives that filer $7,000. From 2027 it gives $10,000.

Readings to check

  1. The domicile test's first tax year. SB 219 took effect when the Governor signed it, and neither the act nor f.4 names a first tax year for the test; the act's other changes start in 2027 by their own terms. This PR applies f.4 from 2027, as the fiscal note assumes (assumption 1: "effective for tax years after January 1, 2027"). The other reading applies it to 2026 returns too. Because the input defaults to true, the choice changes results only for households that set it to false.
  2. The domicile test's reach. SA 1's synopsis speaks of eligibility "for this exemption", in a bill about military pensions, but the text bars "the subtraction under this paragraph (b)(3)". This PR follows the text: a 60+ filer who fails the test loses the whole subtraction, including the $12,500 exclusion for other pensions and eligible retirement income.
  3. Survivor annuities, unchanged. Since 2022, de_pension_exclusion has kept military survivor benefits (military_retirement_pay_survivors) out of the military pension branch. They share the $2,000 or $12,500 pension exclusion instead, and an existing 2024 test encodes that. f.2 defines a United States military pension as "a pension received as a result of an individual's service" in the uniformed services, which could reach a survivor's annuity. This PR keeps main's reading and extends its test to 2029.

Invariants

For every input, with cap the year's military pension cap:

  • Through 2026, results don't change.
  • Under 60, the exclusion is max(min(2,000, pensions), min(cap, military pension)) in every year from 2022, whatever the domicile input says.
  • From 2027, at 60 or older, it is max(min(12,500, basket), min(cap, military pension)) for a filer who meets the domicile requirement and zero for one who doesn't. A filer who meets it never gets less than the 2026 rule gives.
  • The exclusion is never negative and never exceeds the military pension plus the pension basket floored at zero.

Tests

de_pension_exclusion.yaml adds 20 cases:

  • the under-60 cap in 2026 ($12,500), 2027 ($15,000), 2028 ($20,000), 2029 and 2031 ($25,000), and pay under the cap;
  • the 60+ choice in 2026 ($12,500) and from 2027, including a basket that beats the military cap, a filer with no military pension, and the capital loss case in 2026 ($7,000) and 2027 ($10,000);
  • survivor benefits under 60, and a joint return with one spouse on each side of 60;
  • the domicile requirement: a 60+ filer who fails it gets nothing in 2027, and nothing in 2029 with IRA distributions alone; it doesn't bind in 2026 or under 60; and on a joint return it applies to each spouse.

tests/core/test_de_military_pension_exclusion_properties.py uses Hypothesis to draw batches of single filers, varying age, military pay, survivor benefits, other pensions, IRA distributions, interest, capital gains or losses, and the domicile input. It checks the invariants above against a reference that restates the statute, for every year from 2022 to 2031. Setting the age-threshold switch to false for 2027 makes it fail; setting the domicile switch to false for 2027 fails three of the new YAML cases.

All 516 cases under tests/policy/baseline/gov/states/de pass.

Before and after

Delaware income tax for a single filer with $30,000 of military retired pay and no other income:

Year and age Exclusion before Exclusion after Tax before Tax after
2027, 45 $12,500 $15,000 $355 $235
2029, 45 $12,500 $25,000 $355 $0
2029, 67 $12,500 $25,000 $125 $0

For 2027 at 45: $30,000 − $15,000 leaves $15,000 of Delaware AGI and $11,750 of taxable income after the $3,250 standard deduction. The brackets give $66 + $195 + $84 = $345, less the $110 personal credit.

axiom: TheAxiomFoundation/rulespec-us#2078

🤖 Generated with Claude Code

30 Del. C. 1106(b)(3)c-e raise the military pension exclusion cap to
$15,000 for 2027, $20,000 for 2028 and $25,000 from 2029, and from 2027
give filers 60 or older the greater of the $12,500 pension exclusion and
their military pension up to that cap. PolicyEngine-US held the cap at
$12,500 and gave 60+ filers only the pension exclusion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Senate Amendment 1 to SB 219 (85 Del. Laws c. 426) added 30 Del. C.
1106(b)(3)f.4: a person 60 or older takes the (b)(3) subtraction only
after 3 years of Delaware domicile, or 5 years for a person domiciled in
Delaware from 2027. A new person input,
de_pension_exclusion_domicile_requirement_met, defaults to true, and a
switch applies the rule from tax year 2027, as the act's fiscal note
assumes.

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

Copy link
Copy Markdown
Contributor Author

David, I'd like your call on the three Delaware readings in the PR body, above all the first. SB 219 took effect when the Governor signed it and names no first tax year for the domicile test, so the PR applies the test from 2027, as the fiscal note assumes. It also follows the text in denying a filer who fails the test the whole (b)(3) subtraction, and, as main does, it counts military survivor benefits toward the general pension exclusion. 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 (830800d).
⚠️ Report is 93 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main    #10716   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            1         2    +1     
  Lines           37        42    +5     
  Branches         0         3    +3     
=========================================
+ Hits            37        42    +5     
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: Apply Delaware's 2027-2029 military pension caps and the 60+ choice

Summary

I read the enacted bill first: SB 219 as amended by Senate Amendment 1 (85 Del. Laws c. 426, signed and effective 8/17/26), its fiscal note, and 30 Del. C. § 1106(b)(3) as the online Code now prints it. The core change is right. The military caps are $15,000 for 2027, $20,000 for 2028 and $25,000 from 2029. Filers 60 and older get "the greater of" the $12,500 pension exclusion and the capped military pension from 2027. Both work person by person and need no circular calculation. The f.4 domicile test removes the whole (b)(3) subtraction for a 60+ filer who fails it, from 2027. 2026 and earlier results are unchanged from main, which matches the Veterans Day post's 2026 figures ($12,500 under 60 and $12,500 at 60+). I checked 25 expectations by hand and with a small probe at the head, including all five tax figures in the PR body.

Three things are open:

  • The axiom line has no status word.
  • The PR keeps main's reading that military survivor annuities aren't a "United States military pension". That follows the Division of Revenue's instructions over the statute's definition, and the PR doesn't record the choice. My call on that reading is below.
  • One duplicate reference entry.

My calls on the three readings the author asked about:

  1. Domicile test from 2027: agreed.
  2. Whole subtraction lost when the domicile test fails: agreed; the text controls.
  3. Survivor annuities: follow the statute and count them in the military branch, or record the choice as a source-conflict candidate (Should address 2).

Critical (must fix)

None.

Should address

1. Axiom line has no status word.
The PR body says axiom: TheAxiomFoundation/rulespec-us#2078. That isn't one of the valid forms in CLAUDE.md#axiom-parity: <id> encoded-correct, <rulespec PR> encoded, <issue> queued, or n/a: <reason>.

What issue #2078 does well:

  • It is OPEN, labelled pe-parity, and names this head (830800d043).
  • It gives the module gap (us-de/statutes/30/1106), quotes c.2, f.3 and f.4, names the output (de_pension_exclusion), and lists hand-computable cases.

What's missing:

  • No blocker. CLAUDE.md allows queued "only when the signed encoder is blocked; record the blocker in the issue". The issue records an encoding gap but no blocker.
  • Survivors not covered. It doesn't say how survivor annuities are treated, which finding 2 may change.

Fix: change the line to axiom: TheAxiomFoundation/rulespec-us#2078 queued and record the blocker in the issue, or use <rulespec PR> encoded once the module exists. Update #2078 once the survivor reading is settled.

2. Survivor annuities: the PR keeps the instructions' narrower reading over the statute's definition, and doesn't record it (de_pension_exclusion.py:58; de_pension_exclusion.yaml:284, new 2029 case; existing 2024 case at line 136).

The statute. 30 Del. C. § 1106(b)(3)f.2, unchanged by SB 219, defines the term this way:

"United States military pension" means a pension received as a result of an individual's service in the Army, Navy, Air Force, Marine Corps, Space Force, Coast Guard, ... or the National Guard.

The article is indefinite: "an individual's service", not "the taxpayer's service". A Survivor Benefit Plan annuity is a pension received as a result of the deceased member's service. Delaware's own instructions treat survivor pensions as pensions: Line 6 covers "pensions from employers (including pensions of a deceased taxpayer)" (2025 PIT-RES instructions, page 6).

The Division's reading. The same page narrows the military branch to a filer who "retired from the United States military and received pension income". But its checkbox asks only whether "your pension is from the United States military".

The legislature's reading. The SB 219 fiscal note puts survivors in the base for this act (assumption 4): "there were 8,921 retired military and 1,337 survivors receiving a military pension in Delaware" (fiscal note, page 1).

Project rule. Under the 2026-10-20 ruling (CLAUDE.md "Conflicting sources"; CONTRIBUTING.md#source-conflicts), the statute governs over the instructions. The canons say "definitions control" and "the operative words govern over form shortcuts".

What the head computes (probe), and the statute's result:

Case Year Head Under f.2
Survivor, 65, $30,000 SBP annuity 2029 $12,500 $25,000 = max(12,500, min(25,000, 30,000))
Survivor, 50, $30,000 2027 $2,000 $15,000
65, $8,000 own retired pay + $8,000 survivor annuity 2029 $12,500 $16,000

The first and third rows come from this PR's new 60+ branch. The under-60 gap dates from #10182 (October 2026), when the survivor input was added. The PR extends it to 2027-2029 with a new test.

Fix, in my view: follow the statute. In the military branch, read military_retirement_income (retired pay plus survivor annuities) instead of military_retirement_pay. Then:

  • update the 2024 survivor case ($2,000 → $3,000) and the new 2029 survivor case ($2,000 → $25,000);
  • change the Hypothesis reference's military term (test_de_military_pension_exclusion_properties.py:56) to match;
  • add one case with both inputs.

The #10317 readers guard doesn't constrain tax-side files, so either input is allowed.

The same definition has governed (b) since at least the 2024 amendment. So the fix changes survivors' 2024-2026 results too, which is a correction, not a regression. If the team instead keeps main's reading, record it as a candidate in source_conflicts/candidates.yaml: f.2 vs the PIT-RES Line 6 instructions, with both readings, years 2022 on, and pe_us.follows = instructions. Then fix the PR body's "Since 2022" wording, since the survivor input dates from #10182.

3. Duplicate reference entry in cap/military.yaml (lines 23-26).
The new entry "30 Del. C. § 1106(b)(3)c-e, military pension caps for 2027, 2028 and 2029 on" has the same href as the existing entry "The Delaware Code - Title 30 - Chapter 11 - Subchapter II - § 1106 - (b) - (3)". CLAUDE.md allows one reference entry per source document. The title also ends with a dangling "on".

The online Code now prints c-e (checked today), so the existing href corroborates the new values. Fix: delete the new entry, and retitle the existing one, for example "30 Del. C. § 1106(b)(3)b-e".

Suggestions

  1. Reading 1 (first tax year of the domicile test): I agree with 2027. Revisit it when the TY2026 PIT-RES instructions come out.
    • SB 219 has no effective-date section. It took effect on signature (8/17/26; bill page: "Effective Date: 8/17/26").
    • f.4 names no first tax year. Its two tiers key on domicile "before" and "on or after January 1, 2027", which fits a rule that starts in 2027.
    • Fiscal note assumption 1: "Provisions of the Act are effective for tax years after January 1, 2027."
    • The statute is silent rather than in conflict with an agency source, so nothing needs registering now.
    • If the Division applies f.4 to 2026 returns, move domicile_requirement_applies to 2026. With the input defaulting to true, only households flagged false would change.
  2. Python test cost line. CLAUDE.md "Test design and CI cost" asks the PR body for the CI group and elapsed time of a new simulation-building Python test. The head's CI run fills it in, and the cost is small: test_de_military_pension_exclusion_properties.py ran in the core-2 group (Rest Python shard 3) in 1.59 s (job 129305119969, "slowest durations"). Hypothesis invariant files in tests/core are established practice. Consider adding that line to the body.

Verified correct

  • Enacted text.
    • The engrossed SB 219 "as amended by Senate Amendment No. 1" matches the PR body's quotes.
    • b. now covers "taxable years beginning on January 1, 2022, and ending before January 1, 2027".
    • c.: 2027, $15,000. d.: 2028, $20,000. e.: "on or after January 1, 2029", $25,000. Each gives 60+ filers "the greater of" A ($12,500 pensions or eligible retirement income) and B (the military pension cap).
    • f.3 applies the limits "individually to each spouse ... on a joint return", not to exceed "twice the relevant limit". f.4 is the 3-year/5-year domicile test, with domicile meaning "resident individual" under § 1103.
    • Laws of Delaware 85:426; signed 8/17/26. delcode.delaware.gov already prints c-f.4.
  • Parameters.
    • cap/military.yaml steps to 15,000 / 20,000 / 25,000 on 2027/2028/2029-01-01, and the last value carries forward (2031 test).
    • military_retirement_exclusion_available_at_age_threshold is false for 2022 and true for 2027.
    • domicile_requirement_applies is false for 2007 and true for 2027.
    • Bool descriptions follow the house template.
  • Formula.
    • Under 60: max(min(2,000, pensions incl. military and survivors), min(cap, military pay)), the statute's "greater of".
    • 60+ from 2027: max(min(12,500, basket), min(cap, military pay)), computed from two independently capped amounts. There is no election and no circularity; the statute's "greater of" is automatic.
    • Per person; the head_or_spouse gating is kept.
    • The domicile check is vectorized, applies only at 60+ and only when the switch is on.
  • 2026 and earlier results are unchanged. With the new switch false and the domicile switch false, the code path is identical to main's: older_amount = capped_eligible_pension_income, and the domicile block is skipped.
  • Reading 2. f.4 bars "the subtraction under this paragraph (b)(3)". The PR follows that text over the synopsis's "this exemption" and pins it (2029 IRA-only case → $0). A synopsis isn't a form, instructions or agency practice, so no register entry is needed.
  • Mutation pinning. Every new value and branch fails a test if deleted:
    • each cap value has its own case;
    • the 60+ switch is pinned true by the 2027 $15,000 case and false by the 2026 capital-loss case ($7,000; true would give $10,000);
    • the domicile switch is pinned true (2027 → $0) and false (2026 → $12,500);
    • the 60+-only gating is pinned (under 60 → $15,000);
    • the per-spouse application is pinned (joint [0, 22,000]);
    • the default of true is pinned by every 60+ 2027+ case that doesn't set it.
  • No reinvented variables. The PR reuses military_retirement_pay, military_retirement_income and military_retirement_pay_survivors. Main has no residency-duration input, so the new bool input is needed. It's a bool, not a parameter key, so the NH Add New Hampshire state supplementation for 2026 #10156 pattern doesn't apply.
  • Changelog. changelog.d/fix-de-military-pension-cap-2027.fixed.md is top-level with a sensible type. The tests go in the variable-named YAML.
  • Hand checks (head; YAML expectations plus probe):
    1. 2026, age 45, $30,000 military pay: max(min(2,000, 30,000), min(12,500, 30,000)) = 12,500 ✓
    2. 2027, 45: min(15,000, 30,000) = 15,000 ✓. 2028: 20,000 ✓. 2029 and 2031: 25,000 ✓
    3. 2029, 45, $18,000: min(25,000, 18,000) = 18,000 ✓
    4. 2026, 65, $30,000: switch off, min(12,500, basket 30,000) = 12,500 ✓. 2027: max(12,500, 15,000) = 15,000 ✓. 2029: max(12,500, 25,000) = 25,000 ✓
    5. 2029, 65, $10,000 military + $20,000 IRA: A = min(12,500, 30,000) = 12,500; B = 10,000 → 12,500 ✓. 65 with a $30,000 pension and no military pay → 12,500 ✓
    6. 75, $10,000 military, −$3,000 LTCG: basket 7,000. 2026 → 7,000; 2027 → max(7,000, 10,000) = 10,000 ✓
    7. Joint 2029: head 65 with $30,000 → max(12,500, 25,000) = 25,000; spouse 58 with $22,000 → max(2,000, 22,000) = 22,000 → [25,000, 22,000] ✓. Probe: both spouses 60+ with $30,000 each → [25,000, 25,000], twice the limit, as f.3 allows; a 70-year-old dependent → 0 ✓
    8. Domicile: 2027, 65, fails → 0; 2029, 70, IRA $20,000, fails → 0; 2026 → 12,500; under 60 in 2027 → 15,000; joint 2029 [fails → 0, 62 with $22,000 → max(12,500, 22,000) = 22,000] ✓. Probe: 60+ with only a net loss who fails → 0 ✓
    9. PR body tax table (probe de_income_tax):
      • 2027, 45: DE AGI 15,000 − 3,250 = 11,750; tax 66 + 195 + 1,750 × 4.8% = 84, so 345; less the 110 credit = 235 ✓
      • 2026, 45: 14,250; 66 + 195 + 204 = 465 − 110 = 355 ✓
      • 2029, 45: AGI 5,000, taxable 1,750 → 0 ✓
      • 2029, 67, before: 17,500 − 3,250 − 2,500 = 11,750 → 345 − 220 = 125 (the head gives 125 for the same inputs in 2026) ✓. After: 0 ✓
  • Merged-tree guards. On the tree merged with main, test_military_retirement_readers.py and test_source_conflicts.py: 106 passed, 1 failed. The failure is test_unscannable_files_fail_closed, a detector self-test on synthetic strings: a vertical-tab Python snippet didn't raise under the local Python 3.11. It reads no repository file and is unrelated to this PR. CI's guard job passed at this head. Every scanning test passed, including test_conflict_sounding_comments_do_not_grow. DE is tax-side for the Count military SBP annuities in state benefit income lists #10317 guard. The PR's new comments ("names no first tax year", "fiscal note applies the act from tax year 2027") match none of the Tie source-conflict markers to the register shared with Axiom #10287 conflict patterns.
  • Siblings. No overlap with Let Utah military retirees claim the military retirement credit when no retirement credit applies #10702, Model Montana's working military retirement and survivor benefit exemption #10713, Give Michigan's qualified disabled veteran exemption to every veteran with a service-connected disability #10714, Apply Idaho's 2025 eligibility tests and cap to military retired pay #10715 or Set Idaho's 2026 retirement benefits deduction cap from SSA's maximum benefit #10717. Model Montana's working military retirement and survivor benefit exemption #10713 edits only a Montana sentence in military_retirement_pay.py's documentation. No file under tests/policy/baseline/partners/ changes.

Axiom line assessment

axiom: TheAxiomFoundation/rulespec-us#2078 has no status word, so it isn't a valid form (Should address 1). The issue is OPEN, labelled pe-parity, matches head 830800d043, and is close to dispatch-ready: module gap, citation, partial verbatim law, output and cases. It records no encoder blocker, which queued requires. It also needs updating if the survivor treatment changes.

CI status

At head 830800d043, gh pr checks 10716 (re-checked at the end of this review) shows 38 pass and 3 skipping. The skipped ones are the umbrella Baseline, Contrib and Python jobs of an older run, not failures. All checks are green, including PR validation (lock, guard tests, lint, changelog), every baseline and contrib shard, all 9 Rest Python shards, Microsimulation, Household API Partners, and codecov patch and project.

Branch status

The branch is 2 commits ahead of main and 40 behind (merge base ba14b81f7b). git merge-tree --write-tree PolicyEngine/main 830800d043 merges cleanly. Since the merge base, main hasn't touched the Delaware pension exclusion files; it changed one line of test_source_conflicts.py, and the guard result above is on the merged tree.

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: 0 critical, 3 should (axiom line needs a status word and #2078 a recorded blocker; survivor annuities fall under f.2's 'received as a result of an individual's service' but the PR keeps the instructions' retiree-only reading unrecorded; duplicate reference entry), 2 suggestions. The caps, the 60+ choice and 2026 results check out.

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