Skip to content

Fix Vermont student loan interest, Utah's at-home parent credit and Wisconsin's homestead claimant - #10104

Open
MaxGhenis wants to merge 14 commits into
mainfrom
vt-ut-wi-statutory-fixes
Open

MaxGhenis wants to merge 14 commits into
mainfrom
vt-ut-wi-statutory-fixes

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What changes

This PR fixes three statutory errors in Vermont, Utah and Wisconsin formulas, found while applying those states' rules to a filer who can be claimed as a dependent (#10044). They change results for households where nobody is claimed, so they go to main on their own rather than waiting on that stack.

The Vermont child tax credit fix that this PR used to carry now lives in #10644 (Fixes #9996), as a maintainer asked on 2026-10-27. Commit e553dc0 reverts it here.

Formula Rule now Was Source
vt_student_loan_interest_subtraction (new); gov.states.vt.tax.income.agi.subtractions; ...agi.student_loan_interest.income_limit and .in_effect (new) From 2022, Vermont subtracts the qualified education loan interest the filers paid, minus the federal student loan interest deduction, when federal AGI is at most $200,000 (joint) or $120,000 (single, head of household, married filing separately). § 5811(29)(B) sets no limit for a qualifying surviving spouse, so a surviving spouse gets no subtraction. IN-112 line 16c gives "All other filers" $120,000; this PR follows the statute and records the conflict in source_conflicts/candidates.yaml as us-vt:student-loan-interest-surviving-spouse-limit. There's no subtraction in 2021. The raw interest was subtracted from 2021, deducting it a second time for anyone who took the federal deduction, with no income limit 32 V.S.A. § 5811(21)(B)(vi) and (29)(B); 2021, No. 138 (Adj. Sess.), §§ 5, 6, eff. Jan. 1, 2022 (the history note to § 5811); 32 V.S.A. § 5822(a); 2025 Schedule IN-112, lines 16a–16c and instructions. tax.vermont.gov refuses automated downloads, so the booklet links are NBER's archive copies.
ut_at_home_parent_credit_potential $100 per qualifying child per return when the claimant or the joint-filing spouse is an at-home parent $100 per child multiplied by the number of spouses who qualified Utah Code § 59-10-1005(2): $100 for each qualifying child when "the claimant or another claimant filing a joint individual income tax return with the claimant is an at-home parent"
wi_homestead_eligible One spouse, the claimant, must be 18 or older; 62 or older, disabled or with earned income; and not claimed as a dependent on another return unless 62 or older Age, disability and age 62 were tested across the head and spouse, and earned income across every member of the tax unit, dependents included. The dependency bar was never read. Wis. Stat. § 71.53(2)(c) (claimant under 18 at the close of the year), § 71.53(2)(d) (claimant claimed as a dependent, unless 62 or older), § 71.54(1)(gm) (claimant with no earned income, for claims filed in 2018 and later), § 71.52(1) (claimant) and § 71.52(1g) (earned income); 2025 Schedule H instructions, Steps 1 and 2a (p. 4) and the earned income definition (p. 10)

Vermont's federal offset. Under § 5811(21)(B)(vi), Vermont subtracts "the amount of interest paid by a qualified resident taxpayer during the taxable year on a qualified education loan" for attendance at an eligible educational institution. Read with the no-double-deduction canon in CONTRIBUTING.md, the subtraction covers only the interest left after the federal deduction. The filer computes that amount on IN-112 lines 16a to 16c (16b: "Enter any student loan interest already deducted on federal Form 1040, Schedule 1, Line 21").

Examples (2025 unless noted)

  • Vermont, single, wages $10,000, interest paid $1,000: the federal deduction already takes the $1,000, so Vermont subtracts nothing more and Vermont AGI is $9,000 (was $8,000). With $4,000 of interest at $50,000 of wages, the federal deduction is capped at $2,500, so Vermont subtracts $1,500. Single at $125,000 of AGI: no subtraction. Single at exactly $120,000, or joint at exactly $200,000 (2022): the whole $3,000. In 2021: no subtraction.
  • Vermont, surviving spouse with a 10-year-old: at $110,000 of wages and $3,000 of interest, no federal deduction and no Vermont subtraction (the form would give $3,000); at $50,000 and $4,000, a $2,500 federal deduction and no Vermont subtraction (the form would give $1,500).
  • Utah, both spouses earning $3,000 or less, one newborn: $100 (was $200). With two children: $200 (was $400).
  • Wisconsin, a couple aged 30 where the only earner is claimed on a parent's return: not eligible (was eligible). A claimed claimant aged 62 or older stays eligible. An 18-year-old with no earnings married to a 17-year-old earner: not eligible (was eligible).

Readings

  1. Wisconsin homestead, earned income. Schedule H Step 1a lists "earned income" for "you (or your spouse, if married)" without the claimant condition it attaches to age and disability. This PR follows page 10: "The spouse that meets this definition must be the claimant." That matches the statute. Wisconsin applies each test in §§ 71.53(2)(c) and (d) and 71.54(1)(gm) to the claimant, and counts as earned income only the claimant's own wages and net self-employment earnings (§ 71.52(1g)). When two members of a household can each qualify, "they may determine between them as to who the claimant is" (§ 71.52(1)). Under the household reading, a couple where only the claimed spouse earns would qualify.
  2. "Claimed" vs "can be claimed". Wisconsin bars a claimant who "was claimed as a dependent for federal income tax purposes by another person during the year to which the claim relates", unless 62 or older (§ 71.53(2)(d)). In Step 2a of Schedule H, the claimant answers whether someone claimed or will claim them. So the formula reads claimed_as_dependent_on_another_return.
  3. Vermont, qualifying surviving spouse. Vermont sets the § 5811(29)(B) limit for four filing statuses only: "$120,000.00 if the individual's filing status is single, head of household, or married filing separately" and "$200,000.00 if the individual's filing status is married filing jointly". Wherever the chapter includes surviving spouses, the General Assembly names them (§ 5811(21)(C)(i) and (ii), § 5822(a)(1) and (3)). For example, it gave one standard deduction to filers "unmarried (other than surviving spouses or heads of households)" or filing separately (§ 5811(21)(C)(ii)(I)), and one rate schedule to "Married individuals filing joint returns and surviving spouses" (§ 5822(a)(1)). So a surviving spouse falls outside all four statuses, and outside the definition of a qualified resident taxpayer. The statute isn't silent, so the form's "All other filers" $120,000 has no gap to fill. income_limit.yaml gives SURVIVING_SPOUSE -.inf; vt_student_loan_interest_subtraction is its only reader, and its agi <= limit test is false for every surviving spouse. The conflict is candidate us-vt:student-loan-interest-surviving-spouse-limit (follows: statute, state: ruled); the parameter's comment is counted in KNOWN_UNMARKED until the marker lands in the follow-up PR.

Invariants

policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py checks each one over Hypothesis-drawn households:

  1. Vermont, from 2022: subtraction + federal deduction = the filers' interest paid when AGI is within the limit. The model gives no subtraction above the limit or to any surviving spouse, and never a negative one. The property draws single, joint and surviving spouse returns, and the YAML cases pin 2021.
  2. Utah: the credit doesn't change when a second spouse also qualifies as an at-home parent.
  3. Wisconsin: eligibility is the same whichever spouse is labelled head, and marking a spouse as claimed never makes a return eligible.

Tests

  • vt_student_loan_interest_subtraction.yaml: 15 passed. ut_at_home_parent_credit.yaml: 5 passed. wi_homestead_eligible.yaml: 24 passed. test_vt_ut_wi_statutory_invariants.py: 3 passed. test_source_conflicts.py: 103 passed.
  • Mutants, each run in its own process and then restored. Rows M1, M2 and M4 ran at 6f203e8ba3 against the 15-case Vermont file; the other rows ran against the earlier 12-case file:
