Skip to content

Exclude SE health insurance deductions from Schedule A medical expenses - #10055

Draft
MaxGhenis wants to merge 29 commits into
mainfrom
hub-schedA-se-premiums-1010
Draft

MaxGhenis wants to merge 29 commits into
mainfrom
hub-schedA-se-premiums-1010

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Premiums deducted through the self-employed health insurance above-the-line deduction currently remain in the federal Schedule A medical base. For premiums of $6,000, an SE deduction of $1,000, other expenses of $500 and AGI of $30,000, this changes the medical deduction from $4,250 to $3,250.

The code trace confirms the overlap: medical_expense_health_insurance_premiums supplies gross direct/decomposed premiums, itemized_medical_expenses previously added that gross amount, and medical_expense_deduction only applied the AGI floor. Meanwhile self_employed_health_insurance_ald sums the head/spouse's earnings-limited person deductions independently.

The 2025 Schedule A instructions say: “Reduce the insurance premiums by any self-employed health insurance deduction you claimed on Schedule 1”. IRC 162(l)(3) bars counting those amounts under IRC 213.

Changes and invariants

  • Net the filer SE deduction only against filer-paid premiums before Schedule A's floor. Premium inputs use payer attribution: each person reports premiums that person paid, including the full cost of family coverage regardless of whom it covers. For consistent payer-attributed inputs, Schedule A premium use plus the SE deduction equals otherwise eligible premiums paid.
  • Bound the federal exclusion to the combined filer-paid premium pool and each New Jersey dependent exclusion to that person's premiums, preserving other medical expenses and dependent premiums on Schedule A. Tax units remain independent.
  • Keep the raw premium helper and disjoint pre_tax_health_insurance_premiums input unchanged.
  • Apply the exclusion to Montana's separately computed person deduction. Round 2 corrects New Jersey's previously pooled dependent exclusion: cap each dependent's exclusion at that person's own medical premiums before summing and applying the 2% floor, then add the separate SE deduction. The earlier claim of corrected NJ attribution was too broad.
  • Preserve North Dakota's renter refund and New Mexico's senior medical credit/exemption results from main for intact supplied federal medical subtotals and computed components. Their paid-cost base is itemized_medical_expenses plus the exclusion the federal subtotal formula actually applied. The read-only, period- and branch-aware has_input_for_period helper makes that applied amount zero when the subtotal was supplied, and the excluded premiums when it was computed. Supplied subtotals therefore remain unchanged, while computed net costs recover the gross paid cost. This PR requires policyengine-core >=3.32.27 so explicit deletion and recalculation of a supplied subtotal also retains reliable provenance. Supplied and derived tax-unit SE deduction inputs remain supported; New Jersey retains its separate per-person dependent cap.

State audit: AL, AR, AZ, CA, CO, DC, DE, GA, HI, IA, ID, KS, KY, LA, MA, MD, ME, MN, MO, MS, MT, NC, ND, NE, NJ, NM, NY, OK, OR, SC, UT, VA, VT and WI. Raw-premium consumers OH, PA, MI and WA were also checked. KY removes the medical component entirely. CA's unmodeled worker-classification exception and IA's historical separate premium subtraction are existing issues; this patch does not introduce a blanket addback. Round 2 also removes Missouri premiums already excluded from federal taxable income, including the SE health insurance ALD, from the qualifying pool before applying the medical-overlap ratio. 2025 MO-1040 instructions, page 15 require that exclusion. For $6,000 premiums, $1,000 ALD and $500 other costs at $30,000 AGI, the qualifying pool is $5,000. Existing references show no other general exception to the federal SE exclusion.

Separate Missouri rounding correction: Form 5695, line 12 requires the medical-overlap ratio to be rounded to a whole percent; the 2021 instructions, page 36 contain the same requirement. This changes three existing no-SE expectations: $375 → $350, $525 → $480, and $262.50 → $240. Half-percent ties round up, inferred from the explicit 90.5% → 91% percentage example in the 2025 instructions, page 40; Form 5695 line 12 does not separately state a tie rule. In the SE example above, $3,250 / $5,500 = 59.0909% rounds to 59%, so the subtraction is $5,000 × 41% = $2,050.

