Skip to content

Bar only the claimed spouse from the student loan interest deduction (26 U.S.C. 221(c)) - #10577

Merged
MaxGhenis merged 6 commits into
mainfrom
student-loan-interest-claimed-spouse
Nov 2, 2026
Merged

MaxGhenis merged 6 commits into
mainfrom
student-loan-interest-claimed-spouse

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Change

26 U.S.C. 221(c) denies the student loan interest deduction "to an individual" for whom another taxpayer is allowed a section 151 deduction. It doesn't reach that individual's spouse. On a joint return filed only to claim a refund (26 U.S.C. 152(c)(1)(E)), where one spouse is claimed elsewhere, the other spouse now keeps the deduction for their own qualified interest.

  • student_loan_interest_ald_eligible tests the person's own claimed_as_dependent_on_another_return (head_or_spouse & ~separate & ~claimed) instead of barring the return when another taxpayer claims either filer.
  • student_loan_interest_ald_magi now covers every head or spouse (defined_for), so joint MAGI for the 221(b)(2) phase-out includes a claimed spouse's income.
  • Unchanged: the eligible-interest filter, the single return-level $2,500 cap of 221(b)(1), the proportional allocation and the joint phase-out. No parameter changes. The PR leaves the education-credit dependency logic alone.

Ruling: statute governs the form; Max 2026-10-20. The Form 1040 Schedule 1 line 21 instructions bar the return if "you, or your spouse if filing jointly" are claimed; the statute bars only the claimed individual.

Source-conflict register

source_conflicts/candidates.yaml splits the old joint item. us:education-benefits-joint-claimed-spouse now covers the education credits under 25A(g)(3) only (code: is_barred_from_education_credits_by_dependency.py; still conflicting-source, with the page-anchor caveat). The new us:student-loan-interest-joint-claimed-spouse records 221(c) against the Form 1040 instructions, follows: statute, state: ruled, code student_loan_interest_ald_eligible.py, this PR.

State effects