Mutant Changed YAML file Property test Killed by
R1 VT: subtract the whole interest 3 of 12 fail fails YAML and property
R2 VT: no income limit 4 of 12 fail fails YAML and property
R3 UT: one credit per qualifying spouse 2 of 5 fail fails YAML and property
R4 WI: no dependency bar 4 of 24 fail passes YAML
R5 WI: any tax unit member's earnings 6 of 24 fail passes YAML
R6 WI: earnings summed across both spouses 5 of 24 fail passes YAML
R7 VT: < instead of <= 2 of 12 fail (the exact $120,000 and $200,000 cases) passes YAML
R8 VT: SURVIVING_SPOUSE $200,000 2 of 12 fail (both surviving-spouse cases: 3,000 and 1,500 vs 0) fails YAML and property
R9 WI: 18-or-older tested across the couple 2 of 24 fail passes YAML
R10 VT: dependents' interest counted 1 of 12 fails (1,000 vs 0) passes YAML
O1 VT: SURVIVING_SPOUSE $120,000 (the form's reading) 2 of 12 fail (3,000 and 1,500 vs 0) fails YAML and property
O2 VT: no 2022 effective-date gate 1 of 12 fails (1,500 vs 0, 2021) passes YAML
M1 VT: HEAD_OF_HOUSEHOLD -.inf 1 of 15 fails (the head of household case at $120,000: 0 vs 3,000) passes YAML
M2 VT: SEPARATE -.inf 1 of 15 fails (the married filing separately case at $120,000: 0 vs 3,000) passes YAML
M4 VT: HEAD_OF_HOUSEHOLD $200,000 1 of 15 fails (the head of household case at $125,000: 3,000 vs 0) passes YAML

None survives.

  • CI: Pull request run 41947821257 passed all 36 jobs at 6f203e8ba3.

Overlap with open PRs

impact: the hub will measure
axiom: TheAxiomFoundation/rulespec-us#2029 queued (Vermont student loan interest; corpus re-pin and source review); TheAxiomFoundation/rulespec-us#2030 queued (Utah at-home parent credit; historical corpus ingestion and re-pin); TheAxiomFoundation/rulespec-us#2031 queued (Wisconsin homestead claimant; statutory-source verification and corpus re-pin). All await encoder approval.

https://claude.ai/code/session_01LwQq6JwDtRRMHxwVAQMx6f


Generated by Claude Code

MaxGhenis and others added 5 commits October 11, 2026 12:21
32 V.S.A. 5811(21)(B)(vi) (2022 Act 138, from 2022) subtracts the interest a
qualified resident taxpayer paid on a qualified education loan, for AGI up to
$200,000 joint or $120,000 otherwise (5811(29)(B)). Schedule IN-112 line 16
subtracts the interest paid (16a) less the amount "already deducted on
federal Form 1040, Schedule 1" (16b). The model subtracted the raw interest
from 2021, deducting it a second time for anyone who took the federal
deduction, with no income limit; the 2021 IN-112 has no such line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
32 V.S.A. 5830f(b) reduces "the amount of the credit per child" by $20 for
each $1,000 of AGI over $125,000; the model reduced the total once, so two
young children at $150,000 got $1,500 instead of $1,000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Utah Code 59-10-1005(2) allows $100 "for each qualifying child if ... the
claimant or another claimant filing a joint individual income tax return with
the claimant is an at-home parent". The model multiplied the credit by the
number of spouses earning $3,000 or less; the existing two-parent case
asserted $400 for two children and now expects $200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Schedule H instructions put each condition on the claimant: 18 or older;
62 or older, disabled or with earned income ("The spouse that meets this
definition must be the claimant"); and not claimed as a dependent on another
person's federal return unless 62 or older (Wis. Stat. 71.53(2)(d); Step 2a).
Spouses choose the claimant. The model tested age and earnings across the
household and never read the dependency bar. The existing cases now use
person inputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
…split

The Vermont student loan subtraction, the Vermont child tax credit per-child
phase-out, Utah's at-home parent credit once per return and Wisconsin's
homestead claimant tests change results for households where nobody is
claimed, so they now go to main in #10104; this
branch keeps the base formulas there.

New cases: a filer who can be claimed but is not claimed (no Wisconsin $250
exemption; still a Rhode Island relief and Vermont renter credit claimant),
and a would-be claimant who meets the filing exception (South Carolina,
Utah, Vermont and West Virginia dependents survive). The property test now
draws claimability, actual claims and the filing exception per spouse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The subtraction list already omits the variable in 2021, but the variable
itself returned a positive amount when computed for 2021. An in_effect
parameter (false before 2022) makes it zero there. The Act 138 references now
cite Sec. 5 (the subtraction, pages 4-5) and Sec. 13 (effective retroactively
on January 1, 2022, page 15); the earlier reference named Sec. 4.

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

Copy link
Copy Markdown
Collaborator

@MaxGhenis — this draft contains the fix for #9996: 32 V.S.A. § 5830f(b) reduces "the amount of the credit per child", and the 2025 Vermont booklet's table reaches $0 per child at $174,001, but vt_ctc applies the $20-per-$1,000 reduction once per return. On main (4c65c82) a two-child case still gives 1,900 / 2,900 / 900 where the statute gives 1,800 / 2,700 / 0.

The draft does not link #9996 and bundles three other state fixes. Could you split the Vermont CTC change into its own PR that says "Fixes #9996", or add the link here? We will not open a parallel PR.

🤖 Posted by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

A maintainer asked for the per-child phase-out under 32 V.S.A. 5830f(b) as
a separate PR that fixes #9996; #10644 now carries it with its own tests.
This reverts 2f79c67 and drops the matching property and changelog text,
so this PR holds only the Vermont student loan, Utah and Wisconsin fixes.

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

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub, continuing an earlier session's work. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

@MaxGhenis MaxGhenis changed the title Fix Vermont student loan interest and CTC phase-out, Utah's at-home parent credit and Wisconsin's homestead claimant Fix Vermont student loan interest, Utah's at-home parent credit and Wisconsin's homestead claimant Nov 3, 2026
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 99ae1e525a242067b577de6533889ac6a397eff5

I continue an earlier session's work. That session pushed nothing here, so the starting point was the author's head, 7ae084b.

What the items asked for, and how it's resolved

  • Remove the Vermont CTC change with a normal commit. Commit e553dc02 reverts 2f79c67 (vt_ctc.py and vt_ctc.yaml now match main). It also drops the CTC property test_vt_ctc_phase_out_is_per_child and its docstring entry from policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py, and removes the CTC clause from changelog.d/vt-ut-wi-statutory-fixes.fixed.md. Phase out Vermont's child tax credit per child #10644 carries the fix now (Fixes #9996). No CTC file is left in this PR's diff against main.
  • Still needed? Yes. Main at abc67b9 has no vt_student_loan_interest_subtraction, still lists raw student_loan_interest in Vermont's subtractions, and still has the per-parent Utah multiplier and the household-wide Wisconsin test. No open PR duplicates any of the three fixes. See Overlap below.
  • Merge main. Commit 99ae1e52 merges origin/main (abc67b9) with no conflicts.
  • Reviews. There are no reviews or inline comments. The only issue comment besides the pick-ups is the 2026-10-27 request to split out the CTC, which is now done.
  • Body. Rewritten without the CTC and without a local path in the axiom line. It carries impact: the hub will measure and axiom: (hub files pe-parity before landing). The title drops "and CTC phase-out".
  • Changelog. The fragment existed; I only edited it.

Fixes kept (unchanged by me)

  • VT: policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_student_loan_interest_subtraction.py:21-42, the parameters agi/student_loan_interest/{in_effect,income_limit}.yaml, and agi/subtractions.yaml (2022+ list).
  • UT: policyengine_us/variables/gov/states/ut/tax/income/credits/ut_at_home_parent_credit_potential.py:24-38.
  • WI: policyengine_us/variables/gov/states/wi/tax/income/credits/homestead/wi_homestead_eligible.py:9-39.

Hand checks of the YAML cases, done from the rules as the PR states them:

  • VT: $4,000 of interest at $50,000 of wages. The federal deduction is capped at $2,500, so Vermont subtracts $1,500.
  • UT: two at-home spouses with one newborn get 1 × $100.
  • WI: a 30-year-old claimed earner isn't an eligible claimant; at 62 the dependency bar lifts.

Tests (this session, at the merged head)

  • Changed files: vt_student_loan_interest_subtraction.yaml 7 passed, ut_at_home_parent_credit.yaml 5 passed, wi_homestead_eligible.yaml 15 passed, test_vt_ut_wi_statutory_invariants.py 3 passed, code_health/test_source_conflicts.py 103 passed.
  • Dependents: vt_other_subtractions.yaml 2, vt_taxable_income.yaml 4, VT integration.yaml 3, wi_homestead_credit.yaml 4, wi_homestead_property_tax.yaml 1, wi_retirement_income_exclusion_elected.yaml 13, WI integration.yaml 1. All passed.
  • ruff format and ruff check on the edited test file: clean.

Mutants (each in its own process, then restored)

Mutant Failed
VT: subtract the whole interest (no federal offset) 3 of 7
VT: no income limit 2 of 7
UT: one credit per qualifying spouse 2 of 5
WI: no dependency bar 4 of 15
WI: any household member's earnings qualify 2 of 15

CI

The PR is a draft, so CI skips it. The Pull request run at 99ae1e5 concluded skipped (https://github.com/PolicyEngine/policyengine-us/actions/runs/41720633512), so it hasn't run.

Overlap with open PRs

Judgment calls and limits

  • I couldn't fetch any primary source. The network policy blocks legislature.vermont.gov, taxsim.nber.org, le.utah.gov, revenue.wi.gov and web.archive.org, so I couldn't recheck the original author's quotes. The rewritten body paraphrases and links the sources instead of quoting them.
    • One thing to check before landing: whether 32 V.S.A. § 5811(21)(B)(vi) itself limits the subtraction to interest not deducted federally, or only IN-112 line 16b does. If only the form does, that's a statute-vs-form question for CONTRIBUTING.md#source-conflicts. Since federal AGI already excludes the deducted interest, subtracting it again would double-count, so I expect the readings agree. I couldn't confirm that.
  • The two Wisconsin readings (page 10's "earning spouse must be the claimant" over the Step 1a household wording, and "claimed" over "claimable") stay as the author chose them. Both are listed under Readings in the body. Neither pits a statute against a form as I read them, so there's no # Source conflict marker; test_source_conflicts.py passes.
  • I left the WI and VT formulas themselves unchanged. My changes are the CTC removal, the merge, and the body and title.

Proposed follow-up

None.


Generated by Claude Code

@MaxGhenis
MaxGhenis marked this pull request as ready for review November 3, 2026 21:30
@MaxGhenis

MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Hub review in progress

Superseded by the final review: #10104 (comment) (VERDICT: REQUEST_CHANGES at 99ae1e525a242067b577de6533889ac6a397eff5).


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: REQUEST_CHANGES
99ae1e5
METHODOLOGY_CHOICE: yes

The head hasn't moved since the hand-off: 99ae1e525a is still the head, and its last commit is the 20:15Z merge of main.

All three fixes are right on the hub's reading of the sources, and each defect reproduces at main. CI is green at the head. I'm asking for changes for three reasons:

  • The PR follows IN-112's $120,000 for a qualifying surviving spouse, where 32 V.S.A. § 5811(29)(B) names no amount, and it doesn't record that conflict as CONTRIBUTING.md requires.
  • Four mutants survive, including the hub's < and surviving-spouse mutants.
  • Two comments, and the body, state the law wrongly.

Each fix below is small, and I ran the full set on a scratch copy: the guard passes 103, the VT file 11 and the WI file 16.

Sources. This VM's network policy refused every source with a proxy 403 on CONNECT: legislature.vermont.gov, taxsim.nber.org, le.utah.gov, docs.legis.wisconsin.gov, revenue.wi.gov, govinfo.gov, uscode.house.gov, ecfr.gov and web.archive.org. Every legal reading below rests on the hub's reading of 2026-11-03, quoted in the review brief. I quote nothing from memory.

Findings

P2-1. Vermont's surviving-spouse limit follows the form over the statute, and the PR doesn't record the conflict

policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/income_limit.yaml:22-24 gives SURVIVING_SPOUSE $120,000, citing IN-112 line 16c's "All other filers".

On the hub's reading, § 5811(29)(B) defines a "qualified resident taxpayer" by two limits: (i) "$120,000.00 if the individual's filing status is single, head of household, or married filing separately", and (ii) "$200,000.00 if ... married filing jointly". The definition lists those statuses and no others. Read literally, a qualifying surviving spouse never meets it and gets no subtraction at any income. Line 16c gives the surviving spouse $120,000. The two texts give different answers, the canon that definitions control favours the statute, and the PR follows the form. That's a source conflict under CONTRIBUTING.md#source-conflicts.

Failure scenario: a 2025 surviving spouse with $110,000 of federal AGI and $3,000 of interest (none deducted federally, since MAGI is above the $100,000 single end) gets a $3,000 subtraction. A literal reading of (29)(B) gives $0. I ran this case (W5) at the head: vt_student_loan_interest_subtraction 3,000.

The brief asked for a # Source conflict marker and a register entry. CONTRIBUTING.md (step 1, and "The marker comes only in the follow-up PR") has the ruling PR add a source_conflicts/candidates.yaml item and no marker, because the guard rejects a marker whose id isn't in the pinned register. So this PR should add the item below, keep $120,000 until the hub rules, and leave the marker to the follow-up. Insert it after us-vt:snap-child-support-deduction:

- id: us-vt:student-loan-interest-surviving-spouse-limit
  program: income-tax
  years:
    from: 2022
    type: tax
  question: Does a qualifying surviving spouse get Vermont's student loan interest subtraction, when 32 V.S.A. §
    5811(29)(B) sets an income limit only for single, head of household, married filing separately and joint filers?
  sources:
  - kind: statute
    citation: 32 V.S.A. § 5811(21)(B)(vi), (29)(B)
    url: https://legislature.vermont.gov/statutes/section/32/151/05811
  - kind: form
    citation: 2025 Vermont Schedule IN-112, line 16c
    url: https://taxsim.nber.org/historical_state_tax_forms/VT/2025/Income-Booklet-2025.pdf#page=23
  readings:
    statute: (29)(B) defines a qualified resident taxpayer by a $120,000 limit for single, head of household and
      married filing separately filers and a $200,000 limit for joint filers. It names no amount for a qualifying
      surviving spouse, so a surviving spouse doesn't meet the definition and gets no subtraction.
    other: Line 16c applies $120,000 to "All other filers", a qualifying surviving spouse included.
  example: A 2025 surviving spouse with $110,000 of federal AGI and $3,000 of student loan interest, none deducted
    federally, subtracts $3,000 under line 16c and nothing under a literal reading of (29)(B).
  pe_us:
    follows: conflicting-source
    code:
    - policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/income_limit.yaml
    prs:
    - https://github.com/PolicyEngine/policyengine-us/pull/10104
  state: open
  note: The PR keeps the form's $120,000 until the hub rules. Code not yet on main.

Step 1 also asks for the YAML case that pins the reading: that's the surviving-spouse case in P2-2. I checked the item with uv run pytest -q -p no:cacheprovider policyengine_us/tests/code_health/test_source_conflicts.py on a scratch copy: 103 passed.

The federal offset is not a second conflict. (21)(B)'s "to the extent such income is included in federal adjusted gross income" qualifies "items of income". Interest paid isn't an item of income, so that clause has nothing to act on and doesn't produce the offset. Read alone, (vi) subtracts "the amount of interest paid". But CONTRIBUTING.md lists the canon "no double deduction without a clear statement", and (vi) has no clear statement that allows a second deduction of interest already kept out of federal AGI. Read with that canon, the statute gives the same result as lines 16a to 16c. That's one reading, so it needs no register entry. The body should credit the offset to the canon and the form, not to the statute's words (P2-4).

P2-2. Four mutants survive: Vermont's <= boundary, the surviving-spouse limit, the age test and dependents' interest

I reran the hand-off's five mutants, then the hub's three and two of my own, each in its own process on a scratch copy, and restored the code after each. git status was clean at the end.

Mutant Changed YAML file Property test Result
VT: subtract the whole interest 3 of 7 fail fails killed
VT: no income limit 2 of 7 fail fails killed
UT: one credit per qualifying spouse 2 of 5 fail fails killed
WI: no dependency bar 4 of 15 fail passes killed
WI: any household member's earnings 2 of 15 fail passes killed
WI: earnings summed across both spouses (hub) 2 of 15 fail passes killed by the two "only the claimed spouse has earned income" cases
VT: < instead of <= (hub) 7 of 7 pass passes survives
VT: SURVIVING_SPOUSE $200,000 (hub) 7 of 7 pass passes survives
WI: 18-or-older tested across the couple, not the claimant (mine) 15 of 15 pass passes survives
VT: dependents' interest counted (mine) 7 of 7 pass passes survives

The command per mutant was uv run python -m policyengine_core.scripts.policyengine_command test <changed yaml> -c policyengine_us, then uv run pytest -q -p no:cacheprovider policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py -k <state>.

Failure scenarios that no test catches:

  • <: a single filer at exactly $120,000 of AGI loses the subtraction, against "equal to or less than".
  • $200,000: a surviving spouse at $150,000 keeps it.
  • Age across the couple: an 18-year-old with no earnings, married to a 17-year-old earner, counts as eligible. Main gives this couple eligible (W8 below), so this PR fixes it, but nothing pins the fix.
  • Dependents' interest: a dependent's interest flows into the parent's subtraction.

The property test can't catch the first two. It hard-codes the limit as np.where(joint, 200_000, 120_000), never draws a surviving spouse, and with integer wage draws and max_examples=3 it doesn't land on exactly $120,000.

Add these four cases at the end of policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/subtractions/vt_student_loan_interest_subtraction.yaml:

- name: 2025 single filer at exactly $120,000 of adjusted gross income keeps the Vermont student loan interest subtraction
  period: 2025
  input:
    people:
      filer:
        age: 30
        employment_income: 120_000
        student_loan_interest: 3_000
    tax_units:
      tax_unit:
        members: [filer]
    households:
      household:
        members: [filer]
        state_code: VT
  output:
    # 32 V.S.A. 5811(29)(B)(i): AGI "equal to or less than" $120,000. The
    # federal deduction is fully phased out above $100,000 of MAGI, so 16a $3,000
    # - 16b $0 = $3,000.
    adjusted_gross_income: 120_000
    student_loan_interest_ald: 0
    vt_student_loan_interest_subtraction: 3_000

- name: 2022 joint return at exactly $200,000 of adjusted gross income keeps the Vermont student loan interest subtraction
  period: 2022
  input:
    people:
      head:
        age: 30
        employment_income: 200_000
        student_loan_interest: 3_000
      spouse:
        age: 30
        is_tax_unit_spouse: true
    tax_units:
      tax_unit:
        members: [head, spouse]
        filing_status: JOINT
    households:
      household:
        members: [head, spouse]
        state_code: VT
  output:
    # 5811(29)(B)(ii): AGI "equal to or less than" $200,000 if married filing
    # jointly. The 2022 federal deduction ends at $175,000 of joint MAGI.
    adjusted_gross_income: 200_000
    student_loan_interest_ald: [0, 0]
    vt_student_loan_interest_subtraction: 3_000

- name: 2025 surviving spouse with $150,000 of adjusted gross income gets no Vermont student loan interest subtraction
  period: 2025
  input:
    people:
      head:
        age: 40
        employment_income: 150_000
        student_loan_interest: 3_000
      child:
        age: 10
    tax_units:
      tax_unit:
        members: [head, child]
        filing_status: SURVIVING_SPOUSE
    households:
      household:
        members: [head, child]
        state_code: VT
  output:
    # Schedule IN-112 line 16c: "All other filers, if AGI is greater than
    # $120,000, enter -0-." 5811(29)(B) names no amount for this status.
    vt_student_loan_interest_subtraction: 0

- name: 2025 interest paid by a dependent is not part of the filer's Vermont student loan interest subtraction
  period: 2025
  input:
    people:
      head:
        age: 50
        employment_income: 60_000
      student:
        age: 22
        is_full_time_student: true
        is_tax_unit_dependent: true
        student_loan_interest: 1_000
    tax_units:
      tax_unit:
        members: [head, student]
    households:
      household:
        members: [head, student]
        state_code: VT
  output:
    # 5811(21)(B)(vi): interest "paid by a qualified resident taxpayer"; the
    # dependent is not the taxpayer on this return.
    student_loan_interest_ald: [0, 0]
    vt_student_loan_interest_subtraction: 0

Add this case at the end of policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_eligible.yaml:

- name: "2025 joint return where the only earner is 17 and the 18-year-old spouse has no earned income: no Wisconsin homestead claimant"
  period: 2025
  input:
    people:
      head:
        age: 18
      spouse:
        age: 17
        employment_income: 5_000
        is_tax_unit_spouse: true
    tax_units:
      tax_unit:
        members: [head, spouse]
        filing_status: JOINT
        wi_homestead_income: 9_000
    households:
      household:
        members: [head, spouse]
        state_code: WI
  output:
    # Wis. Stat. 71.53(2)(c) bars a claimant under 18, and 71.54(1)(gm) bars a
    # claimant under 62 who is not disabled and has no earned income. Neither
    # spouse meets both alone.
    wi_homestead_eligible: false

All five pass at the head: 4 passed (VT) and 1 passed (WI). They kill the four survivors:

  • <: 2 of 4 VT cases fail ([0.] differs from 3000.0 at 2025 and 2022).
  • $200,000: 1 of 4 fails ([3000.] differs from 0.0).
  • Dependents' interest: 1 of 4 fails ([1000.] differs from 0.0).
  • Age across the couple: the WI case fails ([1.] differs from 0.0).

Merged into the files, VT passes 11 and WI 16.

P2-3. A code comment misstates § 5811(29)(B)

policyengine_us/variables/gov/states/vt/tax/income/adjusted_gross_income/subtractions/vt_student_loan_interest_subtraction.py:35-36 says AGI is "at most $200,000 if married filing jointly and $120,000 otherwise". The statute gives $120,000 only to single, head of household and married filing separately filers. "Otherwise" hides the surviving-spouse gap that P2-1 records. Replace the two lines with:

        # 5811(29)(B): a qualified resident taxpayer's adjusted gross income is
        # at most $120,000 if single, head of household or married filing
        # separately, and $200,000 if married filing jointly. It names no
        # amount for a qualifying surviving spouse; income_limit.yaml applies
        # IN-112 line 16c's $120,000 for "All other filers".

This wording avoids the guard's conflict phrases, so KNOWN_UNMARKED doesn't change. With it in place, test_source_conflicts.py passes 103 and ruff format --check reports the file already formatted.

P2-4. The body states several rules the sources don't support

Each item gives the text to replace and its replacement.

  • VT row, "Rule now": "when AGI is at most $200,000 (joint) or $120,000 (other filers)". Replace with: "when federal AGI is at most $200,000 (joint) or $120,000 (single, head of household, married filing separately). § 5811(29)(B) names no amount for a qualifying surviving spouse. The code follows IN-112 line 16c's $120,000 for "All other filers", a source conflict recorded in source_conflicts/candidates.yaml as us-vt:student-loan-interest-surviving-spouse-limit."
  • VT offset. Add: "§ 5811(21)(B)(vi) subtracts "the amount of interest paid by a qualified resident taxpayer during the taxable year on a qualified education loan". Read with the no-double-deduction canon in CONTRIBUTING.md, that is the interest left after the federal deduction, which is what IN-112 lines 16a to 16c compute ("Enter any student loan interest already deducted on federal Form 1040, Schedule 1, Line 21")."
  • VT effective date. The body cites "2022 Act 138, Sec. 5 (Sec. 13, p. 15, makes it effective January 1, 2022)". On the hub's reading, the § 5811 history note lists "2021, No. 138 (Adj. Sess.), §§ 5, 6, eff. Jan. 1, 2022". I couldn't open the act to check Sec. 13. Replace with: "2021, No. 138 (Adj. Sess.), §§ 5, 6, eff. Jan. 1, 2022 (the history note to § 5811)". The same applies to the in_effect.yaml and income_limit.yaml reference titles and the code comment at line 40.
  • WI row, source column. It cites only "Wis. Stat. § 71.53(2)(d)". Cite § 71.53(2)(c) ("under 18 years of age at the close of the year"), § 71.53(2)(d) (claimed as a dependent unless 62 or older), § 71.54(1)(gm) (no earned income, "For claims filed in 2018 and thereafter"), and § 71.52(1) and (1g), with links. The brief asked for a page link for each source.
  • WI "Was" column. "Age and earnings were tested across the household" is incomplete. Main tested age 18+ and 62+ across head and spouse, and earnings across every tax unit member, dependents included. Replace with: "Age, disability and age 62 were tested across the head and spouse, and earned income across every member of the tax unit, dependents included. The dependency bar was never read."
  • Readings, item 1. "This PR follows page 10, which matches the claimant-based conditions in Wis. Stat. § 71.53." The earned income condition sits in § 71.54(1)(gm), not § 71.53. Replace with: "This PR follows page 10 ("The spouse that meets this definition must be the claimant"). That matches the statute: § 71.53(2)(c) and (d) and § 71.54(1)(gm) each test "the claimant", § 71.52(1g) defines earned income as the claimant's own wages and net self-employment earnings, and § 71.52(1) lets a couple choose which of them is the claimant."
  • Readings, item 2. It can quote the statute: § 71.53(2)(d) bars a claimant who "was claimed as a dependent for federal income tax purposes by another person", which supports claimed_as_dependent_on_another_return over the claimable variable.
  • Utah row. It can quote (2): "$100 for each qualifying child if: (a) the claimant or another claimant filing a joint individual income tax return with the claimant is an at-home parent".
  • The hand-off's CI line is stale. It says CI skipped the draft. The PR is no longer a draft, and the Pull request run at this head (41722397999) concluded success: all 38 jobs succeeded, none were skipped or cancelled.

The impact: and axiom: lines are present. I left them as they are.

P3-1. The WI reference tuple cites no statute

policyengine_us/variables/gov/states/wi/tax/income/credits/homestead/wi_homestead_eligible.py:9-14 cites the 2021 LFB paper and the Schedule H instructions. The comments at lines 23-26 cite only the instructions for the age and earned income tests. #10191's version of this file cites https://docs.legis.wisconsin.gov/statutes/statutes/71/viii/53/2/c and https://docs.legis.wisconsin.gov/statutes/statutes/71/viii/54. Carry those into the tuple, and cite "Wis. Stat. 71.53(2)(c), 71.54(1)(gm)" in the comment at lines 23-24. I couldn't reach either URL to confirm the subchapter path.

Answers to the brief's questions

  • Vermont's AGI. The formula compares federal adjusted_gross_income with the limit, using <= for "equal to or less than". Line 16c's test decides a subtraction that produces Vermont AGI, so reading its "AGI" as Vermont AGI would be circular. Federal AGI is the only reading that works without circularity, and (21)(B) is itself framed as a decrease of "federal adjusted gross income". I couldn't open IN-111 to confirm that line 1 is federal AGI.
  • Utah.
    • (2) has the same wording in every version the hub read. The parameters start in 2021, so the per-return count p.amount * count_qualifying_children * has_at_home_parent is right in every year the model covers.
    • The $50,000 AGI test in (2)(b) is applied through defined_for = "ut_at_home_parent_credit_agi_eligible", on both ut_at_home_parent_credit_potential and ut_at_home_parent_credit. That variable checks federal adjusted_gross_income <= 50_000.
    • (1)(a)(iii) is measured by earned_income: employment income plus net self-employment income. That's net profit, not Schedule C gross income, so a Schedule C loss can offset wages. Main has the same gap (follow-up F1).
  • Wisconsin's earned income start. min_age, min_elderly_age, max_income and earnings_sources all start on 2021-01-01, so the model never reaches a tax year before 2017. No follow-up.
  • Wisconsin's claimant choice. tax_unit.any(filer & qualifies & (~claimed | elderly)) asks whether either spouse alone meets every condition, which matches § 71.52(1). I checked both of the brief's couples:
    • One spouse meets every condition: eligible whichever spouse is the head. The PR's paired "claimed spouse is the head/spouse" cases cover this, and so does the property test.
    • No one spouse meets them all: ineligible. W10 below, where the only earner is claimed and the other spouse has no earnings, is false at the head and true at main. W8 is false at the head and true at main.
  • The overlap with Leave dependents' income out of Wisconsin homestead credit household income #10191 is covered below.

Overlap with #10191 (head a953caf419)

git merge-tree --write-tree --name-only pr-10104 pr-10191 conflicts in exactly the two files the hand-off names, wi_homestead_eligible.py and wi_homestead_eligible.yaml.

This PR's formula is a superset of #10191's. #10191's eight new eligibility cases (Cases 8 to 15, the 208 lines it appends after line 95 of the YAML) all pass on this PR's head: 8 passed.

I ran a trial merge on a scratch copy, keeping this PR's side of both files and appending #10191's Cases 8 to 15:

Simpler order: this PR first. The build on #10191 may not edit either eligibility file. If this PR lands first and carries #10191's Cases 8 to 15 and its two statute URLs, #10191's merge of main resolves by taking main's side of both files whole. That needs no authored edit to either file, so it fits that build's orders.

If #10191 lands first instead, this PR's merge must keep:

Other open work. #10044 (draft) auto-merges in ut_at_home_parent_credit_potential.py. Its UT cases each have only one spouse at $3,000 or less, so the per-return change doesn't move them. #10595 is the only other open PR the overlap search returns. It touches only Vermont 2017 taxable income files, none of this PR's.

Checked and sound

  • Each defect reproduces at main (abc67b98) and is fixed at the head. I ran witness YAMLs at both heads:
    • VT single, $10,000 wages, $1,000 interest: vt_agi is 8,000 at main and 9,000 at the head.
    • UT, both spouses earning $3,000 or less, one newborn: ut_at_home_parent_credit_potential is 200 at main and 100 at the head.
    • WI W7, a 40-year-old with no earnings and a dependent 16-year-old earning $5,000: eligible at main, ineligible at the head.
    • WI W8, an 18-year-old with no earnings and a 17-year-old earning spouse: eligible at main, ineligible at the head.
    • WI W10, a claimed 30-year-old only earner with a spouse who has no earnings: eligible at main, ineligible at the head.
    • A self-employed WI claimant with no wages is eligible at both heads. earnings_sources includes self_employment_income, which matches § 71.52(1g).
  • Hand values, computed before reading the code that produces them:
    • VT $4,000 interest at $50,000 wages: federal cap $2,500, so $4,000 - $2,500 = $1,500.
    • VT joint at $195,000: $2,500 × (1 - 25,000/30,000) = $416.67 federal, AGI $194,583 ≤ $200,000, so $3,000 - $416.67 = $2,583.33.
    • VT single at $125,000 and joint at $205,000: over the limit, so $0. 2021: $0. The § 5811 history note dates the subtraction to Jan. 1, 2022.
    • UT: one at-home parent with two children gives $200, and two at-home parents give $200 as well. The rename of the old "2 eligible parents with 1 child" case to "1 eligible parent with 2 children" matches its inputs.
    • WI: each case's expected value follows § 71.53(2)(c) and (d) and § 71.54(1)(gm) on the hub's reading.
  • The changed test files pass at the head, one per process: VT 7, UT 5, WI 15, test_vt_ut_wi_statutory_invariants.py 3, test_source_conflicts.py 103.
  • Cost of the new Python file. Locally, alone: 75 s wall time including import and model build, of which about 12 s is test calls (WI 9.6 s, VT 2.1 s, UT 0.6 s), and 1.5 GB peak RSS for its process. The file runs in the core group of the rest-Python shards, which the Makefile records at 13.3 GB / 11:51 in CI run 38141803955. I didn't measure the file's marginal memory inside that group. The PR's CI run passed every Rest Python shard.
  • Properties. Each property asserts what the body claims. They're weak: max_examples=3 with derandomize=True makes each one a fixed sample, and the VT property hard-codes the limits (see P2-2).
  • Readers.
    • vt_student_loan_interest_subtraction is read only through gov.states.vt.tax.income.agi.subtractions, then vt_subtractions and vt_agi.
    • ut_at_home_parent_credit_potential is read only by ut_at_home_parent_credit, which gov.states.ut.tax.income.credits.non_refundable lists.
    • wi_homestead_eligible is read only as defined_for of wi_homestead_credit, which feeds gov.states.wi.tax.income.credits.refundable, retirement_income_exclusion_refundable and gov.states.household.state_property_tax_credits.
    • No reader gets worse. The old list item, the raw student_loan_interest, has no other Vermont reader.
  • Baseline. The diff can move baseline outputs:
    • Vermont AGI and tax rise for every Vermont filer with student loan interest that was deducted federally, for filers over the limits, and for every 2021 filer with interest.
    • Utah credits halve for couples where both spouses earn $3,000 or less.
    • Wisconsin eligibility falls for units where only a dependent or an under-18 spouse earns. claimed_as_dependent_on_another_return is an input that defaults to false, so the dependency bar moves little unless the microdata sets it.
    • The hub's population check should report the Vermont revenue change and the Utah and Wisconsin counts.
  • The CTC removal. git diff abc67b98 pr-10104 -- '*vt_ctc*' is empty, and Phase out Vermont's child tax credit per child #10644 carries the CTC fix with its own hand-off.
  • Reviews. There are no reviews or inline comments to answer.
  • Scope. Nothing under .github/ or policyengine_us/tests/policy/baseline/partners/, and a changelog fragment exists at changelog.d/vt-ut-wi-statutory-fixes.fixed.md.
  • What the author's brief asked for that the PR doesn't do: cite each source with a page link (the WI statute has none, P2-4), and record the source conflict (P2-1). Everything else it asked for is done.

Proposed follow-ups (main shares these)

  • F1. Utah's at-home parent test uses net earnings, not W-2 wages plus Schedule C gross income. ut_at_home_parent_credit_earned_income_eligible_person compares earned_income (wages plus net self-employment income) with $3,000, while (1)(a)(iii) counts W-2 wages plus Schedule C gross income. Witness, 2025: a head with $5,000 of wages and a $3,000 Schedule C loss, plus a newborn. The person comes out eligible ([1. 0.]) at both main and this head; the statute's test gives $5,000 > $3,000. The model also doesn't test (1)(a)(i) and (ii), full-time care of and a dependency claim for the parent's own child.
  • F2. Wisconsin earned income includes partnership_s_corp_income. § 71.52(1g) counts wages and "net earnings from self-employment". S corporation income isn't self-employment earnings, so an S-corp shareholder with no wages can qualify. Main has the same list. Check the list against the statute's definition.

What I couldn't check

  • None of the primary sources: every host was blocked (see the top). I relied on the hub's reading for all of them.
  • Act 138's Sec. 13 and its page numbers.
  • IN-111 line 1, the Schedule H page numbers, and the 71/viii path of the WI statute URLs.
  • IRC § 221's text: the code comment at lines 27-29 cites § 221(c) for the dependent bar on the federal deduction.
  • Population impact: I had no access to the microdata.
  • This file's marginal memory inside the core CI group.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

32 V.S.A. § 5811(29)(B) sets limits only for single, head of household,
married filing separately and joint filers, and the chapter names surviving
spouses wherever it includes them, so a surviving spouse is not a qualified
resident taxpayer. IN-112 line 16c's "All other filers" $120,000 has no gap
to fill. The limit for that status becomes -inf; the conflict is recorded as
the candidate us-vt:student-loan-interest-surviving-spouse-limit, and the
parameter's comment is counted in KNOWN_UNMARKED until the marker lands.

Also cite the effective date as the § 5811 history note gives it (2021,
No. 138 (Adj. Sess.), §§ 5, 6, eff. Jan. 1, 2022) and state (29)(B)'s
filing statuses exactly in the variable's comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JwDtRRMHxwVAQMx6f
The age, earned income and earned income definition tests come from Wis.
Stat. 71.53(2)(c), 71.54(1)(gm) and 71.52(1g); the reference tuple and the
comment cited only the LFB paper and the Schedule H instructions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JwDtRRMHxwVAQMx6f
Adds the cases no test covered: AGI exactly at the $120,000 and $200,000
limits ("equal to or less than"), two surviving spouses who get no
subtraction under 32 V.S.A. § 5811(29)(B) (the form would give $3,000 and
$1,500), a dependent's interest, and a Wisconsin couple where neither
spouse alone is both 18 or older and an earner.

Also carries #10191's eligibility Cases 8 to 15 unchanged, so that PR's
merge of main can take main's side of the eligibility file whole, and cites
the effective date as the § 5811 history note gives it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JwDtRRMHxwVAQMx6f
The test hard-coded the limit as $200,000 joint and $120,000 otherwise and
never drew a surviving spouse, so a surviving-spouse limit of $120,000 or
$200,000 passed it. It now draws single, joint and surviving spouse returns
and asserts a zero subtraction for every surviving spouse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JwDtRRMHxwVAQMx6f
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: a35d9295a34a95ddd6b828eea6eaee8abb80c46d

This round answers the r1 review at 99ae1e525a, with the hub's ruling on the surviving spouse. I built on 99ae1e525a; no other commit had landed on the branch. Commits: 53d2ab7926 (surviving spouse, register item, comments), 6c61d40880 (WI statutes), 37fd90982d (cases, #10191's cases, effective-date wording), 8c307c64db (property test), a35d9295a3 (changelog fragment).

Ownership check. The PR's last commit (20:15Z) was inside the 6-hour window, but it is the previous round's own merge commit. That round posted its hand-off at 21:19Z, and the review was made at that head, so I treated no writer as active and picked up at 23:47Z. Judgment call.

Items

  1. Surviving spouse gets no subtraction. income_limit.yaml:22-26: SURVIVING_SPOUSE is -.inf from 2022. The loader accepts it (the MD contrib dependent exemption scales already use -.inf), so I didn't need a formula bar. The comment above that line names the conflict; the file is in KNOWN_UNMARKED with 1 (test_source_conflicts.py, in sorted order, before the WI homestead disqualified_loss_sources.yaml line). Readers: git grep finds one, vt_student_loan_interest_subtraction.py:42 (agi <= p.income_limit[filing_status]), which is false for any finite AGI, so every surviving spouse gets 0. The property test hard-codes its own limits, and now uses -np.inf for a surviving spouse. Nothing else reads gov.states.vt.tax.income.agi.student_loan_interest.

  2. Register item. source_conflicts/candidates.yaml, right after us-vt:snap-child-support-deduction (ids stay sorted): the review's item with follows: statute, state: ruled, the third source (32 V.S.A. § 5822(a)), and the brief's example and note word for word. test_source_conflicts.py: 103 passed. The ratchet counts 1 for income_limit.yaml and 0 for both changed .py files.

  3. Cases. vt_student_loan_interest_subtraction.yaml gains the review's four VT cases, minus its $150,000 surviving-spouse case, plus the two surviving-spouse cases. Hand values, worked before running and recorded in the comments:

    • Single at exactly $120,000 (2025): the federal deduction is 0 above $100,000 MAGI; AGI is $120,000 ≤ $120,000, so VT = $3,000 − $0 = $3,000.
    • Joint at exactly $200,000 (2022): the 2022 joint phase-out ends at $175,000, so federal is [0, 0]; AGI is $200,000 ≤ $200,000, so VT = $3,000.
    • Surviving spouse, $110,000 wages, $3,000 interest (2025): MAGI $110,000 > $100,000 (non-joint $85,000 start + $15,000 divisor), so federal is [0, 0] and AGI is $110,000. VT is 0 under (29)(B); the form would give $3,000.
    • Surviving spouse, $50,000 wages, $4,000 interest (2025): federal min($4,000, $2,500) = [2,500, 0] with no phase-out, AGI is $47,500, VT is 0; the form would give $1,500.
    • Dependent's $1,000 of interest: federal [0, 0] (the dependent is barred; the head paid none), VT 0.
    • WI, an 18-year-old with no earnings and a 17-year-old earner: neither spouse is both 18+ and an earner/62+/disabled, so false.

    The property test (test_vt_ut_wi_statutory_invariants.py:77-98, 102-129) now draws SINGLE, JOINT and SURVIVING_SPOUSE returns (the surviving spouse with a 10-year-old), sets the filing status through a new filing_statuses argument to _situation, and asserts not sub[surviving].any(). Runtime, measured on this VM, one process each: the VT property's call time went from 2.5 s to 3.1 s; the whole file took 77 s before and 84 s after in wall time (import and model build included), with peak RSS 1,495 MB before and 1,497 MB after. These are local measurements, not Linux CI.

  4. Mutants. Each ran in its own process: the state's YAML file, then pytest ... test_vt_ut_wi_statutory_invariants.py -k <state>. The code was restored with git checkout after each, and git status was clean after each.

Mutant Changed YAML file Property test Killed by
R1 VT: subtract the whole interest 3 of 12 fail fails YAML and property
R2 VT: no income limit 4 of 12 fail fails YAML and property
R3 UT: one credit per qualifying spouse 2 of 5 fail fails YAML and property
R4 WI: no dependency bar 4 of 24 fail passes YAML
R5 WI: any tax unit member's earnings 6 of 24 fail passes YAML
R6 WI: earnings summed across both spouses 5 of 24 fail passes YAML
R7 VT: < instead of <= 2 of 12 fail (the exact $120,000 and $200,000 cases) passes YAML
R8 VT: SURVIVING_SPOUSE $200,000 2 of 12 fail (both surviving-spouse cases: 3,000 and 1,500 vs 0) fails YAML and property
R9 WI: 18-or-older tested across the couple 2 of 24 fail passes YAML
R10 VT: dependents' interest counted 1 of 12 fails (1,000 vs 0) passes YAML
O1 VT: SURVIVING_SPOUSE $120,000 (the form's reading) 2 of 12 fail (3,000 and 1,500 vs 0) fails YAML and property
O2 VT: no 2022 effective-date gate 1 of 12 fails (1,500 vs 0, 2021) passes YAML

None survives.

The brief's own "$200,000" mutant is the same as the review's R8, so I ran it once and added O2 (no 2022 effective-date gate) as a 12th.
5. Code comment. vt_student_loan_interest_subtraction.py:35-39 is the brief's text, as a Python comment. The guard counts 0 for this file.
6. Wisconsin references. wi_homestead_eligible.py:14-16 adds the three statute URLs to the tuple; the comment at :26 now begins "Wis. Stat. 71.53(2)(c), 71.54(1)(gm):".
7. #10191 overlap. I appended #10191's lines 98-305 of wi_homestead_eligible.yaml (its note and Cases 8 to 15) to the end of this PR's file, byte for byte (cmp against git show a953caf419:... is identical). #10191's head has since moved to 5c3716b10b, but git diff a953caf419 5c3716b10b on that file is empty. The file passes 24 of 24 (15 old, the review's new case, and #10191's 8). The body says this PR carries them. I didn't touch #10191.
8. Body. I applied each P2-4 replacement, using the hub's VT "Rule now" text. The effective-date fix also went into the two parameter reference titles (income_limit.yaml:12, in_effect.yaml:10), the variable's reference comment and its comment at :43-44. I made the same wording change in three places the review didn't list: subtractions.yaml:9, and two VT YAML comments (lines 130 and 153-154). I replaced the old CI line with this head's run. impact: and axiom: are unchanged.
9. Runs (one file per process):

  • vt_student_loan_interest_subtraction.yaml: 12 passed.
  • ut_at_home_parent_credit.yaml: 5 passed.
  • wi_homestead_eligible.yaml: 24 passed.
  • test_vt_ut_wi_statutory_invariants.py: 3 passed.
  • test_source_conflicts.py: 103 passed.
  • ruff format --check and ruff check on the four changed Python files: ruff format --check reports all 4 already formatted, and ruff check reports "All checks passed!" for each (run on the committed contents).

CI

CI: the Pull request run at a35d9295a3 (41745174793) was still in progress at 01:17Z, 64 minutes after the push. 35 of 36 jobs had concluded success, and one, Contrib tests / Full Suite - Contrib (other-shard-3), started at 00:44Z, was still running. None had failed or been cancelled. Per the brief, I'm handing off with the run URL rather than waiting; the hub should read its final conclusion.

Judgment calls

  • Quotes. This VM's proxy refused every source host (legislature.vermont.gov, docs.legis.wisconsin.gov, revenue.wi.gov, taxsim.nber.org, le.utah.gov, web.archive.org; 403 on CONNECT). So the body quotes only what the brief quoted from the hub's 2026-11-03 reading. Where the review's replacement text quoted Utah § 59-10-1005(2), Wis. Stat. §§ 71.53(2)(c)/(d) and 71.54(1)(gm), § 5811(21)(B)(vi), IN-112 line 16b or Schedule H p. 10, I paraphrased with the citation instead, since I couldn't fetch them. The hub may want to restore the review's quotes after checking them.
  • Act 138 reference. The #page 4-5,15 page list came off both reference titles: I couldn't open the act to check §§ 5, 6 or Sec. 13. The href stays at #page=4, as before.
  • WI statute links. The body links §§ 71.53(2)(c), 71.54 and 71.52(1g), the three URLs the hub opened. §§ 71.53(2)(d) and 71.52(1) are cited without links rather than with guessed URLs.
  • Order in the WI file. The review's new case sits before Leave dependents' income out of Wisconsin homestead credit household income #10191's block, so the block stays a contiguous, unchanged tail.
  • No partner tests and nothing under .github/ were touched.

Proposed follow-ups

  • Mark us-vt:student-loan-interest-surviving-spouse-limit once registered. After this merges and rulespec-us has the entry: add # Source conflict us-vt:student-loan-interest-surviving-spouse-limit at income_limit.yaml's SURVIVING_SPOUSE line, drop its KNOWN_UNMARKED line, delete the candidate and refresh the pin.
  • The review's F1 (Utah at-home parent test uses net rather than gross Schedule C earnings) and F2 (Wisconsin earned income includes partnership_s_corp_income) still stand; main shares both.

Generated by Claude Code

@MaxGhenis

MaxGhenis commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Hub review in progress

Superseded by the final review: #10104 (comment)


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: APPROVE
a35d929
METHODOLOGY_CHOICE: yes

The head hasn't moved since the round 2 hand-off. This is a delta review of the five commits from 99ae1e525a (the round 1 review's head) to a35d9295a3: 53d2ab7926, 6c61d40880, 37fd90982d, 8c307c64db and a35d9295a3.

The round 2 changes are right on the hub's reading. A qualifying surviving spouse now gets no Vermont subtraction at any AGI, including 0 and negative AGI. The register item passes the guard. Every new case matches my hand values. The carried #10191 block is byte-identical to #10191's current head. CI is green: 36 of 36 jobs.

I'm approving because no finding changes a result. Two things should still land before or with the merge:

  • three Vermont cases, because no test pins the head of household or married filing separately limit, and my two mutants of them survive (P2-1);
  • body corrections: a stale CI line, and quotes the hub's reading supports (P2-2).

Sources. I couldn't reach any primary source from this VM. curl got a proxy 403 on CONNECT from legislature.vermont.gov, docs.legis.wisconsin.gov, le.utah.gov, taxsim.nber.org, revenue.wi.gov, tax.vermont.gov, govinfo.gov, uscode.house.gov and ecfr.gov. A second fetch route was refused for legislature.vermont.gov as outside the session's allowed domains. Every legal statement below rests on the hub's 2026-11-03 reading, quoted in the review brief. I quote nothing from memory.

Findings

P2-1. No test pins Vermont's head of household or married filing separately limit; two mutants survive

policyengine_us/parameters/gov/states/vt/tax/income/agi/student_loan_interest/income_limit.yaml:18-21 sets HEAD_OF_HOUSEHOLD and SEPARATE to $120,000. No YAML case pins either one. The one case that defaults to head of household, the dependent's-interest case, expects 0 at any limit. The property test draws only SINGLE, JOINT and SURVIVING_SPOUSE.

Mutant (mine) VT YAML (12 cases) Property Result
M1: HEAD_OF_HOUSEHOLD -.inf 12 passed passes survives
M2: SEPARATE -.inf 12 passed passes survives

Failure scenarios:

  • M1: a 2025 head of household with $119,000 of wages and $3,000 of interest would get 0 against § 5811(29)(B)(i)'s $120,000 limit.
  • M2: the same holds for a married filing separately filer.

Both scenarios give $3,000 at the head; I ran them as witnesses.

Add these three cases at the end of policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/subtractions/vt_student_loan_interest_subtraction.yaml:

- name: 2025 head of household at exactly $120,000 of adjusted gross income keeps the Vermont student loan interest subtraction
  period: 2025
  input:
    people:
      head:
        age: 40
        employment_income: 120_000
        student_loan_interest: 3_000
      child:
        age: 10
    tax_units:
      tax_unit:
        members: [head, child]
        filing_status: HEAD_OF_HOUSEHOLD
    households:
      household:
        members: [head, child]
        state_code: VT
  output:
    # 5811(29)(B)(i): $120,000 if head of household. The federal deduction is
    # fully phased out above $100,000 of MAGI, so 16a $3,000 - 16b $0 = $3,000.
    adjusted_gross_income: 120_000
    student_loan_interest_ald: [0, 0]
    vt_student_loan_interest_subtraction: 3_000

- name: 2025 head of household above $120,000 of adjusted gross income gets no Vermont student loan interest subtraction
  period: 2025
  input:
    people:
      head:
        age: 40
        employment_income: 125_000
        student_loan_interest: 3_000
      child:
        age: 10
    tax_units:
      tax_unit:
        members: [head, child]
        filing_status: HEAD_OF_HOUSEHOLD
    households:
      household:
        members: [head, child]
        state_code: VT
  output:
    # 5811(29)(B)(i): $125,000 is above the $120,000 head of household limit.
    vt_student_loan_interest_subtraction: 0

- name: 2025 married filing separately at exactly $120,000 of adjusted gross income keeps the Vermont student loan interest subtraction
  period: 2025
  input:
    people:
      head:
        age: 40
        employment_income: 120_000
        student_loan_interest: 3_000
    tax_units:
      tax_unit:
        members: [head]
        filing_status: SEPARATE
    households:
      household:
        members: [head]
        state_code: VT
  output:
    # 5811(29)(B)(i): $120,000 if married filing separately. No federal
    # deduction, so 16a $3,000 - 16b $0 = $3,000.
    adjusted_gross_income: 120_000
    student_loan_interest_ald: 0
    vt_student_loan_interest_subtraction: 3_000

I ran the three cases alone, once at the head and once under each mutant, one process per run, on a scratch worktree:

Run Result
Head 3 passed
M1 1 failed
M2 1 failed
M4: HEAD_OF_HOUSEHOLD $200,000 1 failed

Appended to the VT file, the cases give 15 passed at the head. The command was uv run python -m policyengine_core.scripts.policyengine_command test <file> -c policyengine_us.

P2-2. The PR body: a stale CI line, and quotes it can carry

The impact: and axiom: lines are present, and I left them alone. Each item below gives the text to replace and its replacement.

  • CI line (Tests section). "the Pull request run at a35d9295a3 (41745174793) was still in progress at 01:17Z ... one ... was still running." The run has finished. Replace with: "CI: the Pull request run at a35d9295a3 (41745174793) concluded success: all 36 jobs succeeded, none skipped or cancelled." The hand-off comment has the same stale line.
  • Overlap section. "Apply the RI, SC, UT, VT, WV and WI rules for a filer who can be claimed as a dependent #10044 (draft)" is stale: Apply the RI, SC, UT, VT, WV and WI rules for a filer who can be claimed as a dependent #10044 is open and no longer a draft. Replace "(draft)" with nothing. I reran git merge-tree --write-tree --name-only pr-10104 pr-10044 against its current head ce5880d884, and it's still clean.
  • Quotes the body can carry. The fix session paraphrased the sources because it couldn't reach them. The hub's reading quotes each text below, so the body can quote them:
    • VT, "Vermont's federal offset". Replace "§ 5811(21)(B)(vi) subtracts the interest a qualified resident taxpayer paid during the taxable year on a qualified education loan" with: "§ 5811(21)(B)(vi) subtracts "the amount of interest paid by a qualified resident taxpayer during the taxable year on a qualified education loan for the costs of attendance at an eligible educational institution"". Replace "(16b takes the student loan interest already deducted on federal Schedule 1)" with: "(16b: "Enter any student loan interest already deducted on federal Form 1040, Schedule 1, Line 21")".
    • VT, Readings item 3. After "gives limits only for single, head of household, married filing separately and joint filers", add: "("$120,000.00 if the individual's filing status is single, head of household, or married filing separately; or (ii) $200,000.00 if the individual's filing status is married filing jointly")". After "§ 5822(a)(1) and (3)", add: "(for example, (21)(C)(ii)(I): "unmarried (other than surviving spouses or heads of households)"; § 5822(a)(1): "Married individuals filing joint returns and surviving spouses")".
    • Utah row. Replace the source cell's paraphrase with: "§ 59-10-1005(2): "$100 for each qualifying child if: (a) the claimant or another claimant filing a joint individual income tax return with the claimant is an at-home parent"".
    • WI, Readings item 1. Replace "This PR follows page 10, which says the spouse who meets the definition must be the claimant." with: "This PR follows page 10: "The spouse that meets this definition must be the claimant."" Replace "§ 71.52(1) lets a couple choose which of them is the claimant" with: "§ 71.52(1): "When 2 individuals of a household are able to meet the qualifications for a claimant, they may determine between them as to who the claimant is."".
    • WI, Readings item 2. Replace "§ 71.53(2)(d) bars a claimant who was claimed as a dependent for federal income tax purposes by another person" with: "§ 71.53(2)(d) bars a claimant who "was claimed as a dependent for federal income tax purposes by another person during the year to which the claim relates", unless 62 or older".
  • Invariants item 1 says the property test checks that the subtraction is zero in 2021. The property's fixed 2021 example draws no interest at all (P3-1), so 2021 is pinned only by the two YAML cases. Replace "The subtraction is zero above the limit, in 2021 and for every surviving spouse" with: "The subtraction is zero above the limit and for every surviving spouse (the YAML cases pin 2021)".

Every other statement in the body matches what I ran or read:

P3-1. The VT property test's 2021 branch tests nothing

policyengine_us/tests/core/test_vt_ut_wi_statutory_invariants.py:101-129 runs three fixed examples: max_examples=3 with derandomize=True. I recorded every drawn household. The 2021 example is Hypothesis's minimal one: eight SINGLE units with $0 of wages and $0 of interest. So assert not sub.any() for year < 2022 checks nothing. O2 (no 2022 effective-date gate) passes the property; only the YAML 2021 case kills it.

The surviving-spouse draw does reach the statute's assertion:

  • 8 of the 26 drawn units are surviving spouses, in the 2022 and 2026 examples.
  • Exactly one of them separates the statute from the form's $120,000: a 2026 unit with AGI −$1,979, $3,972 paid and $2,500 deducted federally.
  • One 2022 unit at $125,881 separates it from the $200,000 mutant.

That's enough to kill R8 and O1, but thin. No change is required. If the file changes again, drop 2021 from the property's sampled_from years, or draw interest from st.integers(1, 6_000); the YAML already pins 2021.

The property also repeats the parameter's -np.inf. With assert not sub[surviving].any() already stated, within = (agi <= np.where(statuses == "JOINT", 200_000, 120_000)) & ~surviving would assert the statute without copying the parameter's encoding.

P3-2. Follow-up, not this PR's defect: § 71.54(1)(gm)'s exemption, read literally

wi_homestead_eligible.py:26-29 now cites "Wis. Stat. 71.53(2)(c), 71.54(1)(gm)" for the rule that the claimant "must be 62 or older, disabled or have earned income". The hub's quote of (gm) bars a claimant with no earned income "with regard to a claimant who is not disabled or who is under the age of 62". Read literally, that bars any claimant who is not disabled, or who is under 62. So it exempts only a claimant who is both disabled and 62 or older. The code, like main, exempts a claimant who is either one. Schedule H Step 1a supports the code.

The hub's quote has an ellipsis, and I couldn't open the statute. Before the comment's citation stands, the hub should read the full text of (gm). If the literal reading holds, it's a statute-versus-instructions conflict to record as a candidate. Main has the same formula.

The checks the brief asked for

  • The -.inf limit.

    • The loader accepts it. The changed files pass at the head, and 176 parameter files already use -.inf, including baseline Medicaid and CHIP income limits.

    • git grep -n "agi.student_loan_interest" finds one reader, vt_student_loan_interest_subtraction.py:22. Its test agi <= p.income_limit[filing_status] (line 42) is false for every finite AGI.

    • Witnesses at the head (2025 surviving spouse):

      Witness Head Round 1 head 99ae1e525a Main's vt_subtractions
      AGI exactly 0 ($2,500 wages, $4,000 interest, $2,500 federal) 0 $1,500 $4,000
      Negative AGI ($3,000 wages, −$10,000 self-employment, $4,000 interest) 0 $1,500 not run
      $110,000 wages, $3,000 interest 0 $3,000 $3,000

      A single filer at AGI 0, run as a control, gives $1,500 at the head.

    • Only an AGI of exactly −∞ would pass the test, and no input the model takes produces one.

  • The register item (source_conflicts/candidates.yaml:9967-10000).

    • It has every field test_candidate_fields requires.
    • follows: statute is in FOLLOWS and state: ruled is in STATES. The file holds 60 other ruled items with follows: statute.
    • It has three sources: two statutes and a form, all https.
    • Its example matches the $110,000 case.
    • Its ids stay sorted, and it adds no marker, which follows CONTRIBUTING.md step 1.
    • KNOWN_UNMARKED gains income_limit.yaml: 1, in sorted order. The guard counts the parameter's comment ("The 2025 IN-112 line 16c instructions say ...") and nothing in either changed .py file.
    • test_source_conflicts.py: 103 passed.
  • The cases, by hand before running:

    • Single at exactly $120,000 (2025): MAGI is above $100,000, so federal is $0; $120,000 ≤ $120,000, so $3,000.
    • Joint at exactly $200,000 (2022): the 2022 joint phase-out ends at $175,000, so federal is $0, and the subtraction is $3,000.
    • Surviving spouse, $110,000 and $3,000: the non-joint phase-out ($85,000 to $100,000) gives federal $0 and AGI $110,000. The statute gives 0; the form would give $3,000.
    • Surviving spouse, $50,000 and $4,000: federal min($4,000, $2,500) = $2,500, so AGI is $47,500. The statute gives 0; the form would give $1,500.
    • Dependent's $1,000 of interest: the head paid none, so 0.
    • WI, an 18-year-old with no earnings and a 17-year-old earning $5,000: neither spouse is both 18 or older and either 62 or older, disabled or an earner, so false.

    Each matches the file.

  • Runs at the head, one per process:

    File Result
    vt_student_loan_interest_subtraction.yaml 12 passed
    ut_at_home_parent_credit.yaml 5 passed
    wi_homestead_eligible.yaml 24 passed
    test_vt_ut_wi_statutory_invariants.py 3 passed in 28 s (89 s wall time with import)
    test_source_conflicts.py 103 passed

    ruff format --check reports the 5 changed Python files already formatted, and ruff check passes. hypothesis comes with the dev extra (uv sync --extra dev).

  • The property test. The new filing_statuses argument sets each tax unit's filing_status, and the draw reaches surviving spouses (P3-1). The assertion assert not sub[surviving].any() is the statute's rule. Under O1 the property fails on exactly that line: Hypothesis shrinks to a 2022 example with a $1 subtraction.

  • Mutants. Each ran in its own process on a scratch worktree. I removed .hypothesis before each run, so no saved example carried over. After each, I ran git checkout -- ., and git status was clean at the end. Per mutant I ran the state's YAML file, then the property file.

    Mutant YAML Property Killed by
    R7 VT < 2 of 12 fail ($120,000 and $200,000 boundary cases) passes YAML
    R8 VT surviving spouse $200,000 2 of 12 fail (3,000 and 1,500 vs 0) fails YAML and property
    O1 VT surviving spouse $120,000 2 of 12 fail (3,000 and 1,500 vs 0) fails YAML and property
    O2 VT no 2022 gate 1 of 12 fails (1,500 vs 0, 2021) passes YAML
    R4 WI no dependency bar 4 of 24 fail passes YAML
    R9 WI 18-or-older across the couple 2 of 24 fail passes YAML
    M1 VT head of household -.inf (mine) 12 pass passes survives: P2-1
    M2 VT separate -.inf (mine) 12 pass passes survives: P2-1
    M3 WI age-62 dependency exception across the couple (mine) 24 pass passes equivalent: a spouse 62 or older already qualifies alone, so no result can change

    The six reruns match the hand-off's table.

  • Overlap with Leave dependents' income out of Wisconsin homestead credit household income #10191. Its head is now dbe35eb8bc, past the 5c3716b10b the brief names. The two new commits touch only income/sources.yaml and wi_homestead_income.yaml.

  • Other open work.

  • CI. Pull request run 41745174793 at a35d9295a3 concluded success at 01:18Z. All 36 jobs succeeded, and none were skipped or cancelled.

  • Reviews. The PR has no reviews or inline comments.

  • Scope. The PR changes 13 files, none under .github/ or policyengine_us/tests/policy/baseline/partners/. The changelog fragment remains at the top level of changelog.d/.

Checked and sound

  • Each round 2 item from the brief is done: the -.inf limit (1), the register item with all three sources and the hub's note (2), the five cases (3), twelve mutants with none surviving (4), the code comment (5), the WI references and comment (6), Leave dependents' income out of Wisconsin homestead credit household income #10191's cases (7) and the body edits (8). The parameter's description now says "no surviving spouse qualifies". The effective-date wording changed in both reference titles, subtractions.yaml:9, the variable's comment and two YAML comments.
  • What the brief asked for that the PR doesn't fully do. Item 8 asked for each of the round 1 review's body replacements. The fix session couldn't fetch the sources, so it paraphrased the review's quotes (P2-2 gives them back). Everything else is done.
  • Readers.
    • income_limit.yaml and in_effect.yaml are read only by vt_student_loan_interest_subtraction.
    • That variable is read through gov.states.vt.tax.income.agi.subtractions, then vt_subtractions and vt_agi.
    • The WI change since round 1 is references and comments only.
    • No reader gets worse.
  • Baseline. Compared with round 1, the only result this round moves is Vermont surviving spouses with student loan interest: their subtraction falls to 0, at round 1 the remaining interest up to the $120,000 limit. Compared with main, those filers had subtracted all of their interest with no income limit. The hub's population check should count Vermont surviving-spouse returns with student loan interest. I expect the count to be small, but I couldn't measure it.

What I couldn't check

  • Every primary source: the hosts were refused (see above). In particular: the full text of § 71.54(1)(gm) (P3-2); whether § 5822(a) and § 5811(21)(C) are the only places the chapter names surviving spouses; Act 138's page for §§ 5 and 6 (the link stays at #page=4); IN-111 line 1; and IRC § 221's phase-out amounts. For the federal amounts I relied on the model's own outputs.
  • Population impact: I have no access to the microdata.
  • The property file's marginal memory inside its CI group. Locally, the file takes 89 s wall time with import and model build.

Follow-ups (main shares these)

  • The round 1 review's F1 (Utah's at-home parent test uses net rather than gross Schedule C earnings) and F2 (Wisconsin earned income includes partnership_s_corp_income) still stand.
  • P3-2: read § 71.54(1)(gm) in full, and record a candidate if its literal exemption (disabled and 62 or older) conflicts with Schedule H Step 1a.
  • After merge: the marker for us-vt:student-loan-interest-surviving-spouse-limit (CONTRIBUTING.md steps 2 to 4), as the hand-off proposes.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

…mits

No test pinned the $120,000 HEAD_OF_HOUSEHOLD or SEPARATE limit, so the
round 2 review's mutants setting either to -inf, or head of household to
$200,000, passed. Three 2025 cases at and above the limit now kill each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FqQp8Bf92tRiedzTm
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 6f203e8ba3a28d6817b81f2996182ac8a89b3d3b

One commit on top of a35d9295a3: 6f203e8ba3 adds three YAML cases only. No one else pushed to the branch while I worked. git merge-tree with origin/main (0315452583) is clean, so I merged nothing. I left P3-1 and P3-2 for the hub, as the brief said.

What the round 2 review asked for, and what I did

  1. P2-1: the head of household and separate limits.

    • I appended three cases to policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/agi/subtractions/vt_student_loan_interest_subtraction.yaml, using the brief's names and comments and the review's inputs and outputs:

      • HoH at $120,000, :290-314;
      • HoH above $120,000, :316-336;
      • married filing separately at $120,000, :338-360.
    • The file passes at this head: 15 passed.

    • Each mutant ran in its own process. I edited income_limit.yaml in place, cleared .hypothesis, ran the VT YAML and then the property file, and restored with git checkout after each.

      Mutant VT YAML Property Failing case
      M1 HEAD_OF_HOUSEHOLD -.inf 1 of 15 fail 3 passed HoH at $120,000: 0 vs 3,000
      M2 SEPARATE -.inf 1 of 15 fail 3 passed MFS at $120,000: 0 vs 3,000
      M4 HEAD_OF_HOUSEHOLD $200,000 1 of 15 fail 3 passed HoH at $125,000: 3,000 vs 0
    • I added all three rows to the body's mutant table. "None survives." still holds.

    • Hand check: AGI is $120,000 in each case. The non-joint federal phase-out ends at $100,000 of MAGI, and an MFS filer gets no federal deduction, so 16b is $0. $120,000 ≤ $120,000 gives the full $3,000, and $125,000 > $120,000 gives 0.

  2. P2-2: the body. I made each replacement in place with the brief's text: Vermont's federal offset, the Utah source cell, Readings items 1, 2 and 3's first two sentences, and Invariants item 1.

  3. Runs at 6f203e8ba3, one process each:

    File Result
    vt_student_loan_interest_subtraction.yaml 15 passed
    ut_at_home_parent_credit.yaml 5 passed
    wi_homestead_eligible.yaml 24 passed
    test_vt_ut_wi_statutory_invariants.py 3 passed, .hypothesis cleared first
    test_source_conflicts.py 103 passed

    No Python file changed, so ruff had nothing to run on.

CI

Pull request run 41947821257 at 6f203e8ba3 hadn't finished 60 minutes after the 01:20Z push. At 02:21Z, 34 of 36 jobs had concluded success. Contrib tests / Full Suite - Contrib (other-shard-1) and (other-shard-3), both started at 01:51Z, were still running. None had failed or been cancelled.

The body's CI line still describes run 41745174793 at a35d9295a3 as in progress. Item 8 applies only once a run finishes, so I left it for the hub. When this run passes, it should read: "Pull request run 41947821257 passed all 36 jobs at 6f203e8ba3."

Judgment calls

  • I couldn't verify the new quotations. From this session, legislature.vermont.gov, docs.legis.wisconsin.gov, le.utah.gov, revenue.wi.gov and web.archive.org all fail to resolve or connect. The quotations in the offset paragraph, the Utah cell and Readings 1–3 are the brief's text, taken from the hub's reading. The hub should confirm them against the sources before landing.
  • I kept the old mutant rows' "of 12" counts as they were, instead of changing them to 15 without a rerun.

Proposed follow-up

  • Read Wis. Stat. § 71.54(1)(gm) in full for the homestead earned-income exemption. The round 2 review's P3-2 says the literal text may exempt only a claimant who is both disabled and 62 or older. The code and Schedule H Step 1a exempt either one. If the literal reading holds, record a source-conflict candidate.

Generated by Claude Code

@DTrim99

DTrim99 commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Fix Vermont student loan interest, Utah's at-home parent credit and Wisconsin's homestead claimant

Summary

I read each provision's law first and then compared the code. All three fixes match the law:

  • Vermont. 32 V.S.A. § 5811(21)(B)(vi) and (29), added by 2021, No. 138 (Adj. Sess.) § 5 and effective for taxable years beginning on and after Jan. 1, 2022 (§ 13(b)). The subtraction is the interest paid minus the federal deduction (IN-112 lines 16a–16c), with the $120,000 / $200,000 federal AGI limits.
  • Utah. Utah Code § 59-10-1005(2) pays "$100 for each qualifying child" when "the claimant or another claimant filing a joint individual income tax return with the claimant is an at-home parent", so the credit is paid once per child per return.
  • Wisconsin. §§ 71.52(1), (1g) and 71.53(2)(c), (d): one claimant must meet the age, earned income and dependency conditions personally.

The surviving-spouse reading for Vermont is well supported and recorded as a candidate.

Four things block approval:

  • Critical. When main was merged in, vt_other_subtractions was dropped from Vermont's 2021 subtraction list. Vermont 2021 returns with that input now lose the subtraction.
  • Should address.
    • Two new pieces aren't pinned by any test: the Vermont list entry, and the Wisconsin filer guard. I confirmed both by mutation.
    • The Wisconsin age/disability/earned income reading follows Schedule H over the literal text of § 71.54(1)(gm). The PR doesn't record it, and the code comment cites (gm) for it.
    • The Act 138 references cite the wrong section and a page that doesn't show the values.

CI is green: 36 of 36 checks at 6f203e8ba3. The axiom line is valid.

Critical (must fix)

C1. Vermont 2021 no longer subtracts vt_other_subtractions, because the 2021 block was rebuilt during the merge with main.
policyengine_us/parameters/gov/states/vt/tax/income/agi/subtractions.yaml:3-8

  • What changed. Since Support state-specific other additions and subtractions for remaining 30 states (Phase 2) #10302 (merged 2026-10-21, before this branch's merge base), main's list has vt_other_subtractions from 2021-01-01. This PR's new 2021 block lists us_govt_interest, vt_medical_expense_deduction, vt_capital_gains_exclusion, vt_retirement_income_exemption and salt_refund_income. It omits vt_other_subtractions, which now appears only in the 2022 block at line 18. The diff hides this: the old - vt_other_subtractions line survives as unchanged context, but under the new 2022 key.

  • Witness at the head (one process, head tree):

    Case Expected (main) Head
    2021, VT, wages $50,000, vt_other_subtractions $1,300: vt_subtractions $1,300 $0
    2024 control, same inputs $1,300 $1,300

    vt_other_subtractions stands for Schedule IN-112 Part I subtractions that PolicyEngine doesn't compute separately. The 2021 IN-112 allows those subtractions (Part I, lines 7–14), so 2021 Vermont AGI is overstated for every return with that input. 2021 is a claimed year: the PR changes the 2021 list and adds 2021 cases.

  • Fix. Add - vt_other_subtractions to the end of the 2021 block. Pin it with a 2021 case, for example in vt_other_subtractions.yaml: period 2021, state_code: VT, vt_other_subtractions: 1_300 → vt_subtractions: 1_300. That file's two existing cases are both 2024, which is why CI missed this.

Should address

S1. No test pins the new vt_student_loan_interest_subtraction entry in Vermont's 2022 subtraction list.
subtractions.yaml:14

  • Evidence. In 2022 and later, every case asserts the subtraction variable directly. The one 2022+ case that checks vt_agi has a subtraction of $0. The property test reads only vt_student_loan_interest_subtraction, and no other Vermont test uses student_loan_interest.

  • Mutation. I deleted the line in the head tree. All 15 Vermont cases still passed. My witness then gave vt_subtractions $0 instead of $1,500.

  • Fix. Add these outputs to the second case (2025, wages $50,000, interest $4,000; lines 24–42):

    • vt_subtractions: 1_500
    • vt_agi: 46_000 ($50,000 − $2,500 federal = $47,500 federal AGI; minus $1,500 = $46,000)

    Both values pass at the head; I ran them.

S2. No test pins the Wisconsin filer guard. Case 8 was meant to pin it, but its dependent is 17.
policyengine_us/variables/gov/states/wi/tax/income/credits/homestead/wi_homestead_eligible.py:37

  • The guard. eligible_claimant = filer & qualifies & (~claimed | elderly). Without filer, an adult dependent in the tax unit could qualify the return. claimed_as_dependent_on_another_return is false for a dependent claimed on this same return, so the dependency bar doesn't stop them.
  • Why Case 8 doesn't pin it. Case 8 ("a dependent's wages do not meet the claimant's earned income test") uses a 17-year-old dependent. The 18-or-older test rejects her whatever filer does.
  • Mutation. With filer & deleted, the PR's tests still pass: all 24 wi_homestead_eligible.yaml cases, all 4 wi_homestead_credit.yaml cases, and the property test, which draws only head/spouse couples.
  • Witness (2025): a single parent aged 40 with no earnings, $1,000 of property tax, and a 19-year-old full-time student dependent earning $3,000.
    • At the head: wi_homestead_eligible is false.
    • Under the mutant: true.
  • The law. The student is "claimed as a dependent for federal income tax purposes by another person" and is under 62 (§ 71.53(2)(d)). Earned income is the claimant's own (§ 71.52(1g)). So the head's answer, false, is right.
  • Fix. Add the witness as a case, or raise Case 8's dependent to 19. The case's comment and expectation stay the same.

S3. The PR follows Schedule H over the literal text of § 71.54(1)(gm) for the 62-or-older / disabled / earned income routes. That reading isn't recorded, and the comment cites (gm) alone for it.
wi_homestead_eligible.py:26-29

The comment says "Wis. Stat. 71.53(2)(c), 71.54(1)(gm): the claimant must be 18 or older and must be 62 or older, disabled or have earned income." The statute (§ 71.54(1)) says:

  • (dm): "Except as provided in subds. 5. and 7., [pars. (em) and (gm)] ... no credit may be allowed under this paragraph [subsection] unless the claimant or the claimant's spouse is over the age of 61 at the close of the year to which the claim relates."
  • (em): "... no credit may be allowed under this paragraph [subsection] unless the claimant is disabled."
  • (gm): "... with regard to a claimant who is not disabled or who is under the age of 62 at the close of the year to which the claim relates, no credit may be allowed under this paragraph [subsection] if the claimant had no earned income ..."
  • The Legislative Reference Bureau notes under each paragraph say the cross-references are wrong and "Corrective legislation is pending."

How the readings differ:

  • Literal (gm). It bars a claimant 62 or older who isn't disabled and has no earned income. It also bars a disabled claimant under 62 with no earned income.
  • 2025 Schedule H, Step 1a (p. 4). It gives three alternative routes: "Are age 62 or older ...", "Are disabled ...", "Have earned income".
  • The code. It follows Schedule H (as main did), which is the reasonable whole-text reading. CONTRIBUTING.md#source-conflicts still requires recording it: "When a PR takes such a reading, or finds one already in the code, the conflict goes into the register". This PR rewrites the formula and attaches the rule to (gm).
  • Already identified. The PR's own rulespec-us#2031 states the conflict: "Read alone, (gm)'s 'or' would bar a claimant of 62 or older who isn't disabled and has no earned income ..."

Fix:

  • Register. Add a source_conflicts/candidates.yaml item, for example us-wi:homestead-age-disability-earned-income-routes.
    • Sources: § 71.54(1)(dm)–(gm) (statute) and the 2025 Schedule H instructions, Step 1a (instructions).
    • follows: the reading the code takes. That's agency, or statute if you rest it on the whole-text canon.
    • code: wi_homestead_eligible.py.
  • Comment. Cite (dm), (em) and (gm) together. Add a KNOWN_UNMARKED line if the reworded comment trips the ratchet.
  • Reference tuple. It links only 71/viii/53/2/c. Link § 71.53(2) so (d), the dependency bar, is covered.

S4. The Act 138 references name the wrong section and use a page that doesn't show the values.
in_effect.yaml:10-11, income_limit.yaml:12-13, and the variable's reference tuple at vt_student_loan_interest_subtraction.py:11-13

  • Wrong section. The titles cite "2021, No. 138 (Adj. Sess.), §§ 5, 6" at #page=4. In the Act, Sec. 5 amends § 5811 to add (21)(B)(vi) (PDF page 4) and (29) (PDF page 5). Sec. 6 amends (21)(B)(iv), the retirement income exclusion, which has nothing to do with student loans.
  • Pages that don't show the values. The $120,000 / $200,000 limits are on PDF page 5. The effective date ("shall take effect retroactively on January 1, 2022 and shall apply to taxable years beginning on and after January 1, 2022") is in Sec. 13(b) on PDF page 15. Page 4 shows neither.
  • Fix. Follow the CLAUDE.md multi-page rule:
    • Titles: "2021, No. 138 (Adj. Sess.), §§ 5, 13(b) #page 4-5,15" (quoted, href #page=4). In income_limit.yaml, the shorter "... § 5 #page 4-5" is enough.
    • Variable reference tuple: put # PDF pages 4-5, 15 above the href.

Suggestions

  1. Utah reference version (ut_at_home_parent_credit_potential.py:13). The new link is the version effective 5/6/2026. For 2022–2025, the governing text is the version effective 3/24/2022 (C59-10-S1005_2022032420220324.pdf#page=1). Subsection (2) reads the same in both, but cite the earlier version for the claimed years.
  2. Vermont sources.
    • tax.vermont.gov served Income-Booklet-2025.pdf to a plain request, byte-identical to the NBER copy. The official URL could replace the archive link, and the body's note that the site refuses downloads can go.
    • The federal offset also has a textual basis in § 5811(21)(B)'s lead-in, "(to the extent such income is included in federal adjusted gross income)". It's worth citing next to the no-double-deduction canon.
  3. Surviving-spouse candidate. The same Act, in Sec. 7 (PDF page 5), amends § 5830e(a)(1) to "single, married filing separately, head of household, or qualifying widow or widower surviving spouse". Two sections after (29)(B), it names surviving spouses where it includes them. That's strong support for the statute reading and worth adding as a source.
  4. Property test.
    • The 2021 Vermont draw is vacuous: Hypothesis's minimal example has no interest.
    • limit copies the parameter's -np.inf.
    • Cost is fine: about 1.7 s in Rest (Python shard 4). Per CLAUDE.md "Test design and CI cost", say in the body which CI group runs it and what it costs.
  5. Married filing separately above the limit. A married filing separately case above $120,000 would kill a SEPARATE: 200_000 mutant, which currently survives. This one is optional.
  6. Follow-ups that main shares, outside this PR.
    • Utah earned income. The at-home parent test uses earned_income. § 59-10-1005(1)(a)(iii) counts "total wages, tips, and other compensation listed on all of the parent's federal Forms W-2" plus "the gross income listed on the parent's federal Form 1040 Schedule C".
    • Wisconsin earned income. earnings_sources includes partnership_s_corp_income and gross self-employment income. § 71.52(1g) counts wages plus net earnings from self-employment after the § 164(f) deduction.
    • Wisconsin credit amount. The credit uses a continuous 80%. § 71.55(9) makes Table B determining, and Table B gives $804 for $1,000 of taxes (2025 Schedule H instructions, p. 26). The four new wi_homestead_credit: 800 expectations pin the approximation.
  7. PR body.
    • The earned income definition is section F Implement Section 8 Project-Based Rental Assistance #4 (instructions p. 7). Page 10 holds the note "The spouse that meets this definition must be the claimant."
    • "Wisconsin applies each test ... to the claimant" overlooks that (dm) reaches "the claimant or the claimant's spouse". It's result-equivalent here, because a spouse 62 or older always qualifies as the claimant.

Verified correct

  • Vermont law.
    • § 5811(21)(B)(vi) and (29) match the current published text and Act 138 Sec. 5.
    • The limits are inclusive ("equal to or less than"), and § 13(b) starts the subtraction in 2022.
    • The 2021 Schedule IN-112 has no student loan line. The 2022 instructions (lines 15a–15c) and 2025 instructions (lines 16a–16c, booklet PDF pages 23 and 33) subtract interest paid minus "Student loan interest already deducted on federal Form 1040, Schedule 1, Line 21".
    • The surviving-spouse reading (−∞ limit) has a candidate with all required fields, and KNOWN_UNMARKED has the matching line.
  • Vermont code.
    • Only head and spouse interest counts, and the dependent case pins that.
    • student_loan_interest_ald is head/spouse-only and capped per return, so the offset never goes negative.
    • in_effect is pinned by the 2021 $4,000 case, and every filing-status limit except married filing separately above the limit is pinned at and above the limit.
  • Utah. § 59-10-1005(2) (versions effective 3/24/2022 and 5/6/2026) pays per qualifying child once per return. has_at_home_parent = add(...) > 0 gives exactly that, and both two-parent cases pin it ($400→$200, $200→$100).
  • Wisconsin.
    • § 71.52(1) lets the couple choose the claimant, so tax_unit.any(...) over head and spouse is right.
    • § 71.53(2)(c) (18+) and § 71.53(2)(d), the dependency bar with its 62-or-older exception, are applied per claimant using claimed_as_dependent_on_another_return, an actual claim, which matches Step 2a ("Were you claimed or will you be claimed").
    • § 71.52(1g) counts the claimant's own earned income, and § 71.53(1)(c) allows one claimant per household.
    • (dm)'s "claimant or the claimant's spouse" age route gives the same result, since a spouse 62 or older always qualifies as the claimant.
    • Every branch except filer (S2) is pinned.
  • Vectorization and entities.
    • All three formulas are array-safe: tax_unit.sum, add, max_, enum indexing on federal filing_status, tax_unit.any, add(person, ...).
    • No new state-gated int or enum key, so the NH Add New Hampshire state supplementation for 2026 #10156 pattern doesn't apply.
    • Periods are YEAR throughout.
  • Housekeeping.
    • The changelog fragment vt-ut-wi-statutory-fixes.fixed.md is at the top level.
    • Tests are in variable-named YAML files.
    • No partner test files changed.
    • The property file is in the established tests/core Hypothesis pattern and costs about 1.7 s in CI.

Hand-checks (each matches the file):

  1. VT 2025, single, wages $10,000, interest $1,000: federal min($1,000, $2,500) = $1,000, with MAGI under $85,000 so no phase-out. AGI is $9,000, so the subtraction is $1,000 − $1,000 = $0 and vt_agi is $9,000.
  2. VT 2025, single, $50,000 / $4,000: federal $2,500. AGI $47,500 ≤ $120,000, so the subtraction is $4,000 − $2,500 = $1,500.
  3. VT 2025, joint, $195,000 / $3,000: federal $2,500 × (1 − $25,000 / $30,000) = $416.67. AGI $194,583.33 ≤ $200,000, so the subtraction is $3,000 − $416.67 = $2,583.33.
  4. VT 2022, joint at exactly $200,000: the 2022 joint phase-out ends at $175,000, so federal is $0. $200,000 ≤ $200,000, so the subtraction is $3,000.
  5. VT 2025, surviving spouse, $50,000 / $4,000: federal $2,500 (non-joint phase-out $85,000–$100,000), so AGI is $47,500. The limit is −∞, so the subtraction is $0.
  6. VT 2025, married filing separately at $120,000: federal $0 (§ 221(e)(2)). $120,000 ≤ $120,000, so the subtraction is $3,000.
  7. VT 2021, $50,000 / $4,000: federal $2,500. in_effect is false, so the subtraction is $0 and vt_subtractions is $0.
  8. UT 2025, joint, wages $2,000 and $1,000, one newborn: AGI $3,000 ≤ $50,000, both spouses ≤ $3,000. $100 × 1 child × (any at-home parent) = $100 (main gave $200).
  9. UT 2022, two eligible parents and two children: $100 × 2 = $200 (was $400).
  10. WI 2025, 18-year-old with no earnings and 17-year-old earning $5,000: the head is 18 but has no route; the spouse earns but is 17. So false (main: true).
  11. WI 2025, joint, both 30, $500 each, the head claimed: the spouse qualifies. Household income $1,000 ≤ $8,060, so the credit is 0.8 × ($500 + $500) = $800.
  12. WI 2025, both claimed, both 62: the 62-or-older exception applies, so eligible, $800.
  13. WI 2024, Case 8: the 40-year-old's $6,000 is unemployment, which isn't earned income; the 17-year-old dependent isn't a filer. So false.
  14. WI 2024, Case 14: the spouse is 65, so she qualifies through age. Household income is $9,000 of Social Security, under $24,680, so true.

Axiom line assessment

axiom: TheAxiomFoundation/rulespec-us#2029 queued ...; #2030 queued ...; #2031 queued ... is valid.

CI status

All 36 checks pass at 6f203e8ba3 (run 41947821257), rechecked just now. No failures, nothing pending. The new property file ran in Rest (Python shard 4) in about 1.7 s.

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: 1 critical (the merge with main rebuilt Vermont's 2021 subtraction list without vt_other_subtractions, which main has had since #10302; a 2021 filer's subtractions drop from $1,300 to $0; re-add it and pin a 2021 case), 4 should (the new VT student loan entry and the WI filer guard are unpinned; WI follows Schedule H's three routes over 71.54(1)(gm) without a record; the Act 138 page references are off). All three fixes otherwise match the law. Full report in the comment above.

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.

4 participants