Independent state sources: MT 2023 instructions, page 36, NJ PL1999 c222 and Worksheet F, NM senior exemption and credit, ND paid-cost guidance. The ND and NM changes preserve main's existing paid-cost bases and caller-supplied subtotal behavior.

Methodology

  • North Dakota renter refund: Preserve main's paid-cost base as itemized_medical_expenses plus the actually applied exclusion. If has_input_for_period finds a supplied subtotal for the calculation period and branch, the applied amount is zero, regardless of any accompanying components or SE deduction. Otherwise, add back the excluded premiums to the computed federal net subtotal. A $6,500 supplied subtotal with $30,000 income gives $23,500 both by itself and alongside $6,000 premiums, $500 other costs and a $1,000 SE deduction. Computing those components without a supplied subtotal also gives $23,500. Even a supplied $500 subtotal alongside a $1,000 exclusion retains a $500 state base. The paid-cost guidance remains the existing state basis.
  • New Mexico: Both the senior medical credit and exemption use the same applied-exclusion rule as North Dakota. Compatibility with main holds for intact supplied inputs and computed components: a supplied subtotal is used exactly, including when components and an SE deduction are also supplied; a computed subtotal receives the excluded-premium add-back to recover gross unreimbursed costs. The required policyengine-core >=3.32.27 also preserves correct results after a supplied subtotal is explicitly deleted and recalculated. A qualifying senior supplying $27,999 alongside $6,000 premiums and a $1,000 SE deduction retains a $0 credit and $0 exemption. A supplied $28,000 subtotal retains a $2,800 credit and $3,000 exemption, and computed component cases retain their gross paid-cost results under the independent instructions, pages 5–6.
  • Payer attribution: Assign premiums to whoever paid them, including family coverage; the alternative assigns them to covered beneficiaries. For a parent-paid $800 plan with $800 parent ALD, main retains an $800 Schedule A premium base; this head excludes the deducted premiums and leaves a $0 premium base. This follows Fix Idaho health insurance premium subtraction #10023 and Document disjoint health premium inputs and correct consumer treatment #10046; shared premium-input documentation remains owned by Document disjoint health premium inputs and correct consumer treatment #10046.

Round 2

Fix both P2 findings from review-r1: New Jersey no longer lets one dependent's deduction consume another's premiums, and Missouri removes already-deducted premiums from its eligible pool and applies required full-percent rounding. Add the A/B regression ($2,000 NJ deduction), a generated per-person exclusion property, Missouri pool/rounding controls, and both explicit family-attribution cases. Document payer attribution on every variable this PR changes. The federal correction remains unchanged.

The reviewer's beneficiary-attributed parent $300 / dependent $500 with parent ALD $800 leaves $500 because the exclusion is capped at the parent's $300. Excluded $300 plus eligible $500 accounts for the $800 premium inputs; adding the full supplied ALD gives $1,300, so that inconsistent pattern cannot establish ALD-plus-Schedule-A conservation. Callers must put all parent-paid family premiums on the parent. The two explicit family cases document this distinction. NJ dependent aggregate ALD overrides likewise need corresponding person ALDs to establish each person's exclusion.

Round 3 (historical; supplied-subtotal handling superseded by Round 4)

Round 3 removed round 2's choice to ignore caller-supplied federal medical subtotals in North Dakota and New Mexico, but added the excluded premiums unconditionally. That recovered main's gross costs for computed federal subtotals and preserved supplied subtotals only when the exclusion was zero. Review-r3 identified the remaining regression when a supplied subtotal accompanied premium components and an SE deduction; Round 4 corrects it.

The tax-unit excluded-premium variable exposes the existing federal minimum of combined nondependent premiums and the nonnegative supplied or derived tax-unit SE health insurance deduction; the federal calculation reuses it. The separate per-person self_employed_health_insurance_ald_excluded_premiums variable remains the minimum of that person's paid medical premiums and nonnegative person SE deduction. New Jersey reuses it when summing dependent exclusions, retaining the per-person cap. These caps and payer attribution remain unchanged.

Round 4

Round 4 resolves both P2 findings in review-r3 by distinguishing supplied and computed federal medical subtotals through has_input_for_period. The shared applied exclusion is zero for a supplied itemized_medical_expenses value for the calculation period and branch, and the bounded excluded premiums otherwise. North Dakota's renter-refund income and both New Mexico senior medical provisions add only this applied amount. This restores main's results for the three counterexamples: $23,500 ND income for the supplied $6,500 subtotal with components and SE deduction; $0 NM credit and $0 exemption for the supplied $27,999 subtotal with premiums and SE deduction; and a $500 state base for the supplied $500 subtotal with a $1,000 excluded-premium amount.

The SE property now generates independent supplied subtotals and components without rewriting the supplied subtotal. It asserts that the state base equals the supplied subtotal whenever it is supplied, and the gross paid cost otherwise, retaining premium conservation assertions for the computed path. YAML regressions cover all three counterexamples. #10023 has merged into main. Its helper supplies the read-only, period- and branch-aware input provenance used here; this branch merges current main so those inherited changes are absent from this PR's diff.

Round 5 (historical; dependency requirement and version gates superseded by Round 6)

Round 5 adds regressions with #10023's existing version gate for North Dakota's renter-refund income and both New Mexico senior medical provisions. Correct provenance after an explicit set_input followed by delete_arrays and recalculation requires policyengine-core 3.32.27, the first fixed version identified by #10023 and Core #561. At Round 5, the dependency floor was policyengine-core>=3.32.8 and the lock was at 3.32.8; Round 6 raises both to the fixed release. On older cores, deleting a supplied array leaves its _user_input_keys marker behind; if that array is recomputed before the helper runs, the read-only helper cannot distinguish the recomputed value from an intact supplied value. The shared helper and intact-input handling remain unchanged.

The two state regressions use public simulation APIs and assert the correct results after supplying and deleting itemized_medical_expenses for 2025. For North Dakota, $30,000 income, $6,000 premiums, $500 other expenses and $1,000 ALD, with a supplied then deleted $6,500 subtotal, must produce $23,500 income; affected older cores produce $24,500. For an eligible New Mexico senior with $28,000 gross medical expenses and a supplied then deleted subtotal, both provisions must retain the $2,800 credit and $3,000 exemption; affected older cores produce $0 credit and $0 exemption. At Round 5, both regressions used the same strict conditional xfail as #10023: Version(version("policyengine-core")) < Version("3.32.27"). They ran as ordinary passing regressions on fixed cores; Round 6 removes their version gate and xfail markers because older cores are no longer supported. Normal full-file validation on core 3.32.8 gives 2 passed and 2 strict xfailed: the intact-input controls pass. A separate diagnostic of the two deleted-input cases with --runxfail -k deleted gives 2 expected failures and 2 deselected, reproducing the review's $24,500 ND income and $0/$0 NM benefits. Those intentional diagnostic failures are excluded from the normal validation totals. On core 3.32.27, all 4 cases pass, including the deleted-input results of $23,500 ND income and $2,800/$3,000 NM benefits. Each of the three fresh foreground processes ran only policyengine_us/tests/core/test_state_medical_expense_input_deletion.py, one at a time. Raw failure traces and resource measurements remain untracked in hub-evidence/. make format (one new file formatted, 6,823 unchanged), Ruff lint and git diff --check passed. No partner files or CI configuration changed; no folder/full suite or microsimulation ran.

Python coverage is necessary because YAML cannot delete supplied arrays and verify subsequent provenance. Each case uses a fresh single-person, single-year simulation with the existing read-only reference system; the four cases join the existing Rest/core group. This file was absent before Round 5; its added standalone local macOS/Python 3.13.9 cost, including interpreter startup and model initialization, is:

Core Passed Strict xfailed Command wall time Peak child RSS
3.32.8 2 2 2623.32 s 751.23 MiB
3.32.27 4 0 1871.50 s 731.53 MiB

The baseline Linux Rest job, artifact 11620880923, records the core group before these additions at 11:16.36 wall time and 6,757,164 KiB peak RSS. Updated Linux group timing/memory impact remains pending; the local standalone file runs do not establish that impact.

Round 6