On a joint return with one claimed spouse, federal AGI falls by the unclaimed spouse's deduction, so every state whose tax starts from federal AGI moves with it. Hawaii also reads student_loan_interest_ald_eligible directly, and Montana reads student_loan_interest_ald. Tests cover the two cases below. No one has checked whether each state's conformity to the Code carries § 221(c)'s per-person bar.

  • Hawaii (from 2025, hi_student_loan_interest_deduction reads student_loan_interest_ald_eligible). Refund-only joint return: head with $2,000 wages and $1,000 interest, claimed spouse with $700 interest. The Hawaii deduction goes from 0 to 1,000 and hi_agi from 2,000 to 1,000; hi_income_tax stays −220 at this income. Test: hi_student_loan_interest_adjustment.yaml.
  • Kentucky (each column starts from the filer's person-level federal AGI). Head with $50,000 wages and $2,000 interest, claimed spouse with $1,000 interest, 2025: ky_agi for the head goes from 50,000 to 48,000 and ky_income_tax from 1,869.20 to 1,789.20 (2,000 × 4%). Test: ky_federal_agi.yaml (mechanics only).

Invariants (Hypothesis property test policyengine_us/tests/core/test_student_loan_interest_claimed_spouse_invariants.py)

Each drawn couple has the claimed filer as either head or spouse.

  1. An unclaimed filer's deduction never depends on the claimed filer's own interest, or on whether another taxpayer claims a filer with no interest.
  2. The claimed filer's own interest is never deducted.
  3. The return's deduction never exceeds the cap.

Two explicit @example batches (2019 and 2025) put a claimed filer's income inside the joint phase-out, with the claimed filer as head and as spouse. If joint MAGI dropped the claimed filer, invariant 1 would fail by $666.67 to $833.33.

Corrections to the first hand-off comment

  • CI: that comment said "Draft PR, so CI hasn't run". The PR is ready and CI ran at 6ba66caa.
  • MAGI dependents: the formula doesn't fully zero dependents, because ald_sum_person isn't masked by not_dependent (student_loan_interest_ald_magi.py:44, :68). The new defined_for = "is_tax_unit_head_or_spouse" (:25) excludes dependents anyway, so no output changes.
  • Line references: the eligibility return is student_loan_interest_ald_eligible.py:38 at the current head (:35 before the reference became a tuple), and the MAGI defined_for is student_loan_interest_ald_magi.py:25.

Tests

YAML (student_loan_interest_ald.yaml, student_loan_interest_ald_eligible.yaml): the two joint cases that required both spouses to lose the deduction now bar only the claimed spouse; added the two refund-only joint cases (AGI $1,000 and $2,500) and a phase-out case whose joint MAGI counts the claimed spouse. Hawaii and Kentucky each get a joint case with a claimed spouse.

impact: none at baseline. Both changed formulas read claimed_as_dependent_on_another_return, an input with no formula, and the production dataset carries no such column, so the input is false for every record. Eligibility then matches main (head_or_spouse & ~separate). student_loan_interest_ald_magi's wider defined_for adds MAGI only for married filing separately filers, whom eligibility still bars, so student_loan_interest_ald matches main for every record.

axiom: TheAxiomFoundation/rulespec-us#1981 queued

🤖 Generated with Claude Code

26 U.S.C. 221(c) denies the deduction to the individual whom another
taxpayer claims, not to that individual's unclaimed spouse. The Form 1040
worksheet bars the whole joint return; per the 2026-10-20 ruling the
statute governs. Joint MAGI now includes both filers regardless of
eligibility, so a claimed spouse's income still counts in the phase-out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6K4grkKNkfjkW6hbaN
A Hypothesis property test checks that an unclaimed filer's deduction
ignores the claimed spouse, that a claimed spouse's own interest is never
deducted, and that the return never exceeds the 221(b)(1) cap. The
float32 determinism probe sets is_tax_unit_head_or_spouse, which MAGI's
defined_for now reads.

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

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 6ba66caa5bdbd02fe69ec700e3f4259bf579f3f5

What the item asked and how it's resolved

  • Person's own claim, not return-wide: student_loan_interest_ald_eligible.py:23-34 returns head_or_spouse & ~separate & ~claimed.
  • Joint MAGI includes both filers: student_loan_interest_ald_magi.py:24 defined_for = "is_tax_unit_head_or_spouse" (the formula already zeroes dependents).
  • Eligible-interest filter, single cap, allocation, joint phase-out: student_loan_interest_ald.py unchanged.
  • Tests that required both spouses to lose: student_loan_interest_ald_eligible.yaml cases at lines 59 and 85 now expect [false, true] and [true, false].
  • "The existing invariant": tests/core/test_claimable_filer_federal_invariants.py asserts nothing about student loan interest (its docstring says that gate is covered by YAML only), so it's unchanged. The only Python test that broke was the float32 probe in tests/core/test_ald_determinism.py:41, which sets the MAGI defined_for variable as an input; it now also sets is_tax_unit_head_or_spouse.
  • Education-credit dependency logic: untouched.
  • Spec test cases: both added to student_loan_interest_ald.yaml with the spec's outputs, plus a mechanics case for the joint phase-out with a claimed spouse's MAGI.

Hand computations

  • Case 1 (2025): eligible interest 1,000 + 0; cap 2,500 and joint start 170,000 don't bind; AGI 2,000 − 1,000 = 1,000.
  • Case 2: eligible 4,000 capped at 2,500; MAGI 1,000 + 4,000; AGI 5,000 − 2,500 = 2,500.
  • Phase-out case: MAGI 150,000 + 30,000 = 180,000; 2,500 × (1 − 10,000/30,000) = 1,666.67.

Tests (one file per process)

  • student_loan_interest_ald.yaml 16 passed; student_loan_interest_ald_eligible.yaml 9 passed; student_loan_interest_ald_magi.yaml 15 passed.
  • dependent_above_the_line_deductions.yaml 9 passed; adjusted_gross_income_person_own_deductions.yaml 15 passed.
  • test_student_loan_interest_claimed_spouse_invariants.py 2 passed (6.5 s test time, ~40 s wall including import, local; peak memory not measured, no time -v on this VM). 4 Hypothesis examples × 10–30 couples × 4 variants in one simulation each.
  • test_ald_determinism.py 9 passed; test_tax_unit_non_dep_add_projector.py 1 passed; test_modified_agi_add_backs.py 13 passed; test_dependent_above_the_line_deductions_invariants.py 9 passed.

Mutants (each in its own process, code restored after)

  • M1, return-wide bar restored in eligibility: ALD YAML 3 failed; property test 2 failed. Killed.
  • M2, MAGI defined_for back to eligibility: ALD YAML 2 failed (claimed-head MAGI, phase-out); property test 2 failed. Killed.

CI

Draft PR, so CI hasn't run.

Judgment calls

  • Invariant 1 is checked as: with the spouse claimed, the head's deduction is the same whatever the spouse's interest, and equals the head's deduction when an interest-free spouse is unclaimed. With an unclaimed spouse who has interest, the shared cap can lower the head's share, so the test checks only that it's no higher.
  • The phase-out case sets the claim as an input on a return that couldn't be refund-only at that income; it pins mechanics only, and says so.
  • Statute text not re-fetched here (law.cornell.edu not tried after network limits noted in the brief); quotes come from the brief's appendix.

Proposed follow-up

  • Education credits: re-check the joint-return dependency bar against 25A(g)(3) — is_barred_from_education_credits_by_dependency bars the whole return. The appendix found no lawful positive counterexample, so this needs a dependency-rules case before any change.

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

The property test now draws whether the claimed filer is the head or the
spouse, so a bar that reached only a claimed spouse fails it, and pins
phase-out examples where the claimed filer's income moves joint MAGI in
2019 and 2025. The candidates register splits the student loan interest
half off the education-credit item, since PE-US now follows 221(c).
Hawaii and Kentucky each get a joint case with a claimed spouse, the
states where this eligibility reaches state tax. The eligibility
reference now cites 221(e)(2) as well as 221(c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FN4kh1QukzuAxXmrD
Its head earns enough that the joint return couldn't be filed only for a
refund, so the claim is an input that pins the column arithmetic.

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

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 63fde0b1afbe50c7a68cf42887713c9b23b2dfac (two commits on top of 6ba66caa, which was the head when I picked this up; nobody else pushed in between).

Items and how each is resolved

  1. Property test (P3-1, P3-2). policyengine_us/tests/core/test_student_loan_interest_claimed_spouse_invariants.py
    • Each drawn couple now draws claimed_role from head or spouse (:55). The situation builder gives the claimable filer the head or spouse role to match (:104-125), so invariants 1 and 2 are checked with either filer claimed. Rows are ordered unclaimed filer then claimable filer, whatever their roles.
    • Two explicit @example batches (:179-180): PHASE_OUT_2025 and PHASE_OUT_2019 (:81-98). Each puts a claimed filer's income inside the joint phase-out, once with the claimed filer as spouse and once as head, plus a zero or below-phase-out couple. The hand computations are in the comments there.
    • max_examples=4 and derandomize=True are unchanged. The explicit examples run on top of the 4 draws.
    • Run time, local VM (not Linux CI): property test call 2.78 s (was 2.52 s), crafted witness 0.12 s, 2 passed in 6.62 s, about 42 s wall including import. Peak RSS of the pytest process was 909 MB (was 903 MB), measured with resource.getrusage. This file is part of the core Python test group; I didn't check that group's CI timing report.
  2. candidates.yaml (P3-3). source_conflicts/candidates.yaml
    • us:education-benefits-joint-claimed-spouse (:10213) now covers only the education credits. Its question, statute (25A(g)(3)), instructions citation and readings cover the credits alone. code has only is_barred_from_education_credits_by_dependency.py, follows: conflicting-source is unchanged, and the note drops the student loan interest half but keeps the page-anchor caveat.
    • New us:student-loan-interest-joint-claimed-spouse (:10725), placed between us:ssi-spousal-deeming-child-scope and us:surviving-spouse-child-gross-income-test in sorted order. It has: statute 26 U.S.C. 221(c) (…/26/221#c); instructions at the same i1040gi--2025.pdf#page=98 the old item cited; follows: statute; code set to the eligibility file; prs set to this PR; state: ruled; and an example drawn from this PR's refund-only YAML case.
    • test_source_conflicts.py: 103 passed.
  3. State cases (P3-5).
    • Hawaii: policyengine_us/tests/policy/baseline/gov/states/hi/tax/income/hi_student_loan_interest_adjustment.yaml:277. 2025 refund-only joint return: head with $2,000 wages and $1,000 interest, claimed full-time-student spouse with $700 interest.

      • Hawaii interest is the head's only: min(1,000, 2,500 cap) = 1,000.
      • Federal AGI is 2,000 − 1,000 = 1,000, and Hawaii MAGI adds back the 1,000 federal deduction, giving 2,000. That is below the 100,000 joint start, so the deduction is 1,000.
      • The adjustment is 1,000 − 1,000 = 0, so hi_agi is 1,000.
    • Kentucky: policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_federal_agi.yaml:365. 2025 joint return: head with $50,000 wages and $2,000 interest, claimed spouse with $1,000 interest and no income.

      • Federal deduction: [2,000, 0], since joint MAGI of 50,000 is below the 170,000 start.
      • ky_federal_agi = ky_agi = [48,000, 0].
      • Joint path: (48,000 − 3,270 standard deduction) × 4% = 1,789.20. The separate path gives the same, so joint stands.
      • Family size credit: KY MAGI is well above 133% of the two-person guideline, so it doesn't apply; no personal or refundable credits apply either. ky_income_tax = 1,789.20.
      • Labelled mechanics only: at this income the joint return couldn't be refund-only.
    • Before and after, computed with main's student loan interest variables checked out in a separate process:

      before (main) after
      hi_student_loan_interest_deduction 0 1,000
      hi_agi 2,000 1,000
      hi_income_tax −220 −220
      ky_agi (head) 50,000 48,000
      ky_income_tax 1,869.20 1,789.20
    • These differ from the reviewer's figures (HI −29.22 → −73.22, KY 200.76 → 125.84) because the households differ. The review didn't give its inputs. Hawaii's deduction moves 0 → 1,000 in both. At $1,000–2,000 AGI the Hawaii return owes nothing before refundable credits, so hi_income_tax doesn't move in my case. Kentucky's change is the head's deduction × 4% (−80 here; the reviewer's −74.92 is consistent with a different deduction amount, which I couldn't check).

  4. P3-7. student_loan_interest_ald_eligible.py:9-12: reference is now a tuple with …/26/221#c and …/26/221#e_2. The long documentation line is wrapped (:17-19).
  5. Mutants. Each ran in its own process, with git checkout of the variable folder after and a clean tree confirmed.
    • H, bar only a claimed spouse: return head_or_spouse & ~separate & ~(claimed & person("is_tax_unit_spouse", period)). Property test: 1 failed, 1 passed. Hypothesis reports 2 distinct failures in the explicit examples: the 2025 batch fails invariant 1 (max abs difference 476.19), and the 2019 batch fails invariant 2 (ald[:, [a, b], 1] == 0). The crafted-witness test passes because it claims only the spouse. Killed by the property test.
    • P, phase-out (MAGI defined_for back to student_loan_interest_ald_eligible): 2 failed. Both explicit examples fail invariant 1, one with max abs difference 666.67 (the 2019 hand value) and the other with the same error. The crafted witness fails with 833.33. Both gaps are far above 10 × TOLERANCE = 0.1. Killed by the property test.
  6. Runs, one file per process:
    • student_loan_interest_ald.yaml: 16 passed.
    • student_loan_interest_ald_eligible.yaml: 9 passed.
    • test_student_loan_interest_claimed_spouse_invariants.py: 2 passed.
    • test_ald_determinism.py: 9 passed.
    • hi_student_loan_interest_adjustment.yaml: 11 passed.
    • ky_federal_agi.yaml: 15 passed.
    • test_source_conflicts.py: 103 passed.
    • No full suites or microsimulations were run.
  7. Body. Updated through REST PATCH and read back. It adds the "Source-conflict register" and "State effects" sections, notes either-role claims and the explicit examples under Invariants, and adds a "Corrections to the first hand-off comment" section. That section covers the stale CI line, ald_sum_person not being masked by not_dependent (_magi.py:44, :68) with the new defined_for (:25) excluding dependents so no output changes, and corrected line numbers (_eligible.py:38 at this head, :35 before the reference became a tuple; _magi.py:25). The impact: and axiom: lines are unchanged.
  8. Keep claimable-filer rules in the repeal of state dependent exemptions #10134: not touched.

CI

The latest Pull request run at 63fde0b1, https://github.com/PolicyEngine/policyengine-us/actions/runs/41469966080, is still in progress more than 60 minutes after the push, so I'm handing off before it finishes.

  • Every job that has finished passed: all Baseline shards (including irs and states-shards 1–4), all Python shards, Microsimulation, Household API Partners, PR validation, Package and compatibility, and Quick Feedback.
  • Still running when I checked at 16:21 UTC: the nine Contrib shards (congress, other-shard-1/2a/2b/3, states-shard-1–4).

The run at 60bf6c66 was superseded by the second push and doesn't count.

Judgment calls

  • I kept max_examples=4 and added explicit examples instead of raising it. The examples guarantee phase-out coverage for both roles and both years without changing the derandomized draws, and they cost about 0.3 s.
  • In the property test, the variant key is still named spouse_interest; it now means the claimable filer's interest. I left the name to keep the diff small.
  • The new candidates item's readings.other paraphrases the instructions from the quote in this PR's body ("you, or your spouse if filing jointly"). I didn't re-fetch the Form 1040 instructions or 26 U.S.C. 221 here. The quotes come from the PR's own references and comments, so the 221(c) and 221(e)(2) anchors and the #page=98 anchor are unverified in this session.
  • The second push (63fde0b1) only adds a "mechanics only" comment to the Kentucky case. It went up about two minutes after the first, so CI at 60bf6c66 was superseded rather than finished.

Proposed follow-up

  • Hawaii and Kentucky: confirm conformity covers the per-person 221(c) bar. Both states read the federal eligibility variable. Check that Hawaii's static-conformity IRC date and Kentucky's KRS 141.010 conformity carry the "to an individual" reading, not the Form 1040 whole-return bar.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: APPROVE
63fde0b
METHODOLOGY_CHOICE: no

This is round 2 of the review. The head hasn't moved since 63fde0b1. I found no earlier in-progress review comment, so I started from the beginning. I reviewed the two commits in 6ba66caa..63fde0b1 in full.

Summary:

  • Every round-1 gap the build was asked to close is closed.
  • I reran the head-claimed mutant (round 1's M4). The property test now kills it, and so does the phase-out mutant, each by far more than 10 × TOLERANCE.
  • Every hand value in the new comments and state cases matches my own arithmetic and a run at head. The Hawaii and Kentucky before/after table in the hand-off reproduces exactly at main and at head.
  • CI run 41469966080 finished green: 34 of 34 jobs succeeded.

Nothing here is P1 or P2. The three P3 notes below don't block.

The METHODOLOGY_CHOICE is no because the PR applies a recorded statute-over-instructions ruling. It doesn't choose between two readings of the statute. One related point, whether § 221(c) turns on an actual claim or on claimability, is under "What I couldn't check".

Findings

P3-1. The "State effects" section understates the reach and mislabels Kentucky.

Where: the PR body, "State effects": "States that read this eligibility change on a joint return with one claimed spouse. Both follow § 221 …"

What's wrong:

  • Kentucky doesn't read student_loan_interest_ald_eligible. ky_federal_agi starts from adjusted_gross_income_person.
  • The change reaches every state whose tax starts from federal AGI, not only these two. Hawaii is the only state code that reads the eligibility (hi_student_loan_interest_adjustment.py:22). Montana reads student_loan_interest_ald (mt_taxable_social_security.py:37, mt_taxable_social_security_joint.py:39).
  • Witness: the Kentucky household from the PR's Kentucky test, moved to Illinois for 2025, run at main and at head:
main: adjusted_gross_income 50000  il_base_income 50000  il_income_tax 2333.925  income_tax 1850
head: adjusted_gross_income 48000  il_base_income 48000  il_income_tax 2234.925  income_tax 1650

That's −$99 = $2,000 × 4.95%. Nothing is wrong in the code. A reader of the body would just think only two states move.

Replacement for the section's first two sentences:

On a joint return with one claimed spouse, federal AGI falls by the unclaimed spouse's deduction, so every state whose tax starts from federal AGI moves with it. Hawaii also reads student_loan_interest_ald_eligible directly, and Montana reads student_loan_interest_ald. The two cases below are tested. Whether each state's IRC conformity carries § 221(c)'s per-person bar hasn't been checked against state law.

P3-2. A candidates.yaml merge conflict with #10599.

Where: source_conflicts/candidates.yaml:10725.

  • Open PR Apply the standard plan's $50 minimum once across a borrower's Direct and Parent PLUS loans #10599 inserts us:standard-plan-minimum-consolidated and us:standard-plan-minimum-per-borrower at the same spot: hunk @@ -10723,6 +10723,70 @@, between us:ssi-spousal-deeming-child-scope and us:surviving-spouse-child-gross-income-test. This PR puts us:student-loan-interest-joint-claimed-spouse in that same gap.
  • Whichever PR lands second will conflict there.
  • Resolution: keep all three items in sorted order, the two us:standard-plan-* items first, then us:student-loan-interest-joint-claimed-spouse.

P3-3. The register bookkeeping still points at the old id after the split.

  • The hub's rulings row for this ruling (2026-10-20) is keyed to us:education-benefits-joint-claimed-spouse. After this PR, the ruled § 221(c) question lives under us:student-loan-interest-joint-claimed-spouse. The education item stays follows: conflicting-source.
  • When the hub registers this ruling in rulespec-us, it should re-key the row to the new id and pin the new entry's follows with student_loan_interest_ald.yaml's refund-only case. That case's inputs and result match the new item's example: head $2,000 wages and $1,000 interest, claimed spouse $700, deduction $1,000.
  • None of this is a code change in this PR.

Checked and sound

Defect at main. I copied head's four changed YAML files and ran them against main's code:

File Result at main
student_loan_interest_ald.yaml 3 failed, 13 passed
student_loan_interest_ald_eligible.yaml 2 failed, 7 passed
hi_student_loan_interest_adjustment.yaml 1 failed, 10 passed
ky_federal_agi.yaml 1 failed, 14 passed

Every failure is a new or changed case, so the defect reproduces.

P3-1 and P3-2 (round 1), the property test.

  • couples() draws claimed_role from ["spouse", "head"] (test_…_invariants.py:55).
  • _situation sets is_tax_unit_head and is_tax_unit_spouse from that role for both people.
  • Rows are ordered unclaimed then claimable, so ald[:, :, 0] is always the unclaimed filer.
  • The parameters at head give a 2025 joint phase-out start of $170,000 and a 2019 start of $140,000, with a $30,000 range in both years. I recomputed every hand value in the PHASE_OUT_2025 and PHASE_OUT_2019 comments:
    • 2025: 180,000 → 2,500 × (1 − 10/30) = 1,666.67, against 2,500 without the claimed filer;
    • 2025: 185,000 → 2,000 × (1 − 15/30) = 1,000, against 2,000;
    • 2019: 150,000 → 2,000 × (1 − 10/30) = 1,333.33, against 2,000.
  • A run at head of each phase-out couple, claimed filer as head and as spouse, gave unclaimed student_loan_interest_ald of 1,666.67, 1,666.67, 1,000, 1,333.33 and 1,333.33. The claimed filer gets 0 in every case.
  • In each phase-out example, the claimed filer's own wages are what push joint MAGI past the start. Each year has one claimed-head and at least one claimed-spouse phase-out example.

Mutants. Each ran in its own process on a scratch worktree. I printed policyengine_us.__file__ to show the mutated tree was the one loaded, and confirmed a clean tree after each.

Mutant Property test YAML Killed by
MH (round 1's M4): ~(claimed & is_tax_unit_spouse) 1 failed: "2 distinct failures in explicit examples", max diff 476.19 _ald.yaml 1 failed, _eligible.yaml 2 failed property test (explicit examples)
MP: MAGI defined_for back to eligibility 2 failed: max diff 666.67 (2019 batch), 833.33 (crafted witness) _ald.yaml 2 failed property test
MC (mine): eligibility reads claimable_as_dependent_on_another_return 2 passed _eligible.yaml 1 failed ("2024 single filer who can be claimed but is not claimed…"); _ald.yaml, HI and KY pass YAML only
MK (mine): Hawaii sums all members' interest, dropping * eligible n/a hi_student_loan_interest_adjustment.yaml 1 failed (the new case) new Hawaii case
  • The 476.19 for MH is the hand value: with the head claimed but not barred, interest of 3,500 is capped at 2,500. The unclaimed share is 2,500 × 2,500 / 3,500 × 2/3 = 1,190.48, against 1,666.67.
  • MC survives the property test because claimable_as_dependent_on_another_return defaults to the claimed input. A single YAML case holds the claimed-vs-claimable distinction. That's enough for a kill, but it matters for the open question below.

Tests at head, one file per process:

File Result
student_loan_interest_ald.yaml 16 passed
student_loan_interest_ald_eligible.yaml 9 passed
hi_student_loan_interest_adjustment.yaml 11 passed
ky_federal_agi.yaml 15 passed
test_student_loan_interest_claimed_spouse_invariants.py 2 passed
test_ald_determinism.py 9 passed
test_source_conflicts.py 103 passed

For the property test, the test call took 2.87 s and the crafted witness 0.13 s. Wall time was 53.5 s including import, and peak child RSS was 913 MB. These were local VM figures, not Linux CI, and are in line with the hand-off's 2.78 s and 909 MB.

P3-5 (round 1), the state cases. I computed these from the parameters at head; the law itself was unreachable (see below).

Hawaii, 2025 (in_effect true from 2025, cap 2,500, joint phase-out start 100,000, range 30,000):

  • Federal deduction: [1,000, 0]. Federal AGI: 2,000 − 1,000 = 1,000.
  • hi_modified_agi = AGI + federal deduction = 2,000, under 100,000.
  • Hawaii deduction: min(1,000, 2,500) = 1,000. The adjustment is 1,000 − 1,000 = 0, so hi_agi = 1,000.

Kentucky, 2025 (rate 4%, standard deduction 3,270):

  • Head: (48,000 − 3,270) × 0.04 = 1,789.20.
  • At main: (50,000 − 3,270) × 0.04 = 1,869.20.

Run at main and at head, these match the hand-off's table:

main head
hi_student_loan_interest_deduction 0 1,000
hi_agi 2,000 1,000
hi_income_tax −220 −220
ky_agi (head) 50,000 48,000
ky_income_tax 1,869.20 1,789.20

The Hawaii change runs entirely through federal AGI: the Hawaii adjustment is 0 at both. The hand-off's explanation of why these differ from round 1's figures is consistent: the households differ.

P3-3 (round 1), the register split.

  • The education item now cites only 25A(g)(3). Its code holds only is_barred_from_education_credits_by_dependency.py, follows is still conflicting-source, and the note keeps the page-anchor caveat.
  • The new item carries the fields the build brief listed: follows: statute, state: ruled, the eligibility file, this PR, and § 221(c) and #page=98 sources.
  • The 22 us: ids are in sorted order, with the new item between ssi-spousal-deeming-child-scope and surviving-spouse-child-gross-income-test.
  • The new note names no person ("the project lead").
  • test_source_conflicts.py passes: 103 passed locally and green in CI.

P3-7 (round 1).

  • student_loan_interest_ald_eligible.py:9-12 is a tuple with …/26/221#c and …/26/221#e_2, matching the repo's LII anchor style (#b_2_C in the MAGI file and 305 other #x_n anchors).
  • The documentation line is wrapped (:15-18).

The body's "Corrections" section, checked against head.

  • The eligibility return is at student_loan_interest_ald_eligible.py:38.
  • In student_loan_interest_ald_magi.py, defined_for is at :25, ald_sum_person at :44 (built without not_dependent), and its use at :68. All correct.
  • "no output changes" holds for dependents: at main, MAGI was defined_for the eligibility, which excludes non-filers, so dependents were already 0. Its only code reader is student_loan_interest_ald.py:35.
  • The CI correction is right. The run at 6ba66caa passed, as round 1 reported.

Readers.

  • In code, only student_loan_interest_ald reads student_loan_interest_ald_magi.
  • student_loan_interest_ald_eligible is read by student_loan_interest_ald.py:12,22 and by Hawaii (:22).
  • The eligibility output moves only on joint returns with exactly one claimed filer. Downstream it reaches federal AGI and everything built on it (P3-1 above). I found no reader that this PR makes worse.

Population.

  • claimed_as_dependent_on_another_return has no formula. claimable_as_dependent_on_another_return defaults to it.
  • policyengine-us-data at main (42ed5d45, an archived repo) never sets either flag.
  • Populace, its replacement, at 4b1b1b10 lists the claimed flag under "Inputs no concept covers". Its engine ABI lock marks it declared_absent_engine_input with policyengine_default_if_absent.
  • So at baseline the flag is False for everyone, and the diff should move no aggregate. The state test cases don't change that: they're test inputs, not data.
  • The hub's impact check should confirm main-vs-head totals of student_loan_interest_ald, adjusted_gross_income, income_tax and state_income_tax differ by exactly 0. Household API users who set the flag on a joint return will see changes.

CI. Run 41469966080 (Pull request) at 63fde0b1 completed with conclusion success. All 34 jobs succeeded, none were skipped or cancelled:

  • 11 Baseline shards;
  • 9 Contrib shards, which were still running at hand-off;
  • Microsimulation, 7 Python shards plus plan and check;
  • Household API Partners, PR validation, Package and compatibility, and Quick Feedback.

Body lines. impact: the hub will measure and axiom: TheAxiomFoundation/rulespec-us#1981 queued are present.

Scope.

What the build brief asked for that the PR doesn't do. Nothing material. The "State effects" paragraph exists but understates its reach (P3-1).

What I couldn't check

  • No law source was reachable. The egress proxy refused www.law.cornell.edu, uscode.house.gov, www.govinfo.gov, www.irs.gov, www.ecfr.gov, capitol.hawaii.gov, files.hawaii.gov and apps.legislature.ky.gov. So I couldn't quote § 221(c), § 221(e)(2) or the Form 1040 instructions at i1040gi--2025.pdf#page=98, or confirm that either candidates item's anchors land on the text it cites. Round 1 fetched LII and page 98 and quoted both; I haven't re-verified them.
  • Claimed or claimable. The eligibility formula turns on an actual claim (claimed_as_dependent_on_another_return). Whether that's right depends on § 221(c)'s exact verb, "allowed" or "allowable". Only one YAML case separates the two (MC above). Round 1 quoted the statute as "allowed to another taxpayer". The hub should confirm the wording from the source.
  • State conformity. I couldn't fetch Hawaii's HRS 235-2.3/235-2.4 conformity date or KRS 141.010, so I can't say whether either carries § 221(c)'s per-person bar. Both states take federal AGI or the federal eligibility as computed, so a state that had decoupled from § 221(c) would need its own variable. That would be a follow-up and doesn't block this PR, whose federal change is the ruled one.
  • rulespec-us#1981. It's outside this session's GitHub scope, so I couldn't confirm it is dispatch-ready and labelled pe-parity as queued requires.
  • Population totals. I didn't run them; the hub measures them.
  • Environment note. Plain uv sync leaves out the dev extra, so the property test first failed to import hypothesis. I installed the locked version and reran it. The results above are from that rerun.

Generated by Claude Code

@MaxGhenis
MaxGhenis merged commit 12a633c into main Nov 2, 2026
35 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#10577 at f1f1551ead6fee50929c853a8a1b0191117d594a (squash): Bar only the claimed spouse from the student loan interest deduction (26 U.S.C. 221(c)).

  • Review: GPT-6.1 Sol reviewed round 1 and approved 6ba66ca with seven P3 notes; a cloud build closed six of them, and the seventh (draft Keep claimable-filer rules in the repeal of state dependent exemptions #10134) follows separately. An independent Opus 5.5 review in a cloud session approved 63fde0b in round 2. It ran the four changed YAML files against main (each fails on main), reran round 1's mutant and a phase-out mutant (the property test kills both), and recomputed every hand value. Its three P3 notes: the hub rewrote the body's state-effects lead, re-keyed the hub's ruling record to the split item, and set the merge order below. The hub then merged main into the branch as 4e2afae to clear the semantic-overlap guard, and an in-session Opus 5.5 check approved that merge: the PR's per-file patches match byte for byte before and after, and none of main's 18 new commits reaches the changed formulas or the test expectations. Main then changed source_conflicts/candidates.yaml again, so the hub updated the branch through the API as f1f1551: the PR's per-file patches still match apart from hunk offsets, and CI ran at that head.
  • Gates checked live at merge:
    • gh pr checks exits 0;
    • the latest Pull request run at the head concluded success;
    • MERGEABLE;
    • not a draft;
    • no reviewer's latest review requests changes;
    • the semantic-overlap guard;
    • head pinned.
  • Impact: None at baseline, from the code. Both changed formulas read claimed_as_dependent_on_another_return, an input with no formula that the production dataset (populace_us_2024, revision 9a814a3b) doesn't carry. So eligibility and the joint MAGI sum match main for every record, and the wider MAGI defined_for reaches only married filing separately filers, whom eligibility still bars. No microsimulation pair ran.
  • Order: Apply the standard plan's $50 minimum once across a borrower's Direct and Parent PLUS loans #10599 inserts two items at the same place in source_conflicts/candidates.yaml; it lands second and keeps all three in sorted order, the two us:standard-plan-* items first.
  • Axiom: [pe-parity] Encode §221(c) as an individual bar on a joint return (PE-US PR 10577) TheAxiomFoundation/rulespec-us#1981 queued

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants