Elect Kentucky combined-separate filing on tax after non-refundable credits - #9349
Elect Kentucky combined-separate filing on tax after non-refundable credits#9349DTrim99 wants to merge 4 commits into
Conversation
…redits Kentucky's combined-separate election (ky_files_separately) compared income tax BEFORE non-refundable credits, choosing combined-separate whenever it lowered pre-credit tax by any amount. But under combined-separate each spouse's personal tax credits offset only their own column's tax (Form 740 lines 16-18), so a credit can be wasted when a column's tax is too low to absorb it. A couple could elect combined-separate for a tiny pre-credit saving and then forfeit a larger credit -- e.g. an over-65 spouse with only retirement income loses the $40 age-65 personal credit because their column tax is $0. Elect on liability after non-refundable credits instead, matching Delaware (issue PolicyEngine#7931). Two path-specific variables compute Kentucky income tax after the four ordered non-refundable credits for the combined-separate and joint paths without reading ky_files_separately (avoiding a circular dependency), and the election picks the cheaper path. Combined-separate is also now gated to married couples. Verified on the issue household (KY joint 2022, head 65 with no earned income, spouse 61 with $31,429 wages, one dependent): the couple now files jointly and ky_income_tax is 1,392.95 (was 1,432.95), preserving the $40 credit and matching the TaxAct return. Full Kentucky baseline suite passes. Fixes PolicyEngine#9342. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ECQhxDkVchXm1RNVyqwnx
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9349 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 1 4 +3
Lines 35 60 +25
Branches 2 0 -2
=========================================
+ Hits 35 60 +25
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… hb13 test - Use a changelog.d fragment instead of changelog_entry.yaml (the fragment format CI requires). - Gate the combined-separate election on a married couple identified by EITHER a spouse in the tax unit OR a joint filing status. Test and microdata inputs represent a couple with different conventions (some set is_tax_unit_spouse with a separate filing status, others set filing_status JOINT without a spouse flag), so accept either. This keeps the two "files separately" baseline integration tests electing combined-separate while excluding single filers. - The KY HB 13 "chooses separate filing" reform test supplies income directly as Kentucky taxable income, leaving modelled gross income at $0, which gives a spurious 100% family-size credit rate that zeroes both filing paths. Set ky_family_size_tax_credit_rate to 0 for that high-income couple so the after-credit election reflects the intended scenario. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ECQhxDkVchXm1RNVyqwnx
PavelMakarchuk
left a comment
There was a problem hiding this comment.
PR 9349 review — Elect Kentucky combined-separate filing on tax after non-refundable credits
- PR: #9349 by DTrim99 — fixes #9342 (found via PolicyEngine-TAXSIM #1149); mirrors DE #7931
- PR_HEAD:
18a7b93aa17b3ab819dd163232e9240fabbea168 - Branch status: BEHIND=0, AHEAD=2 — current against main
- CI: all 33 checks pass
- Mode: full (context, regulatory + PDF audit, code patterns, tests, references, verification queue)
- Severity: REQUEST_CHANGES — 2 CRITICAL / 8 SHOULD ADDRESS / 7 SUGGESTIONS. Both criticals are narrowly scoped and quickly fixable (one test input tweak, one page-anchor edit); the model change itself is verified correct.
What the PR does
Kentucky lets married couples choose between filing jointly (Status 3) and "married filing
separately on this combined return" (Status 2). Before this PR, ky_files_separately elected
the cheaper path by comparing pre-credit tax (ky_income_tax_before_non_refundable_credits_indiv
summed vs _joint). The PR changes the election to compare post-non-refundable-credit tax via
two new TaxUnit variables (ky_income_tax_before_refundable_credits_if_separate / _if_joint)
computed by a shared helper (ky_combined_separate.py), and gates the election to married couples.
Flippers: couples where combined-separate saves a little pre-credit but strands a personal credit
in a low/zero-tax column (e.g. an over-65 spouse with no taxed income losing the $40 age credit) —
they now correctly file jointly. Issue household: PE $1,433 → $1,393, matching TaxAct.
Verified substance (the fix is correct)
All of the following was verified against the fetched 2022 KY Form 740 + 740 Packet Instructions
(saved at /tmp/policyengine-command-runs/2b3d452ec8fd/pr9349-ky-740-2022-instructions.pdf),
with the snapshot model run empirically:
- Election criterion (post-credit tax) — correct. The instructions frame Status 2 vs 3 as a
free choice made to "reduce your tax"; "your tax" on the return is line 26 liability, i.e. tax
after non-refundable credits. Refundable credits are path-independent. An optimizing filer
(and TaxAct) compares final tax — the PR's central claim holds. - Form 740 credit ordering — matches. Lines 16–18 apply personal credits per column with a
zero floor in each column; line 19 sums columns; then family-size (lines 20–21), tuition
(line 23), CDCC at 20% (line 24). The helper applies exactly this order;_if_separatecaps each
spouse's personal credit at their own column tax,_if_jointpools. Also confirmed statutorily:
KRS 141.0205(2)(a)–(d) (read verbatim at legislature.ky.gov, id=57934) prescribes exactly
personal → family size (141.066) → tuition (141.069) → dependent care (141.067). - Path-neutrality of FSTC / tuition / CDCC — confirmed independently by the regulatory and
code roles: FSTC rate derives from totalky_modified_agi(identical under Status 2 and 3),
tuition potential from federal education credits, CDCC from the federal Form 2441 credit. The
helper's reuse of these variables is sound. - Cycle safety — traced two levels down and verified at runtime. The new
_if_*variables and
helper deliberately do not readky_files_separately; its only readers remain
ky_income_tax_before_non_refundable_credits_unitandky_personal_tax_credits_potential, both
downstream. Snapshot runs end-to-end with no recursion (avoids the MT-style cycle). - Arithmetic — every expected value in all three test files re-derived by hand and correct,
including the issue-9342 pin: joint (31,429 − 2,770) × 5% = 1,432.95 − $40 age credit =
1,392.95 ✓; FSTC rate 0 (MAGI 1.365 × threshold > 1.330 cutoff) ✓; the elected-path
predictor equals the computedky_income_tax_before_refundable_creditsin every run ✓. - DE mirror (#7931) — convention confirmed:
de_files_separately/
de_income_tax_before_refundable_credits_separate/_jointuse the same pattern and the same
bare-URL form-instruction citation style. (Naming deviates — see A4.) - hb13 contrib test edit — legitimate test hygiene, not hardcoding: with taxable income
supplied directly, modeled MGI is $0 → spurious 100% FSTC rate would zero both paths; the
ky_family_size_tax_credit_rate: 0override is verified necessary and all expected outputs
are unchanged.
CRITICAL
C1. The headline regression test is a pre-credit tie — it does not pin the behavioral change
policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/integration.yaml, new case
"Combined-separate election preserves the age-65 credit (issue 9342)".
The head (65) has $0 income and the spouse earns $31,429, so the two pre-credit paths tie
exactly — verified two independent ways:
- Empirically (regulatory role, snapshot run): pre-credit indiv sum = joint = 1,432.95
identical;if_separate= 1,432.95,if_joint= 1,392.95. - By float derivation (tests role): both paths are the same 28,659 × 5% product; sums of
[0, x] vs [x, 0] are bit-identical.
Under the OLD strict-< pre-credit election, a tie also resolves to joint → pooled $40 credit →
1,392.95 — exactly the new expected output. So this test passes on pre-PR code too. The
tests role swept every other election-exercising case (both 2022 SEPARATE/JOINT integration pairs,
blind couple, taxsim 1076, 2025 MFJ optimization, hb13): old and new formulas elect the same path
in all of them. The only tests failing on main are the rewritten ky_files_separately.yaml unit
cases, which inject the _if_* variables directly — pinning the comparison plumbing, not the
credit-aware path computation this PR exists to add. (As written, the PR household also does not
reproduce the issue's reported old output of ~$1,433 — old PE already returned 1,392.95 for it;
the real TAXSIM record evidently had a small pre-credit gap.)
To be clear: the fix itself is verified correct — the regulatory role reproduced 1,392.95 on
the snapshot and verified the post-credit criterion against the Form 740 instructions. What is
missing is a genuinely discriminating case. Fix (small): give the head a little KY-taxable
income, 0 < h < $800. Verified concretely both ways:
- head
employment_income: 400(tests role): OLD elects separate → 1,432.95 (credit wasted);
NEW:if_separate1,432.95 >if_joint(31,829 − 2,770) × 5% − 40 = 1,412.95 →
ky_files_separately: false,ky_income_tax: 1_412.95. - head
taxable_interest_income: 500(regulatory role, run on the snapshot): OLD 1,432.95 vs
NEW 1,417.95.
Either variant fails under the old logic and pins the actual issue-9342 mechanism (Form 740
lines 16–18 per-column zero floors vs line 26).
C2. Wrong page anchor on the two new path variables (#page=11 → #page=12)
ky_income_tax_before_refundable_credits_if_joint.py and ..._if_separate.py cite
https://revenue.ky.gov/Forms/740%20Packet%20Instructions%205-9-23.pdf#page=11, and the
_if_separate comment says "(Form 740 lines 16-18)". Verified against the fetched PDF (file page
= printed page + 2): file page 11 (printed 9) contains the filing-status definitions and
lines 5–13 only; the lines 15–24 credit application and ordering these variables implement —
"Line 19, Total Tax Liability—Married taxpayers filing a combined return must add the amounts on
line 18, Columns A and B…", family size, tuition, CDCC — is on file page 12 (printed 10).
Fix: change to #page=12 (or cite both: 11 for the combined-separate column structure, 12 for
the credit application). Same defect class as CT #9274 C1 — the review contract is that clicking
the link shows the fact. Note: the #page=11 cite on ky_filing_separately.py itself is correct
(filing-status election content) and passes; the anchor was evidently copied from KY-tree siblings
(ky_agi, ky_taxable_income_*) where page 11 is right for their content.
SHOULD ADDRESS
A1. Comment misnumbers the filing status ("filing status 3" should be Status 2)
ky_filing_separately.py (diff lines 192–193): "Combined-separate (filing status 3) is only
available to married couples". On the 2022 Form 740, combined-separate is Filing Status 2;
Status 3 is married filing joint (instructions file p.11). The PR body repeats the slip.
Comment-only; the logic is unaffected.
A2. Cite KRS 141.0205(2) — helper is uncited; add to the _if_* variables for consistency
ky_combined_separate.py implements the statutory credit ordering — the load-bearing legal claim
of this PR — but carries only prose ("Form 740 applies the credits in order…") with no URL or
statute. Add to its docstring, and to the two _if_* variables' reference, the verified cite:
KRS 141.0205(2)(a)–(d), "Priority of application and use of tax credits"
(https://apps.legislature.ky.gov/law/statutes/statute.aspx?id=57934 — use this current-version
id; the old id=53501 is a superseded-version stub), read verbatim:
"(2) After the application of the nonrefundable credits in subsection (1)…, the nonrefundable
personal tax credits against the tax imposed by KRS 141.020 shall be taken in the following
order: (a) The individual credits permitted by KRS 141.020(3); (b) The credit permitted by KRS
141.066; (c) The tuition credit permitted by KRS 141.069; (d) The household and dependent care
credit permitted by KRS 141.067…"
This is exactly the helper's order, is year-stable (unlike packet page anchors), and matches the
KY-tree convention of statute-first citations (ky_personal_tax_credits* → KRS 141.020, etc.).
Caveat for the record: the (2)(a)–(d) text was verified in the current codification; the TY2022-era
version could not be fetched (ordering presumed unchanged for 2022).
A3. Helper hard-codes the credit list/order that the real chain reads from a parameter
ky_combined_separate.py:20-33 applies personal → family size → tuition → CDCC in code, while the
post-election chain (ky_non_refundable_credits → ordered_capped_state_non_refundable_credits)
derives that same order from parameters/gov/states/ky/tax/income/credits/non_refundable.yaml.
Today they agree — verified item-for-item identical arithmetic. But a credit added to or reordered
in the parameter list would apply on the elected path while remaining invisible to the election
comparison, silently reintroducing the class of inconsistency this PR fixes. Full parameter-driven
reuse is hard (personal and FSTC potentials are path-dependent); at minimum add a comment
cross-referencing non_refundable.yaml, or iterate the parameter list for the path-neutral tail
credits. Maintainability, not a present-day bug.
A4. Naming deviates from the Delaware mirror (_if_separate/_if_joint vs _separate/_joint)
DE #7931 named the same-concept variables de_income_tax_before_refundable_credits_separate /
_joint. KY introduces an _if_ convention. Mitigating rationale: KY already has a Person-level
ky_income_tax_before_non_refundable_credits_joint, so _if_ disambiguates the TaxUnit
path-scenario variables from per-person column variables — an ambiguity DE lacks. Still now a
two-state pattern with two names; maintainers should pick one convention (rename to DE style, or
bless _if_ going forward). No name collisions either way.
A5. HEAD_OF_HOUSEHOLD and SURVIVING_SPOUSE gating untested
The new married-only gate is pinned only for SINGLE. Add direct-input unit cases mirroring the
single-filer case showing HOH and SURVIVING_SPOUSE units never elect (ky_files_separately.yaml).
A6. Post-credit tie behavior unpinned
Strict < means an exact post-credit tie files jointly. Add a unit case with
_if_separate: 100, _if_joint: 100 → ky_files_separately: false.
A7. FSTC × election interaction now untested everywhere
The hb13 edit (necessary — see verified substance) removed the only case where a nonzero FSTC
coexisted with the election, and no baseline married-election test has FSTC > 0. The helper
re-implements FSTC as rate × (base − applied_personal); verified equivalent to
ky_family_size_tax_credit_potential + the ordered-cap chain by code reading, but no test proves
the elected path's final ky_income_tax equals the winning _if_* value with FSTC nonzero. Add a
married couple near the FPL pinning the remaining-liability interaction.
A8. Tuition and CDCC steps of the helper untested
ky_tuition_tax_credit_potential and ky_cdcc_potential are 0 in every election test — steps 3–4
of the helper (and their path-neutrality in practice) are exercised by no test. One married case
with a nonzero federal CDCC (KY 20% match) alongside the election would pin Form 740 ordering
parity between the helper and ky_non_refundable_credits.
SUGGESTIONS
- Helper filename reads like a variable. Non-Variable modules in variables directories are
precedented and loader-safe, but precedents signal it (_apply_ssi_exclusions.py,
_jurisdictions.py,marginal_tax_rate_helpers.py). Consider_ky_combined_separate.pyor
folding the function into a consumer file. - "Both spouses had income." Form 740 conditions Status 2 on both spouses having income; the
model gates only on married. Near-harmless under post-credit election (a zero-income spouse's
column offers no advantage and strands credits, so joint weakly dominates) — add a comment
noting the deliberate simplification. - Redundant cap
min_(rate * remaining, remaining)in the helper's FSTC step (rate brackets
top out at 1.0) — harmless, arguably defensive; leave or simplify. - Gate asymmetry vs DE: KY gates on
is_joint | has_spousewhere DE uses onlyhas_spouse;
the extra JOINT arm is defensive and commented — fine, just noting the cross-state asymmetry. - Citation year-drift: the cited packet is TY2022; the 2023 packet repeats the same election
language with different pagination. Keeping the TY2022 cite is fine; the KRS cite (A2) removes
the drift risk. Pre-existing sibling statute links (id=53500,id=53501) now resolve to
superseded-version stubs — a follow-up could refresh them. - Test cosmetics: the new integration case's
absolute_error_margin: 0.1also covers the
boolean output (functional since < 1, but convention is margin-free booleans); a zero-tax-couple
case (both columns $0 → false, tax 0) would pin that outcome; the C1 discriminator could be
cheaply duplicated at 2025; case naming deviates fromCase N, description.but matches
neighboring KY files. - Pre-existing scope note (unchanged by this PR): Schedule ITC Section A business incentive
credits (Form 740 lines 15–16), which sit before the personal credits in each column, are not
modeled.
Validation summary
| Check | Result |
|---|---|
| Regulatory baseline vs code | PASS — election criterion, credit ordering, per-column capping, joint pooling, married-only gate all match 2022 Form 740 + instructions (quoted) |
| PDF audit | Done — pdf-collector folded into regulatory (740 Packet PDF fetched with browser UA, saved, quoted, paged); references role independently fetched the same PDF + KRS 141.0205 |
| Snapshot runs | PASS — issue-9342 household reproduces 1,392.95 (TaxAct $1,393); no recursion; predictor = computed tax in every run |
| Code patterns | 12/14 categories clean; no hardcoding, vectorization/entity/period/defined_for/changelog all clean |
| Test arithmetic | All expected values in all three test files re-derived by hand — correct |
| Reference anchors (5E) | The one added anchor (#page=11 on both new variables) was checked against the file and found wrong — that finding IS C2; correction #page=12 verified |
| Verification queue | NONE — both criticals settled empirically in Phase 4 (tie via regulatory's snapshot runs incl. the discriminator variant; wrong-page via references' paged evidence) |
| Cross-checks | Regulatory and code independently traced cycle safety and path-neutrality to the same conclusion; no XREF/EXT/MISMATCH remains |
| Branch/CI | BEHIND=0 AHEAD=2 (current); 33/33 checks pass |
Verdict
REQUEST_CHANGES. The model change is regulatorily and arithmetically verified correct, but
(C1) the test that claims to pin the behavioral fix is a pre-credit tie that passes under the old
logic — one small input tweak (head income ~$400–500) closes it — and (C2) the new variables'
page anchor points at the wrong page of the cited instructions (#page=12 is the verified
correction). Both are quick fixes; the eight should-address items (comment slip, KRS cite,
helper/parameter drift comment, naming convention, four coverage gaps) round out the request.
…rage Applies the Critical, Should-Address, and actionable Suggestion items from PavelMakarchuk's 2026-08-27 review. No formula-logic or numeric-behavior change; the model fix is unchanged. - C1: make the issue-9342 headline case discriminating — give the age-65 head $400 of income so the paths differ (was a pre-credit tie that passed on old code); assert ky_files_separately false and ky_income_tax 1,412.95. - C2: correct the new path variables' page anchor — cite Form 740 Packet #page=12 (credit application) alongside #page=11 (column structure). - A1: fix the comment — combined-separate is Filing Status 2, not 3. - A2: cite KRS 141.0205(2)(a)-(d) (priority of credit application) on the helper and both _if_* variables. - A3: comment cross-referencing the parameter-driven non_refundable.yaml order the helper mirrors. - A4: document the deliberate _if_ naming (disambiguates from the Person-level ky_income_tax_before_non_refundable_credits_joint). - A5: add HEAD_OF_HOUSEHOLD and SURVIVING_SPOUSE gate cases (never elect). - A6: add a post-credit tie unit case (equal paths -> file jointly). - A7/A8: add FSTC-nonzero and CDCC-nonzero married election cases. - Suggestion 1: rename the helper module to _ky_combined_separate.py. - Suggestion 2: comment the married-only gate simplification. - Suggestion 6: margin-free boolean assertions, a zero-tax couple case, and a 2025 duplicate of the discriminator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YCwrSsGY7vPCpKYGfV1xRr
Review addressed — thanks @PavelMakarchukApplied both Critical items, all eight Should-Address items, and the actionable Suggestions. No change to the model logic (verified correct); everything is a test, citation, comment, or file-rename. Critical
Should Address
Suggestions
Fixed with Claude Code assistance. |
…ctions) CI showed the A8 CDCC married-couple case elects combined-separate (ky_files_separately true), not joint: with two equal $30k earners the separate path gets each column its own standard deduction (2 x 2,770), so it is cheaper pre-credit under KY's flat 5% tax. The prior joint-based expected value was wrong. The exact separate-path post-credit ky_income_tax depends on KY personal-credit details that cannot be pinned without running the model, so the case now asserts only the CI-confirmed election outcome (the CDCC still flows through the helper's dependent-care step); C1 pins the post-credit currency. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YCwrSsGY7vPCpKYGfV1xRr
Follow-up: A8 corrected after CIThe first CI run flagged exactly one of the new cases — A8 (the CDCC married couple). It turned out the couple elects combined-separate rather than filing jointly: with two equal $30k earners, the combined-separate path gives each column its own standard deduction (2 × $2,770 vs one on the joint return), so it's cheaper pre-credit under KY's flat 5% rate. My initial "does not elect" assumption (and the joint-based expected tax) were wrong. I corrected A8 to assert the CI-confirmed election ( If you'd prefer A8 to also pin the separate-path post-credit tax, I can add the exact figure once it can be read off a run. |
Summary
Kentucky's combined-separate election (
ky_files_separately) compared income tax before non-refundable credits, electing combined-separate whenever it lowered pre-credit tax by any amount. But under combined-separate each spouse's personal tax credits offset only their own column's tax (Form 740 lines 16–18), so a credit is wasted when a column's tax is too low to absorb it. A couple could elect combined-separate for a tiny pre-credit saving and then forfeit a larger credit.Concrete case (taxsim #1149): a couple where the 65-year-old spouse has only retirement income and the other spouse has wages. Combined-separate saves ~$0.18 of pre-credit tax but strands the over-65 spouse's $40 age-65 personal credit against their $0 column tax. PE elected combined-separate and reported $1,433; the TaxAct return files jointly and applies the $40, landing at $1,393.
Change
Elect on liability after non-refundable credits, mirroring Delaware (#7931):
ky_income_tax_before_refundable_credits_if_separateand…_if_joint— compute Kentucky tax after the four ordered non-refundable credits (personal, family size, tuition, dependent care) for each path, without readingky_files_separately(avoiding a circular dependency). A shared helper (ky_combined_separate.py) applies the Form 740 credit ordering.ky_files_separatelynow picks the cheaper post-credit path and is gated to married couples (combined-separate, filing status 3, is only available to them).ky_income_tax_before_refundable_creditscomputes as before.Testing
ky_income_tax= 1,392.95 (was 1,432.95), preserving the $40 credit and matching TaxAct. Added as a regression test.ky_files_separatelyunit test for the post-credit, spouse-gated logic.Found via PolicyEngine-TAXSIM discrepancy #1149, reported by @feenberg.
Fixes #9342.
🤖 Generated with Claude Code