Round 6 resolves review-r5's supported-core deletion failure by requiring policyengine-core>=3.32.27 and regenerating uv.lock for that requirement. Core's tagged changelog, release tags and tagged deletion implementation confirm that 3.32.27 (tag commit b853f4989e973a25160698e28b8b3d57a580c654, released 2026-10-09) is the first release containing Core #561: deleting an input clears its _user_input_keys marker, so the read-only has_input_for_period helper distinguishes subsequent computed values from intact supplied values. The lock update is confined to this dependency requirement and the corresponding core package entry.

The North Dakota and New Mexico state deletion regressions now run as ordinary tests with no core-version gate or xfail. They retain the intact-input controls and assert $23,500 ND income and $2,800 NM credit/$3,000 NM exemption after deletion and recalculation. The Round-5 old-core results above are historical diagnostics, not a limitation of the supported dependency range.

This branch first merged gh/main at 94fca350bff7ae26813f31d6be0e97c25298d64e with plain merge commit 9b38af49de79aea638cd2a206d2426358b15b1a3, then refreshed to main 7bd779527ff4dde560bd7c5cafa341467515385b with plain merge commit ba2f3b80ae0bf3dc9ad4d7312eebb41f4ba7f48a. #10023 is already on main, so its Idaho implementation and helper are no longer introduced by this PR. The $134,000 → $129,560 Idaho couple comparison below is explicitly relative to pre-#10023 main.

Round-6 validation: make format after the final merge (Ruff 0.15.5; 6,841 files unchanged; lint passed), uv lock --check with CI's uv 0.12.13, and working-tree/PR whitespace checks passed. A parsed lock comparison confirms that only the core package record (3.32.8 → 3.32.27, with release URLs/hashes) and the US core requirement changed; all other package records are identical. The final PR diff against main is 31 files, 1,699 additions and 66 deletions. #10023's implementation, helper, Python tests and released changelog fragment are absent from that diff; only the existing Idaho YAML case's federal medical deduction expectation/comments change. The Montana conflict resolution preserves all main cases and supplies mt_agi_joint: 40_000 in this PR's joint premium case, matching main's updated AGI interface. Partner tests are untouched.

Round-6 single-file validation: 35 tests passed, using the supplied Python 3.13.9 interpreter and a workspace-only core 3.32.27 overlay. Each file ran alone in a fresh foreground process: the state deletion file through pytest, then the Montana and Idaho files through the standard core CLI. No run reached the 25-minute cutoff.

File Result Pytest time Total wall time Peak child RSS
test_state_medical_expense_input_deletion.py 4 passed, no xfails 17.98 s 490.92 s unavailable
mt_medical_expense_deduction_joint.yaml 8 passed, 1 plugin warning; exit 0 118.26 s 1114.25 s 2.02 GiB
id_health_insurance_premiums_subtraction.yaml 23 passed, 1 plugin warning; exit 0 326.45 s 1220.87 s 3.05 GiB

The state file's BSD time wrapper returned exit 1 after the 4 passed summary because the sandbox denied sysctl kern.clockrate; that resource-measurement failure leaves peak RSS unavailable. Both YAML warnings report that the already-imported anyio plugin cannot be assertion-rewritten. Later runs used Python resource measurements. Raw logs and measurements remain untracked in hub-evidence/; these are local standalone measurements, not Linux CI group results.

The three runs tested dfa0863bd694f1c70853c5b68df92838151d11bd. The subsequent clean main refresh changes only SSI resource deeming, a West Virginia deduction, and the US package version. Static comparison confirms that all three tested files and their ND/NM/MT/ID medical code and parameter paths are unchanged; the core-only lock comparison and formatting/PR whitespace checks passed again after that merge. The cached merge diff reports two trailing blank lines already on main in unrelated SSI YAML files; those files match main and are absent from this PR's diff.

The final push-triggered PR workflow skipped its jobs because this PR remains draft. Linux CI validation and hub population impact remain pending. No CI configuration, partner tests, local microsimulation, or full/folder suite changed or ran.

#10023's inherited test_deleted_medical_input_recalculated_federal_first in test_id_health_insurance_premiums.py still has a core <3.32.27 xfail condition and its version/Version imports. That condition is redundant under the new floor, but the file matches main and is outside this PR's diff, so it is unchanged.

Validation

Round-4 validation: 83 distinct tests passed and one xfailed. Locally, 76 YAML cases passed across eight files, each run separately and serially in the foreground: ND 9, NM credit 10, NM exemption 9, Idaho deductions 7, Idaho tax before credits 4, Idaho itemized deductions 7, Idaho integration 7, and Idaho health-insurance-premium subtraction 23. The standard single-file YAML runner used the fully initialized, unmodified model with Python 3.13.9 and a workspace-only overlay of core 3.32.8 and filelock 3.20.3. The local Idaho Python file also recorded two passes and one xfail for the documented old-core deleted-input limitation; those passes are counted once in the distinct total.

The locked Linux CI run ran both changed Python files serially: premium conservation 5 passed, 1 warning in 7.49s; Idaho 2 passed, 1 xfailed in 3.73s. CI used Python 3.14.7, core 3.32.8 and filelock 3.20.3; uv sync followed the project's Python pin despite the preceding Setup Python 3.13 step. Local premium-conservation attempts were interrupted during collection/imports without reaching assertions; the fresh retry was stopped after CI passed.

Round-4 CI tested 380afa50, which included a temporary branch-only docs.yaml change for the exact-file run. The original workflow was restored byte for byte. The historical Round-4 final HEAD e6855037bc12a809fe22b61a616b6990fc22d1f6 had the same complete tree as implementation commit 7c96 and the same source as the CI-tested commit; it was pushed as a fast-forward. The PR remains draft.

make format passed after the dependency merge (7,126 files unchanged). Targeted Ruff formatting and checks passed for six Python files (one formatted, five unchanged), as did git diff --check and git diff --check gh/main...HEAD. No partner test edits, whole-state/folder runs or local microsimulation.

Historical Round-3 validation: 42 YAML cases across five files and five Hypothesis properties passed. The YAML counts are ND 6, NM credit 9, NM exemption 8, federal medical deduction 11, and NJ 8. Each property ran 12 generated examples plus one explicit example. The without-SE property covered arbitrary subtotal/component mixes; review-r3 found that the SE property rewrote generated supplied subtotals, so those passing checks did not establish supplied-subtotal compatibility. Round 4 replaces that interpretation with independent subtotal/component generation. No Hypothesis health check was suppressed.

Historical Round-3 runner/environment notes: Each YAML file ran serially as one explicit file per call to the standard core runner, retaining the fully initialized, unmodified model and its runner cache; the Python property file then ran alone through pytest in the same foreground process. This avoids the CLI's duplicate full-model initialization. Validation used the shared Python 3.13.9 interpreter with a workspace-only overlay of the core 3.32.8 and filelock 3.20.3 versions pinned in this checkout's lockfile. The two initial direct CLI attempts were interrupted during initialization or baseline cloning before any assertion results. make format, git diff --check, and git diff --check gh/main...HEAD passed. No folder suite or local microsimulation was run.

Round-2 validation: all 16 test files then changed by this PR passed, run serially as individual files in fresh foreground processes: 118 YAML cases across 15 files and 3 Hypothesis property tests (12 generated examples plus one explicit example per property). This includes the NJ, MO, MT, NM and ND medical-output files and the federal/state propagation cases. The two federal properties retain their original strategies and assertions; the third checks New Jersey's per-person exclusion bound.

make format (Ruff formatting and checks) passed before each round-2 commit, and git diff --check gh/main...HEAD passed. No local Hypothesis health-check suppression was needed. No full/folder suite or microsimulation was run.

No partner test was edited. A read-only inventory of all 144 partner YAML files (623 cases) found no nonzero premium/SE-ALD inputs or affected combination; the partner suite was not run.

Relationship to merged #10023

#10023 has merged into main. This PR uses its read-only, period- and branch-aware has_input_for_period helper. Merging current gh/main into this branch removes #10023's inherited changes from the PR diff; no sequencing requirement remains.

Current main already includes #10023's Idaho premium subtraction, claimant-only medical reconciliation, combined deduction/subtraction election, mandatory itemization, and 2026 limitation adjustment. Relative to pre-#10023 main, an enrolled senior couple without reported premium inputs receives a $4,440 Idaho premium subtraction (2 × $185 × 12), reducing Idaho taxable income from $134,000 to $129,560: $200,000 − $60,000 − $6,000 − $4,440. Current main already produces $129,560 in this example; it is historical context for #10023, not a change introduced by this PR. #10023 retains the existing model's assumption that derived out-of-pocket Part B premiums for enrolled taxpayers or spouses were paid by them. Main includes those modeled premiums in the federal Schedule A medical base and makes them available to the Idaho subtraction as well.

In “Derived claimant medical overlap uses premiums excluding pretax payroll payments” in id_health_insurance_premiums_subtraction.yaml, only the federal expectation changes: medical_expense_deduction: 4_250 becomes 3_250, with the arithmetic recorded in a comment: 6,000 - 1,000 + 500 - 2,250 = 3,250. This is the renamed case identified in the assignment as “Excluded premiums cannot enter derived claimant medical overlap”. Its Idaho outputs remain unchanged: qualified premiums $5,000, claimant medical deduction $3,250, and premium subtraction $1,750. The other SE-related cases either supply the Schedule A subtotal/deduction, supply Idaho itemization, or have no filer SE ALD; their asserted Idaho values remain unchanged. #10023 is already merged.

axiom: TheAxiomFoundation/rulespec-us#1654 queued

The current RuleSpec section 213 module lacks the section 162(l)(3) exclusion and no section 162 module was found. A signed encoder follow-up is needed to encode the cross-section exclusion with companion tests; no billed encoder run was initiated.

Hub microsimulation impact

Pending hub microsimulation. No local microsimulation or full/folder suite was run. Keep this PR in draft until the hub records population impact.

MaxGhenis added a commit that referenced this pull request Oct 11, 2026
- Line 2 reuses #10055's per-person excluded premiums; the new line 3
  variable nets the remainder of the self-employed deduction from
  long-term care premiums (26 U.S.C. 162(l)(2)(C) lets those premiums
  into that deduction).
- The separate-filing variant applies lines 2 and 3 only while the
  Itemized Deductions Schedule applies; a reform that allows separate
  filing after 2023 keeps premiums under the floor.
- Tests: supply the self-employed deduction where its earnings limit
  matters, a below-floor dependent case, #10055's Montana cases moved to
  lines 1-3, and invariants for premium conservation and #10055's
  applied exclusion.

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

Copy link
Copy Markdown
Contributor Author

Interaction with #10109 (self-employed health insurance deduction capped at 401(c) earned income: net profit less the deductible part of SE tax and the plan deduction, per 26 U.S.C. 162(l)(2)(A), 401(c)(2)(A)(v)-(vi) and Form 7206 lines 4-14).

Whichever of the two PRs lands second needs these updates. The values below are hand-computed from Form 7206, not yet run against a merge of the two branches.

policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/itemizing/medical_expense_deduction.yaml has two cases with $1,000 of Schedule C profit. The 2025 SE tax deduction is 1,000 x 0.9235 x 0.153 / 2 = 70.64775, so the derived deduction becomes 929.35225, not 1,000:

  • An earnings-limited derived SE deduction leaves unused premiums on Schedule A:
    • self_employed_health_insurance_ald 1,000 -> 929.35
    • itemized_medical_expenses 5,500 -> 5,570.65
    • medical_expense_deduction 3,250 -> 3,320.65
    • The case needs an absolute_error_margin.
  • Each filing unit excludes its own SE deduction independently of pretax premiums:
    • self_employed_health_insurance_ald [1,000, 1,000, 0] -> [929.35, 929.35, 0]
    • itemized_medical_expenses [5,500, 5,500, 6,500] -> [5,570.65, 5,570.65, 6,500]
    • medical_expense_deduction [3,250, 3,250, 4,250] -> [3,320.65, 3,320.65, 4,250]

tests/core/test_medical_expense_premium_conservation.py checks schedule_a_premium_use + se_deduction == paid with np.testing.assert_array_equal. The identity still holds after #10109, but the derived deduction is now fractional (float32), so exact equality may fail on rounding. assert_allclose(..., atol=0.01) is safe either way.

The other cases I checked are unaffected because premiums are under the new limit or there is a loss: the NJ dependent cases in #10055 (profit 10,000, premiums 1,000), and the Montana cases in #9947 (profits 30,000-50,000, premiums 5,000-6,000).

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.

1 participant