Skip to content

Count FFEL debt in student loan proration and caps, use the latest return's AGI, and add the parent PLUS standard payment - #10415

Open
DTrim99 wants to merge 10 commits into
mainfrom
student-loan-idr-restrictions
Open

DTrim99 wants to merge 10 commits into
mainfrom
student-loan-idr-restrictions

Conversation

@DTrim99

@DTrim99 DTrim99 commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #10411. This PR addresses items 1, 2, 5 and 6 and documents item 3. Item 4 asked for a couple's cap; the law caps each borrower, so this PR keeps per-borrower caps and adds the borrower's own FFEL loans to them (correction posted on #10411). #10152 (PAYE, ICR, tiered standard plan, July 2026 loan bar) merged first, so this PR also carries the FFEL and AGI changes into PAYE and ICR, and applies the July 2026 bar to the Parent PLUS payment.

What changed and why

1. FFEL debt in spousal proration (34 CFR 685.209(b)(5), (e)(2)(i), (g)(1)(i)–(ii), (g)(3)(i)).

  • New input federal_ffel_loan_balance (Person, YEAR, not uprated; see item 5): eligible FFEL loans under (b)(5)(ii), excluding Federal PLUS loans to parents and excepted consolidation loans. The Direct Loan plans don't repay these loans; the balance enters the proration, the borrower's own IBR and PAYE cap, and the term of a consolidated Parent PLUS loan.
  • IBR, SAVE and PAYE (income_driven_repayment_balance_share): (g)(1)(i) times (g)(1)(ii) is (own Direct + own FFEL) / couple's (Direct + FFEL) × own Direct / (own Direct + own FFEL), which simplifies to own Direct / couple's (Direct + FFEL). The couple is the head and spouse of a joint tax unit.
  • RAP (repayment_assistance_plan_payment): (g)(3)(i) share = (own Direct + own FFEL) / couple's (Direct + FFEL). (g)(3) has no FFEL adjustment like (g)(1)(ii), and RAP can't repay FFEL loans ((d)(4)), so the code follows the literal formula: a borrower's own FFEL loans raise their share of the payment on their Direct Loans. Case 22 in repayment_assistance_plan_payment.yaml pins this. The proposed rule (91 FR 4284-85) and the final rule's preamble (91 FR 23838) copied the IBR proration "to provide consistency with IBR" without discussing FFEL loans; the variable documentation records this.

2. AGI year.

  • New income_driven_repayment_adjusted_gross_income (TaxUnit, YEAR). For RAP, 20 U.S.C. 1087e(q)(4)(A) defines AGI "for the most recent taxable year". For IBR, SAVE, PAYE and ICR, 34 CFR 685.209(b)(10)(i) defines income as AGI as reported to the IRS, which the Department takes from the latest return it obtains (685.209(l)(1)-(3); 20 U.S.C. 1098e(c)(2)(A)(i)). Whose income counts follows 685.209(e)(1)(i), and (e)(1)(ii) for ICR.
  • It follows the clean_vehicle_credit_prior_year_magi pattern: adds = ["adjusted_gross_income"], so it defaults to the current year's AGI, because household inputs usually cover only the simulated year. The default overstates payments for borrowers whose income rose since their last return. An explicit input overrides it; family size and RAP's dependent count still come from the current year.
  • These now use it:
    • repayment_assistance_plan_base_payment;
    • the IBR, SAVE, PAYE and ICR discretionary incomes;
    • ICR's income_contingent_repayment_income_percentage_factor.
  • With $20,000 on the latest return and $60,000 of current AGI, RAP is 1% × 20,000 / 12 = $16.67 (the Student loan plans: close the disclosed modeling restrictions (FFEL proration, AGI year, tax dependents, mixed spousal rates) #10411 example).

4. Each borrower's own standard payment caps IBR and PAYE (20 U.S.C. 1098e(b)(6)(A); 34 CFR 685.209(e)(2)(i), (f)(2)(ii), (f)(3)(ii), (g)(1)).

5. Documentation.

  • Balances are the opening balance for the year. Later years hold them at their dataset value, with no price or income uprating and no repayment roll-forward. Within a year, only RAP's final-payment cap draws them down.
  • Balance reference date (microcosm#1357): federal_student_loan_balance, federal_parent_plus_loan_balance and federal_ffel_loan_balance no longer declare CPI-U uprating. policyengine_us/tools/default_uprating.py also exempts them from the default dollar-input uprating (AGI per capita) through a small NO_DEFAULT_UPRATING_VARIABLES set; _is_float_dollar_input is unchanged for every other variable. A new test in test_uprating_extensions.py checks that each exempt name exists, declares no uprater and is a stock.
  • What the change does (Modal, a one-person dataset dated 2024 with a $30,000 balance, read in 2026): $31,626 before with CPI-U uprating; $33,271 with uprating removed alone (the default AGI-per-capita uprating applies); $30,000 in 2025 and 2026 with the exemption, for a dataset-backed Microsimulation and a household Simulation.
  • The payment variables (RAP, IBR, SAVE, PAYE) assume the borrower is enrolled and in active repayment, not in deferment, forbearance or default.
  • Item 3, tax-dependent borrowers: income_driven_repayment_eligible and repayment_assistance_plan_eligible document this as a modeling restriction. The code treats these borrowers as ineligible ($0), but the law doesn't exclude them, and RAP's $10 minimum would apply (companion case C17).
  • programs.yaml notes are updated to match.

6. Parent PLUS standard payment (34 CFR 685.208(b)(1)-(2); 685.220(i)(2)(ii), (i)(3)(i)).

  • New parent_plus_standard_repayment_payment (Person, MONTH): the standard payment on federal_parent_plus_loan_balance at the person's federal_student_loan_interest_rate, with the $50 minimum and the cap at the balance. $30,000 at 6% over 10 years: $333.06; the code gave $0 before.
  • Consolidated Parent PLUS loans: a Direct Consolidation Loan that entered repayment on or after July 1, 2006 repays over 10 to 30 years under 685.208(b)(2)(iii) (685.208(c) and (j) before July 1, 2026), by the consolidation loan plus the borrower's other education loans, counted up to the consolidation loan's amount. New input federal_parent_plus_loan_is_consolidated and parameter gov.ed.student_loans.standard_repayment.consolidation_repayment_period (thresholds $0, $7,500, $10,000, $20,000, $40,000, $60,000; 10, 12, 15, 20, 25, 30 years). $30,000 consolidated at 6%: $214.93 over 20 years.
  • It's separate from standard_repayment_payment, so the Direct Loan standard payment and the income-driven caps stay on the loans those plans can repay.
  • July 1, 2026 bar: a borrower with a Direct Loan made on or after July 1, 2026 can't use these plans, and Add Pay As You Earn, Income-Contingent Repayment and tiered standard repayment payments, and the July 1, 2026 loan bar #10152's tiered plan already amortizes their Parent PLUS balance, so this payment is $0 for them: $0 here and $253.16 under the tiered plan. The flag is yearly, so a loan made in the second half of 2026 moves all of 2026 to the tiered plan.
  • Not modeled: one interest rate for all of a person's federal loans; private education loans in the consolidation term; consolidated Parent PLUS loans repaid under ICR. The same consolidation term for Direct Consolidation Loans in standard_repayment_payment (code on main) is Standard repayment: Direct Consolidation Loans that entered repayment after June 30, 2006 use the 10-to-30-year term #10485.

Scope

Companion cases (TheAxiomFoundation/rulespec-us#1455, #1711)

Expected values are hand-computed from the law with fractional cents and absolute_error_margin: 0.01. RAP cases are labelled rulespec-us#1455 Cn / En in their names. The IBR, PAYE and Parent PLUS cases for this head are posted as dispatch payloads on #1455 and #1711.

Case Where Existing or new
C1, C2, C3, C5, C6, C7, C8 repayment_assistance_plan_base_payment.yaml Cases 1–4, 6–8 Existing B cases; added a $30,000 balance and A and P outputs
C4 repayment_assistance_plan_base_payment.yaml Case 9 New
E1–E7 (both sides) repayment_assistance_plan_base_payment.yaml Cases 10–16 New, two tax units and two marital units per pair, B, A and P
C9 repayment_assistance_plan_payment.yaml Case 6 Existing; added A
C10 repayment_assistance_plan_payment.yaml Case 20 New
C11 repayment_assistance_plan_payment.yaml Case 10 Existing
C12 repayment_assistance_plan_payment.yaml Case 16 Existing; added A
C13 repayment_assistance_plan_payment.yaml Case 14 Existing (annual period, in-year drawdown)
C14 repayment_assistance_plan_payment.yaml Case 17 Existing; added A
C15 repayment_assistance_plan/integration.yaml Case 4 Existing (annual period); added January
C16 repayment_assistance_plan_payment.yaml Case 21 New (law correction: $62.50, was $250)
C17 not added Deferred to the tax-dependent PR
C18 repayment_assistance_plan/integration.yaml Case 2 Existing
C19 repayment_assistance_plan_payment.yaml Case 12 Existing
C20 repayment_assistance_plan_payment.yaml Case 15 Existing (annual period, in-year drawdown)

Other new cases:

  • IBR (Cases 18–24): own caps with 0% and 12% rates ($250 + $300; $250 + $400 when binding), mixed new-borrower rates, the C16 analogue ($86.06), own FFEL loans ($50.19, $111.02), a Direct balance under $10 ($8), and a spouse barred by a July 2026 loan who still counts in the proration ($187.50).
  • PAYE: own caps with 0% and 12% rates, a spouse's FFEL debt ($59.46), and a payment within half a cent under $5 (the rounding choice, $10).
  • SAVE: own FFEL loans.
  • Balance share: FFEL cases, married filing separately with FFEL ([1, 0.4]), and a spouse's Parent PLUS plus a dependent's FFEL that don't dilute the head's share.
  • Standard payment cap: a new test file: each spouse's own standard payment ([250, 430.41]), FFEL at the person's rate, the $50 minimum, a dependent's own amount, and joint filers where only one spouse borrowed ([333.06, 0]).
  • RAP: a spouse's Parent PLUS and a dependent's FFEL ($550), the final-payment cap on the Direct balance only ($100 then $0), and an FFEL-only borrower who isn't eligible.
  • Parent PLUS standard payment: a new test file: $333.06, the $50 minimum, a final payment under the minimum, Parent PLUS with Direct Loans, no balance, the tiered plan after a July 2026 loan, the consolidation tiers ($214.93, $168.77, $143.29, $386.58, $359.73), and other loans counted up to the consolidation amount ($107.46).
  • AGI: a new test file, plus latest-return AGI cases for RAP (base and payment), the IBR, SAVE, PAYE and ICR discretionary incomes and ICR's income percentage factor.

Testing

Head fa6ac3a, locally (Python 3.11, policyengine-core 3.33), one file group per process:

  • All 23 student loan YAML files: 197 cases passed.
  • test_uprating_extensions.py and test_programs_registry.py: 943 passed, 10 skipped.
  • Code health (parameters.py, test_parameter_yaml_keys.py, reference URL tests, variable_names.py, test_uprating_placement.py, test_no_group_variables_in_person_adds.py, test_source_conflicts.py): 269 passed, 1 failed. The failure, test_source_conflicts.py::test_unscannable_files_fail_closed, is a Python 3.11 tokenizer difference in the scanner's self-test; that test file and source_conflicts/ are identical to main, and this PR touches no source-conflict file. CI runs a newer Python.
  • Earlier, on Modal at d563231 and cb6363a: the uprating tests (core/test_per_capita_uprating.py, core/test_uprating_series.py, core/test_pre_first_modeled_year_uprated_inputs.py, test_system_import.py, microsimulation/data/test_extend_single_year_dataset.py) passed, and the $30,000 balance read unchanged in 2025 and 2026.

No partner test files changed.

axiom: us:statutes/20/1098e, us:statutes/20/1087e/q queued (TheAxiomFoundation/rulespec-us#1455) | us:regulations/34-cfr/685/208, us:regulations/34-cfr/685/209 queued (TheAxiomFoundation/rulespec-us#1711) | 34 CFR 685.209(f)(1) (SAVE) n/a: counterfactual of a blocked rule, not current law

🤖 Generated with Claude Code

https://claude.ai/code/session_01LqCGPgxDukji8Bhs57c5rH

DTrim99 and others added 3 commits October 23, 2026 15:28
…d cap IBR at the couple's standard payment

- Add federal_ffel_loan_balance and count eligible FFEL debt in the
  income-driven repayment balance share (34 CFR 685.209(b)(5),
  (g)(1)(i)-(ii)) and in the Repayment Assistance Plan's spousal share
  ((g)(3)(i)).
- Add income_driven_repayment_adjusted_gross_income, the AGI on the most
  recent return (20 U.S.C. 1087e(q)(4)(A)), defaulting to current-year AGI,
  and use it for RAP, IBR and SAVE.
- Cap IBR at the couple's combined 10-year standard payment on eligible
  Direct and FFEL loans before prorating (685.209(f)(2)-(3), (e)(2)(i)).
- Document balance timing, the active-repayment assumption and the
  tax-dependent restriction.
- Add the rulespec-us#1455 companion cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e threshold

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add parent_plus_standard_repayment_payment, the 10-year standard payment
on federal_parent_plus_loan_balance under 34 CFR 685.208(b)(1), through
the shared amortization helper with the standard plan's $50 minimum and
balance cap. It is kept apart from standard_repayment_payment so the
income-driven caps stay on the loans those plans repay (#10411 item 6).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@DTrim99
DTrim99 marked this pull request as ready for review October 26, 2026 14:07
@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (909176a) to head (c6ee53c).
⚠️ Report is 692 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##              main    #10415    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files            4        21    +17     
  Lines           76       328   +252     
  Branches         2         1     -1     
==========================================
+ Hits            76       328   +252     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

DTrim99 and others added 4 commits October 26, 2026 11:27
Drop CPI-U uprating from federal_student_loan_balance,
federal_parent_plus_loan_balance and federal_ffel_loan_balance, and
exempt them from the default dollar-input uprating in
default_uprating.py. A loan stock is not a price- or income-indexed
flow, and repayment is not rolled forward, so with auto carry-over a
later year reads the stored balance unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…strictions branch

Resolve the IBR payment and eligibility documentation, the programs.yaml
IBR note and the standard payment test numbering against #10152. Keep the
July 1, 2026 loan bar in IBR, and apply it to the parent PLUS 10-year
payment too: the tiered plan repays those borrowers' parent PLUS loans, so
counting them under the 10-year plan as well would double the payment.
PAYE now picks up the FFEL proration through the shared balance share.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and ICR

- PAYE and ICR discretionary income and ICR's income percentage factor
  read income_driven_repayment_adjusted_gross_income.
- PAYE caps at the couple's standard payment on eligible Direct and FFEL
  loans before prorating (685.209(f)(2), (g)(1)), as IBR does, and keeps
  the FFEL proration through the shared balance share.
- Add PAYE cases for spouses at 0% and 12%, a binding couple's cap, a
  spouse's FFEL debt, and the latest-return AGI for PAYE and ICR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test_float_dollar_inputs_have_long_run_upraters requires every float
dollar input to carry an uprater. The loan balances that
default_uprating.py now holds at their dataset value are the deliberate
exception, so the check skips NO_DEFAULT_UPRATING_VARIABLES.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@PavelMakarchuk PavelMakarchuk 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.

Program Review

PR #10415, "Count FFEL debt in spousal proration, use the latest return's AGI, and cap IBR at the couple's standard payment" (DTrim99). Round 1, full review.

IDs: C = critical, SA = should address, G = suggestion.

Paths: V = policyengine_us/variables/gov/ed/student_loans/, T = policyengine_us/tests/policy/baseline/gov/ed/student_loans/. "Standard payment file" = T/income_driven_repayment/income_driven_repayment_standard_payment.yaml. Line numbers refer to the reviewed head. PDF links use the file page number, not the printed page number.

Source Documents

  • Reviewed head SHA: cb6363a
  • Mode: full
  • Scope: the diff from the merge base aa8596e3d8: 42 files, +1,530/-114 (20 variables, 17 YAML test files, the uprating tool and its test, programs.yaml, the changelog, one methodology page). No parameter changes.
  • Regulations: 34 CFR 685.209, eCFR versions of 2024-12-31, 2025-06-01, 2026-05-01 and 2026-07-01. The /current/ anchors resolve to the 2026-07-01 text. The 2026-01 test periods fall under the earlier text, and the cited paragraphs ((b)(5), (e)(2)(i), (f)(2)(ii), (f)(3)(ii), (g)(1)) say the same in both. Also 685.208 (2025-06-01, 2026-07-01), 685.220(i) (2026-10-01), 685.221 (IBR before 2024, read at 2022-01-01), and the FFEL rules 682.215 and 682.209 (2026-07-01).
  • Statutes: 20 U.S.C. 1098e and 20 U.S.C. 1087e (OLRC release point 119-134 and LII, current through Pub. L. 119-21); 20 U.S.C. 1078(b)(1)(L) with its LII notes; 26 U.S.C. 6103(l)(13).
  • PDF: Pub. L. 119-21 (page 274, 1087e(q)(4); pages 276-278, sec. 82001(f) and its effective date). RISE final rule, FR 2026-08556 (pages 71, 79-80, 98-99, 123). SAVE final rule, FR 2023-13112 (page 82). ICR factors, FR 2026-11540 (pages 2, 4). All text layers were clean.
  • Other rulemaking: the RAP proposed rule at 91 FR 4284-85, the 2009 IBR rule at 74 FR 55972, the 2012 PAYE rule at 77 FR 66088, the 2008 rule at 73 FR 63232, and ED's Income-Driven Repayment Plan Request form (OMB 1845-0102), Table 1.
  • Companion spec: TheAxiomFoundation/rulespec-us#1455 (cases C1-C20 and E1-E7, in MaxGhenis's comment of 2026-10-23).
  • Data: policyengine-us-data main 42ed5d45 (archived), microcosm main 5810fce, draft PRs microcosm#1246 and microcosm#1357, us-student-debt#9.
  • Year: target 2026. The tests cover 2023-2027.

Branch Status

  • The branch is 7 commits ahead and 4 behind main (9ee14cdb43). The 4 commits change no student-loan file. They add an Iowa LIHEAP entry near programs.yaml:1060, far from this PR's hunks at lines 1609-1660. GitHub reported the merge state as UNKNOWN at review time.
  • #10453, #10400 and #10119 (open) also edit test_uprating_extensions.py, in other functions. The hunks do not overlap.
  • CI passes 34 of 34 at the head.

What the PR gets right

  • FFEL proration. The IBR, PAYE and SAVE share, own Direct / the couple's (Direct + FFEL), correctly reduces 685.209(g)(1)(i) times (g)(1)(ii). The RAP share follows the literal (g)(3)(i) text, and the head-or-spouse gate is what (g)(3)(i) and (e)(2)(i) require. The #10411 examples reproduce ($62.50, $86.06, $50.19 and $111.02, $25.09).
  • Latest-return AGI. One overridable variable feeds all six consumers, and each consumer has its own override case. The $16.67 example reproduces.
  • Own FFEL in the cap amount is right (1098e(b)(6) covers loans "under part B or D"), and so is the order: lesser-of, proration, rounding, small-payment rule, Direct-balance cap.
  • Parent PLUS for unconsolidated loans: $333.06, the $50 minimum, the final payment, and the July 2026 bar with no double count against the tiered $253.16.
  • Uprating exemption: documented; $31,626, $33,271 and $30,000 reproduce; the carry-forward YAML cases catch every change to it.
  • Tests and code: 35 expected values recomputed by hand, all correct; 26 of 26 sampled rulespec-us#1455 cases match. No hard-coded values; correct entities and periods; ruff clean.

Critical (Must Fix)

C1. IBR and PAYE cap each spouse at the couple's combined 10-year payment, but the law caps each borrower at their own.

  • Where: V/income_driven_repayment/income_driven_repayment_standard_payment.py:9, 11-26, 31, 53-54 (TaxUnit sum over the head and spouse, cited to (e)(2)(i)); V/income_based_repayment/income_based_repayment_payment.py:12-20, 50-63; V/pay_as_you_earn/pay_as_you_earn_payment.py:12-16, 50-66.

  • Evidence (law):

    • 20 U.S.C. 1098e(b)(6)(A), which the IBR variable cites: when the payment "for all loans made to the borrower under part B or D" exceeds the 10-year amount under 1087e(d)(1)(A), "the maximum monthly payment required to be paid for all loans made to the borrower ... shall be the standard monthly repayment amount". Pub. L. 119-21 sec. 82001(f) rewrote this paragraph with effect from 2025-07-04 for every borrower in repayment (#page=276, pages 276-278), so it governs the 2026 test periods.
    • 685.209(e)(2)(i), the PR's reference for the spouse sum, includes the spouse's eligible debt only "for the purposes of adjusting the borrower's monthly payment amount as described in paragraph (g)". It does not bring the spouse's loans into (f).
    • (f)(2)(ii) and (f)(3)(ii): "What the borrower would have paid on a 10-year standard repayment plan based on the eligible loan balances". A standard plan never repays a spouse's loans.
    • 685.209 uses the same amount as one borrower's whole payment: (l)(9)(i) (a borrower who does not recertify pays it) and (j)(2)(ii) (interest capitalizes when a borrower pays it). With a couple cap times a share, no spouse can ever pay that amount.
    • The FFEL rule for the same statute, 682.215(d)(1)(i), uses "the amount of the borrower's eligible loans", as did 685.221(d)(1)(i) and the 2012 PAYE text. ED's request form (Table 1) and the 2009 rule use the spouse's loans only in the old partial-financial-hardship entry test and the proration, never in the cap.
    • No ED preamble applies a combined cap. The RISE preamble repeats that the capped payment is "what the borrower would have paid under a standard 10-year repayment plan" (#page=79, pages 79-80).
  • Evidence (runs): with $30,000 for each spouse at 0% and 12%, the own standard payments are $250 and $430.41. IBR Cases 17-18 and PAYE Cases 10-11 give person1 $300 and $340.21, above the $250 maximum (table below). A probe with $40,000 and $1,000 at 6% and AGI $300,000 gives [482.03, 12.05] instead of [444.08, 50]: the small-balance spouse's $50 minimum moves onto the other spouse. The comments in IBR Cases 17-18 and PAYE Cases 10-11 state the per-borrower result ("250 + 300", "250 + 400") and then pin the other one.

  • Why critical: a mismatch with a clear statutory rule, a value mismatch confirmed by runs, and a reference ((e)(2)(i)) that does not corroborate the rule. The docs disclose the couple cap but not that a spouse can pay above their own standard payment. Documentation does not save a choice that contradicts a clear rule.

  • The order argument does not change this. The (f)-then-(g) order says nothing about whose loans the cap covers, and (e)(2)(i) limits the spouse's debt to (g). 1098e(d) (a separate filer is computed on their own debt) implies that joint filers combine debt, and the (g) proration already does that. Issue #10411 item 4 asserts the couple cap without a citation; please correct it there too. The pre-PR order (prorate, then the own cap) was right. It only left out the borrower's own FFEL loans, which this PR correctly adds.

  • Fix (minimal): make the standard payment a per-person amount and apply it after the proration.

    In V/income_driven_repayment/income_driven_repayment_standard_payment.py:

-    entity = TaxUnit
+    entity = Person
-        "https://www.ecfr.gov/current/title-34/section-685.209#p-685.209(e)(2)(i)",
+        "https://www.law.cornell.edu/uscode/text/20/1098e#b_6_A",
-    def formula(tax_unit, period, parameters):
+    def formula(person, period, parameters):
         p = parameters(period).gov.ed.student_loans.standard_repayment
-        person = tax_unit.members
         ...
-        is_head_or_spouse = person("is_tax_unit_head_or_spouse", period.this_year)
-        return tax_unit.sum(where(is_head_or_spouse, standard_payment, 0))
+        return standard_payment

In V/income_based_repayment/income_based_repayment_payment.py:50-63, and the same in V/pay_as_you_earn/pay_as_you_earn_payment.py:50-66 with p.pay_as_you_earn.rate in place of rate:

        # 685.209(g)(1)(i)-(ii) prorate the income-based amount to the
        # borrower's Direct Loans. 20 U.S.C. 1098e(b)(6)(A) and 685.209(f)(2)(ii),
        # (f)(3)(ii) cap the payment on all of the borrower's eligible loans at
        # their own 10-year standard payment; the Direct Loans carry the
        # (g)(1)(ii) part of that cap.
        balance_share = person(
            "income_driven_repayment_balance_share", period.this_year
        )
        balance = person("federal_student_loan_balance", period.this_year)
        eligible_balance = balance + person(
            "federal_ffel_loan_balance", period.this_year
        )
        direct_fraction = np.divide(
            balance,
            eligible_balance,
            out=np.zeros_like(balance),
            where=eligible_balance > 0,
        )
        standard_payment = person("income_driven_repayment_standard_payment", period)
        # (keep the existing comment on rounding to whole cents)
        prorated_payment = np.round(
            min_(
                rate * discretionary_income * balance_share,
                standard_payment * direct_fraction,
            ),
            2,
        )

Then delete the later balance = person(...) line (IBR :70, PAYE :73).

Expected values that move. A run of this fix on the full 186-case student-loan folder moves exactly these 8 cases, and all 186 pass with the new values:

File, case Head Fixed rule
IBR Case 17 at :423 (Case 18 after SA3) [300, 300] [250, 300]
IBR Case 18 at :453 (Case 19 after SA3) [340.21, 340.21] [250, 400]
PAYE Case 10 [300, 300] [250, 300]
PAYE Case 11 [340.21, 340.21] [250, 400]
Standard payment file Case 2 680.41 [250, 430.41]
Standard payment file Case 3 444.08 [111.02, 333.06]
Standard payment file Case 4 383.06 [333.06, 50]
Standard payment file Case 5 222.04 [222.04, 166.53]

The couple totals fall from $600 to $550 (Case 17) and from $680.42 to $650 (Case 18). Unchanged: standard payment file Cases 1 and 6, Parent PLUS Cases 1 and 4, IBR Cases 8 and 19-21, PAYE Cases 3, 4 and 12.

  • Other edits for C1:
    • Names with "capped together" or "couple's combined": IBR Cases 17-18, PAYE Cases 4 and 10-11. Standard payment file Case 2 becomes "each spouse has their own standard payment at their own rate" and Case 5 "each person's standard payment covers only their own loans" (a dependent's IBR and PAYE stay $0 through income_driven_repayment_eligible).
    • Comments: new arithmetic in IBR Cases 17-18 and PAYE Cases 10-11 (for example "300 each; min(300, 250) = 250 and min(300, 430.41) = 300"); "couple's cap" wording in IBR Cases 19-20 and PAYE Case 12 (values unchanged). IBR :195-196 and PAYE :81-82 become correct again.
    • Headers (IBR :1-8, PAYE :1-8, standard payment file :1-5), docs (income_driven_repayment_standard_payment.py:11-26, IBR :12-20, PAYE :12-19), the changelog and PR body item 4: the cap is the borrower's own 10-year payment on their Direct and FFEL loans, and the Direct Loans carry the (g)(1)(ii) part of it.

C2. The latest-return AGI variable cites RAP's definition for four plans that it does not govern, and it omits their definition.

  • Where: V/income_driven_repayment/income_driven_repayment_adjusted_gross_income.py:9-21 (docs) and :23-28 (references); T/income_driven_repayment/income_driven_repayment_adjusted_gross_income.yaml:1-2; PR body item 2.
  • Evidence:
    • The docs say that RAP and the IBR, SAVE, PAYE and ICR plans use as income what 1087e(q)(4)(A) defines. But 1087e(q)(4) opens "Definitions In this subsection:", and subsection (q) is RAP.
    • The definition that covers every IDR plan is 685.209(b)(10): "Income means either— (i) The borrower's and, if applicable, the spouse's Adjusted Gross Income (AGI) as reported to the Internal Revenue Service; or (ii) ... alternative documentation". It has the same text, unnumbered, in the version for the 2026-01 tests. No file in the PR cites it.
    • The latest-return timing for those plans rests on (b)(10)(i) with (l)(1)-(3) and (l)(6), and on 1098e(c)(2)(A)(i) and 1087e(e)(8)(A)(i). 26 U.S.C. 6103(l)(13)(A) leaves the tax year to ED.
    • The variable cites (e)(1)(i), which covers RAP, REPAYE, IBR and PAYE. ICR, which also reads the variable, is (e)(1)(ii).
  • Why critical: the review criteria treat missing or non-corroborating references and incorrect citations as critical. The docs name an authority that does not apply to four of the five plans, and the governing definition is missing. No value changes, so the fix is small.
  • Fix: references and docs. The docs below also fix the microsimulation sentence (in a microsimulation, inputs given only for 2024 still produce 2025 AGI of $51,984.86) and state the direction of the default.
    documentation = (
        "Adjusted gross income on the most recent federal income tax return "
        "available when the Department of Education sets the monthly payment. "
        "For the Repayment Assistance Plan, 20 U.S.C. 1087e(q)(4)(A) defines it "
        "as adjusted gross income 'for the most recent taxable year'. For the "
        "Income-Based Repayment, SAVE, Pay As You Earn and Income-Contingent "
        "Repayment plans, 34 CFR 685.209(b)(10)(i) defines income as adjusted "
        "gross income as reported to the IRS, which the Department takes from "
        "the latest return it obtains (685.209(l)(1)-(3), (l)(6)). Whose income "
        "counts follows 685.209(e)(1)(i), and (e)(1)(ii) for ICR. A borrower "
        "whose income fell can document current income instead. When not "
        "provided, this defaults to the current year's adjusted gross income, "
        "because household inputs usually cover only the simulated year. The "
        "default overstates payments for borrowers whose income rose since "
        "their last return. An explicit input overrides it."
    )
    definition_period = YEAR
    reference = (
        "https://www.law.cornell.edu/uscode/text/20/1087e#q_4_A",
        "https://www.ecfr.gov/current/title-34/section-685.209#p-685.209(b)(10)",
        "https://www.ecfr.gov/current/title-34/section-685.209#p-685.209(e)(1)(i)",
        "https://www.ecfr.gov/current/title-34/section-685.209#p-685.209(e)(1)(ii)",
        "https://www.ecfr.gov/current/title-34/section-685.209#p-685.209(l)(4)",
        "https://www.ecfr.gov/current/title-34/section-685.209#p-685.209(l)(6)",
    )

Make the same split in the test header ("1087e(q)(4)(A) for RAP; 34 CFR 685.209(b)(10) for IBR, SAVE, PAYE and ICR") and in PR body item 2.

C3. The new Parent PLUS payment amortizes consolidated Parent PLUS loans over 10 years, but their standard plan runs 10 to 30 years.

  • Where: V/standard_repayment/parent_plus_standard_repayment_payment.py:11-15, 31, 36-44 (always p.repayment_period, 10 years; only reference 685.208(b)(1)); V/federal_parent_plus_loan_balance.py:9-14 (the input includes "Direct Consolidation Loans that repaid such loans", and the PR adds that this payment "repays them under the standard plan").
  • Evidence:
    • 685.208(b)(1) covers Direct Consolidation Loan borrowers only if they "entered repayment before July 1, 2006".
    • 685.208(b)(2) is the standard plan "for Direct Consolidation Loan borrowers entering repayment on or after July 1, 2006". Under (b)(2)(iii), the term follows the total of the consolidation loan and the borrower's other student loans: under $7,500, 10 years; $7,500, 12; $10,000, 15; $20,000, 20; $40,000, 25; $60,000 or more, 30.
    • 685.220(i)(2)(ii) applies that rule to these consolidations, and (i)(3)(i) limits the other loans counted to the consolidation amount. The text before 2026-07-01 was the same (685.208(c) and (j)).
    • Values at 6%: $30,000 gives $333.06 in the model and $214.93 over 20 years, $118.13 (55%) too high. $15,000 gives $166.53 against $126.58, $50,000 gives $555.10 against $322.15, and $80,000 gives $888.16 against $479.64.
  • Why critical: a value mismatch, confirmed on the code path, in a variable this PR adds, for a loan class its own input docs include. It contradicts a clear rule, and "Not modeled" lists only consolidated Parent PLUS under ICR. The same gap in standard_repayment_payment on main is older code (see Out of scope).
  • Fix:
    1. Add a Person, YEAR boolean input federal_parent_plus_loan_is_consolidated (default false): true when the Parent PLUS balance is a Direct Consolidation Loan that entered repayment from 2006-07-01 to 2026-06-30.
    2. Add gov.ed.student_loans.standard_repayment.consolidation_repayment_period, a single_amount scale with thresholds 0, 7,500, 10,000, 20,000, 40,000 and 60,000 and amounts 10, 12, 15, 20, 25 and 30 years, from 2006-07-01. Cite 685.208(b)(2)(iii), 685.220(i)(2)(ii) and (i)(3)(i). The default right=False matches "equal to or greater than".
    3. In the formula, use that term when the flag is true. Base the tier on the Parent PLUS balance plus the person's other education loans (federal_student_loan_balance + federal_ffel_loan_balance), with the other loans capped at the Parent PLUS balance.
    4. Tests at 6%, consolidated, no other loans: $30,000 gives 214.93; $19,999.99 gives 168.77 (15 years); $20,000 gives 143.29 (20 years); $59,999.99 gives 386.58 (25 years); $60,000 gives 359.73 (30 years). Unconsolidated $30,000 stays 333.06.
    5. Fix the docs in both files to match. Docs alone do not clear this item, because the value stays wrong for an input the variable says it covers.

Should Address

SA1. Five FFEL and excepted-loan edge cases in the proration and RAP have no test.
Each change below passes all 186 cases at the head (except (c), caught once by accident), on a path this PR adds or makes reachable. The five cases pass at the head and under the C1 fix, fail on the change they pin, and do not depend on the couple cap.

  • (a) A tax dependent's FFEL loans. V/income_driven_repayment/income_driven_repayment_balance_share.py:36-38 and V/repayment_assistance_plan/repayment_assistance_plan_payment.py:41 keep a dependent's FFEL out of the couple's balance ((e)(2)(i)). Without the gate, a parent with $20,000 Direct and a dependent with $15,000 FFEL gets share 0.571 instead of 1.
  • (b) A spouse's Parent PLUS loans. (b)(5)(i)-(ii) exclude them from eligible loans. Adding them to the couple's balance changes Case 8 below from 1 to 0.4, and RAP from 550 to 220.
  • (c) Married filing separately with FFEL. income_driven_repayment_balance_share.py:19-20 states the rule, but only IBR Case 21 catches a share summed over the marital unit. It catches it by accident: its two single filers share one auto-built marital unit.
  • (d) RAP eligibility of an FFEL-only borrower. V/repayment_assistance_plan/repayment_assistance_plan_eligible.py:25-33 is unchanged and correct ((c)(6), (d)(4)). Counting FFEL there changes the eligibility flag and the applicable payment ($0 to $250). The payment stays $0 only because of the Direct-balance cap, so the two safeguards hide each other.
  • (e) RAP final-payment cap. V/repayment_assistance_plan/repayment_assistance_plan_payment.py:58 caps at the Direct balance only, as (q)(4)(B)(iii) and (d)(4) require. A cap on Direct + FFEL turns January's $100 into $250.
  • Why should address: missing edge cases for already-tested variables.
  • Fix: append to T/income_driven_repayment/income_driven_repayment_balance_share.yaml (Case 7 pins (c); Case 8 pins (a) and (b)):
- name: Case 7, married filing separately counts only the person's own Direct and FFEL loans.
  period: 2026
  absolute_error_margin: 0.0001
  input:
    people:
      person1:
        age: 40
        federal_student_loan_balance: 10_000
      person2:
        age: 38
        federal_student_loan_balance: 20_000
        federal_ffel_loan_balance: 30_000
    tax_units:
      tax_unit1:
        members: [person1]
        filing_status: SEPARATE
      tax_unit2:
        members: [person2]
        filing_status: SEPARATE
    marital_units:
      marital_unit:
        members: [person1, person2]
  output:
    # Separate returns: the spouse's income is not used, so the spouse's loans
    # are not included either (685.209(e)(1)(i)(A), (e)(2)(i)) and there is no
    # (g)(1)(i) proration. person1: 10,000 / 10,000 = 1. person2, (g)(1)(ii)
    # alone: 20,000 / (20,000 + 30,000) = 0.4.
    income_driven_repayment_balance_share: [1, 0.4]

- name: Case 8, a spouse's Parent PLUS loans and a tax dependent's FFEL loans do not dilute the head's share.
  period: 2026
  absolute_error_margin: 0.0001
  input:
    people:
      person1:
        age: 50
        is_tax_unit_head: true
        is_tax_unit_spouse: false
        federal_student_loan_balance: 20_000
      person2:
        age: 48
        is_tax_unit_head: false
        is_tax_unit_spouse: true
        federal_parent_plus_loan_balance: 30_000
      person3:
        # An adult relative claimed as a dependent, with FFEL loans made
        # before the FFEL Program stopped new loans on June 30, 2010.
        age: 40
        is_tax_unit_head: false
        is_tax_unit_spouse: false
        federal_ffel_loan_balance: 15_000
    tax_units:
      tax_unit:
        members: [person1, person2, person3]
        filing_status: JOINT
  output:
    # Parent PLUS loans are not eligible loans (685.209(b)(5)(i)-(ii)), and
    # only a spouse's loans join the borrower's (685.209(e)(2)(i)), so the
    # couple's eligible balance is person1's 20,000: 20,000 / 20,000 = 1.
    income_driven_repayment_balance_share: [1, 0, 0]

Append to T/repayment_assistance_plan/repayment_assistance_plan_payment.yaml (Case 24 pins (a) and (b); Case 25 pins (e)):

- name: Case 24, a spouse's Parent PLUS loans and a tax dependent's FFEL loans do not dilute the borrower's share.
  period: 2027-01
  absolute_error_margin: 0.01
  input:
    people:
      person1:
        age: 50
        is_tax_unit_head: true
        is_tax_unit_spouse: false
        federal_student_loan_balance: 20_000
      person2:
        age: 48
        is_tax_unit_head: false
        is_tax_unit_spouse: true
        federal_parent_plus_loan_balance: 30_000
      person3:
        # An adult relative claimed as a dependent (FFEL loans before 2010).
        age: 40
        is_tax_unit_head: false
        is_tax_unit_spouse: false
        federal_ffel_loan_balance: 15_000
    tax_units:
      tax_unit:
        members: [person1, person2, person3]
        filing_status: JOINT
        adjusted_gross_income: 90_000
  output:
    # 8% x 90,000 = 7,200 / 12 = 600; 600 - 1 x 50 = 550. Parent PLUS loans
    # are not eligible loans (685.209(b)(5)(i)-(ii)), and only a spouse's loans
    # join the borrower's (685.209(e)(2)(i), (g)(3)(i)), so person1's share is
    # 20,000 / 20,000 = 1.
    repayment_assistance_plan_eligible: [true, false, false]
    repayment_assistance_plan_applicable_monthly_payment: 550
    repayment_assistance_plan_payment: [550, 0, 0]

- name: Case 25, the final-payment cap uses the Direct Loan balance, not FFEL loans the plan cannot repay.
  period: 2027
  absolute_error_margin: 0.01
  input:
    people:
      person1:
        age: 40
        federal_student_loan_balance: 100
        federal_ffel_loan_balance: 30_000
    tax_units:
      tax_unit:
        members: [person1]
        filing_status: SINGLE
        adjusted_gross_income: 60_000
  output:
    # 5% x 60,000 = 3,000 / 12 = 250. The plan repays only the 100 of Direct
    # Loans (685.209(d)(4)), so January pays the 100 left to repay
    # ((q)(4)(B)(iii)) and later months pay 0.
    repayment_assistance_plan_payment:
      2027-01: 100
      2027-02: 0
      2027: 100

Append to T/repayment_assistance_plan/repayment_assistance_plan_eligible.yaml (pins (d)):

- name: Case 6, a borrower with only FFEL loans is not eligible.
  period: 2027-01
  input:
    people:
      person1:
        age: 40
        federal_ffel_loan_balance: 30_000
    tax_units:
      tax_unit:
        members: [person1]
  output:
    # The plan repays only Direct Loans (685.209(c)(6), (d)(4); 20 U.S.C.
    # 1087e(q)(1)(A)). FFEL loans count only in the spousal proration
    # (685.209(b)(5)(ii), (g)(3)(i)).
    repayment_assistance_plan_eligible: false

SA2. Four IBR and PAYE edge cases have no test.
Each change below passes all 186 cases at the head, except that one unrelated Parent PLUS assertion catches (b). The four cases pass at the head and under the C1 fix, and each fails on its change at both.

  • (a) A spouse barred by a loan made on or after July 1, 2026 still counts in the proration. Their Direct Loans stay eligible loans ((b)(5) has no date limit); (d)(5) only stops the plan from repaying them. Dropping the barred spouse from the denominator doubles person1's IBR from $166.96 to $333.91 at the head ($333.06 under C1).
  • (b) Joint filers where only one spouse borrowed, the most common married-borrower case. Without the cap at the eligible balance (income_driven_repayment_standard_payment.py:50-52), the non-borrower adds $50 and IBR and PAYE become $383.06 instead of $333.06. Only a Parent PLUS assertion catches this today, by chance.
  • (c) The final cap at the Direct balance (IBR :71, PAYE :74) never binds in a test. Without it, a $8 balance pays $10.
  • (d) PAYE's cent rounding (V/pay_as_you_earn/pay_as_you_earn_payment.py:66). Removing np.round passes every case, because PAYE Case 9 is exactly 5.0. The docs (:26-30) describe the choice that $4.996 becomes $10.
  • Why should address: missing edge cases for already-tested variables. (c) and (d) predate the PR, but the PR rewrote these lines and its docs now state the behavior, so they sit at the low end.
  • Fix: append to the standard payment file (pins (b)). The first output assumes C1's Person entity. Before the fix it is the scalar 333.06.
- name: Case 7, joint filers where only one spouse borrowed are capped at that spouse's standard payment.
  period: 2026-01
  absolute_error_margin: 0.01
  input:
    people:
      person1:
        age: 40
        federal_student_loan_balance: 30_000
        federal_student_loan_interest_rate: 0.06
      person2:
        age: 38
    tax_units:
      tax_unit:
        members: [person1, person2]
        filing_status: JOINT
        adjusted_gross_income: 150_000
    households:
      household:
        members: [person1, person2]
        state_code: TX
  output:
    # The spouse has no eligible loans, so their standard payment is
    # min(max(0, 50), 0) = 0, not the 50 dollar minimum. IBR 15% and PAYE 10%
    # of (150,000 - 32,460) / 12 are 1,469.25 and 979.50, above 333.06.
    income_driven_repayment_standard_payment: [333.06, 0]
    income_based_repayment_payment: [333.06, 0]
    pay_as_you_earn_payment: [333.06, 0]

Append to T/income_based_repayment/income_based_repayment_payment.yaml after the SA3 renumbering (Case 23 pins (c) for both plans; Case 24 pins (a)):

- name: Case 23, a Direct Loan balance under 10 dollars caps the 10 dollar minimum payment.
  period: 2026-01
  absolute_error_margin: 0.01
  input:
    people:
      person1:
        age: 35
        federal_student_loan_balance: 8
        federal_student_loan_interest_rate: 0.06
    tax_units:
      tax_unit:
        members: [person1]
        filing_status: SINGLE
        adjusted_gross_income: 100_000
    households:
      household:
        members: [person1]
        state_code: TX
  output:
    # Standard payment min(max(0.09, 50), 8) = 8. IBR min(950.75, 8) and PAYE
    # min(633.83, 8) give 8, which 685.209(g)(1)(iii)(B) raises to 10; the
    # 8 dollar Direct Loan balance caps both at 8.
    income_based_repayment_payment: 8
    pay_as_you_earn_payment: 8

- name: Case 24, a spouse barred by a loan made on or after July 1, 2026 still counts in the proration.
  period: 2027-01
  absolute_error_margin: 0.01
  input:
    people:
      person1:
        age: 40
        federal_student_loan_balance: 30_000
        federal_student_loan_interest_rate: 0.06
      person2:
        age: 38
        federal_student_loan_balance: 30_000
        federal_student_loan_interest_rate: 0.06
        has_federal_student_loan_made_on_or_after_2026_07_01: true
    tax_units:
      tax_unit:
        members: [person1, person2]
        filing_status: JOINT
        adjusted_gross_income: 60_000
    households:
      household:
        members: [person1, person2]
        state_code: TX
  output:
    # The spouse's Direct Loans stay eligible loans (685.209(b)(5)); (d)(5)
    # only stops the plan from repaying them. 1.5 x 22,191.28 (2027 guideline
    # for two) = 33,286.92; (60,000 - 33,286.92) / 12 = 2,226.09. IBR 15% =
    # 333.91 and PAYE 10% = 222.61, each x 30,000 / 60,000; the spouse owes 0.
    income_based_repayment_payment: [166.96, 0]
    pay_as_you_earn_payment: [111.30, 0]

Append to T/pay_as_you_earn/pay_as_you_earn_payment.yaml (pins (d)):

- name: Case 13, a payment within half a cent below 5 dollars rounds to 5 and rises to 10.
  period: 2026-01
  absolute_error_margin: 0.001
  input:
    people:
      person1:
        age: 35
        federal_student_loan_balance: 30_000
        federal_student_loan_interest_rate: 0.06
    tax_units:
      tax_unit:
        members: [person1]
        filing_status: SINGLE
        adjusted_gross_income: 24_539.52
    households:
      household:
        members: [person1]
        state_code: TX
  output:
    # 24,539.52 - 23,940 = 599.52; 10% x 599.52 / 12 = 4.996, which rounds to
    # 5.00 and rises to 10 (the modeling choice in the documentation).
    pay_as_you_earn_payment: 10

If you later drop the round-before-threshold choice, pin the rounding with PAYE Case 1's inputs and absolute_error_margin: 0.001 instead (expected 133.83).

SA3. Two IBR cases are named "Case 17".

  • Where: T/income_based_repayment/income_based_repayment_payment.yaml:400 ("Case 17, a payment one dollar of income below 5 dollars is zero.", from #10152) and :423 (this PR's "Case 17, spouses with different interest rates ..."), then Cases 18-21.
  • Evidence: the merge b59f7ab4a5 kept both numberings. #10152 inserted a new Case 15 and shifted its old 15-16 to 16-17, while this PR had already numbered its cases 17-21. No comment or body text refers to these case numbers.
  • Why should address: test naming, created by this PR. The effect is cosmetic, because the runner prints full names, so it sits at the low end.
  • Fix: renumber the PR's cases to 18-22 before you add SA2's Cases 23-24:
sed -i '' -e '423s/- name: Case 17,/- name: Case 18,/' -e '453s/- name: Case 18,/- name: Case 19,/' -e '481s/- name: Case 19,/- name: Case 20,/' -e '510s/- name: Case 20,/- name: Case 21,/' -e '540s/- name: Case 21,/- name: Case 22,/' policyengine_us/tests/policy/baseline/gov/ed/student_loans/income_based_repayment/income_based_repayment_payment.yaml

Suggestions

G1. Statute links. The uscode.house.gov view.xhtml?req=granuleid:... links return HTTP 404 today, and the PR adds two more (income_driven_repayment_adjusted_gross_income.py:24, parent_plus_standard_repayment_payment.py:30). Both also cite the whole of 1087e. For new references, use paragraph links: https://uscode.house.gov/us/usc/t20/s1087e/#q-4-A or https://www.law.cornell.edu/uscode/text/20/1087e#q_4_A, and (d)(1)(A) for the Parent PLUS payment. About 64 repository files use the old form, so a repo-wide fix belongs in a separate PR.

G2. Paragraph cites to add or tighten.

  • V/federal_ffel_loan_balance.py: add #p-685.209(g)(3), and (f)(2)(ii)/(f)(3)(ii) after C1; the docs rely on both.
  • V/income_driven_repayment/income_driven_repayment_eligible.py:19-22: the docs cite (c)(2)-(5), the tuple only (d)(1)-(2). Add (c)(3)-(c)(5), and (d)(3) for ICR loans.
  • "20 U.S.C. 1087e(d)(6)-(7)" (Parent PLUS :45, older IBR and PAYE lines): the borrower-level bar is in the (d)(1) chapeau and (d)(7)(A).
  • V/federal_student_loan_interest_rate.py: the docs apply the rate to FFEL and Parent PLUS, the references cover Direct Loans. Cite 34 CFR 682.202(a), or call one rate per person a simplification, as the PR body does.

G3. AGI default. C2's text states the direction. Also say that the RAP dependent count and family size come from current-year inputs, so an AGI override from an earlier return mixes years. The size, for one year of model AGI growth (3.7%): RAP +$22.09 a month on average (up to +$47.59), IBR up to +$22.31. See Out of scope for the dataset follow-up.

G4. Record why the RAP share differs from the IBR share. Case 22 pins the literal (g)(3)(i) reading: $125 here against $31.25 under an IBR-like reading. Cite the proposed rule (91 FR 4284-85) and the RISE preamble (#page=71), which copy the IBR (g)(1)(i) approach "to provide consistency with IBR" and do not discuss FFEL. Revisit the reading if ED publishes servicing guidance.

G5. Shared helpers. The couple's Direct + FFEL balance is computed in income_driven_repayment_balance_share.py, repayment_assistance_plan_payment.py and the standard payment. min_(max_(amortized, minimum), balance) appears in four files, and the round, small-payment and cap block in IBR, PAYE and SAVE. One helper each would keep the rules in step.

G6. Uprating exemption guard. tools/default_uprating.py:106-107 says "Loan balances", but mortgage and auto loan balances stay uprated. Write "Federal student loan balances". Add a pytest assertion that each name in NO_DEFAULT_UPRATING_VARIABLES exists, has uprating is None and is a STOCK. Today, removing a name from the set leaves all 141 pytest cases passing, and only the YAML carry-forward cases catch it.

G7. Two doc sentences. Copy PAYE's sentence on the $4.995 rounding band (pay_as_you_earn_payment.py:26-30) into the IBR docs, which run the same code. State in the standard payment docs that one $50 minimum covers the Direct and FFEL loans together (see the FFEL $600 item under Investigated and cleared).

G8. programs.yaml. The SAVE note (:1636) omits "tax-dependent borrowers treated as ineligible", which the other IDR notes now carry. No entry names standard_repayment_payment or the new parent_plus_standard_repayment_payment. Consider a standard_repayment entry.

G9. Changelog type. The PR adds four public variables. An added fragment (changelog.d/student-loan-idr-restrictions.added.md) would make the release a minor bump. The fixed fragment is also defensible.

G10. Marital units in two test groups. RAP base payment Cases 10-16 (E1-E7) and IBR Case 21 list two unrelated single filers without marital_units. The builder puts both in one two-person marital unit. The values do not change, but give each person their own marital unit so the cases model what their names say.

G11. The July 1, 2026 bar works by year. The Parent PLUS payment extends #10152's pattern: a flag set for 2026 moves all of 2026 to the tiered plan, which then pays from January. The two plans never both pay. Document this, or gate the plans with a parameter in effect from 2026-07-01, as RAP's in_effect does.

G12. A one-unit couple forced to SEPARATE. If a user sets filing_status: SEPARATE on a one-unit couple, the spouse's income and loans count (RAP [500, 0] instead of [100, 0]). The model never builds this unit itself. Gating the spouse's balance on a joint return would make the (e)(2)(i) condition explicit.

Investigated and cleared

  • A separate FFEL minimum of $600 a year (34 CFR 682.209(c)(1)(i)). Rejected. It applies per borrower across that borrower's FFEL holders and yields to an income-based plan; the statute's couple-level minimum was struck in 1992 (Pub. L. 102-325 sec. 416(d)). The (f)(2)(ii)/(f)(3)(ii) amount is one 10-year plan on all eligible loans (682.215(d)(1)(i) counts Direct and FFEL together) with one $50 minimum (685.208(b)(1)(ii)). $2,000 Direct and $2,000 FFEL at 6% give a $50 standard payment and $25 IBR, at the head and under the C1 fix.
  • Cites: (b)(6) on federal_ffel_loan_balance is the excepted-consolidation definition the docs exclude. The (c)(2)-(5) and (c)(6) cites in the eligibility docs are accurate: no eligibility paragraph excludes tax dependents.
  • RAP: the literal (g)(3)(i) share in Case 22 follows the text ((b)(5) defines eligible loans for (g); (g)(3) has no (g)(1)(ii) step). The head-or-spouse gate is required, because an FFEL-only spouse's debt counts. ICR stays unprorated, because (g)(1) covers only (f)(1)-(3).
  • Married filing separately as two tax units is correct (IBR Case 9, PAYE Case 7, RAP integration Case 2).
  • The current-year AGI default is a documented proxy with an input override and the clean_vehicle_credit_prior_year_magi precedent; its population effect today is nil. A split AGI read inside the RAP base payment is not plausible: one agi read feeds both the rate and the $10,000 test (V/repayment_assistance_plan/repayment_assistance_plan_base_payment.py:27-30).
  • Uprating exemption design: core has no variable-level "no uprating" flag, so a name set in default_uprating.py fits. The three balances are the only float USD stocks without an uprater. student_loan_interest keeps CPI-U, which is right for a flow.
  • /current/ eCFR anchors against 2026-01 test periods: same substance in both versions.
  • PR-body figures reproduce ($62.50, $16.67, $86.06, $50.19, $111.02, $25.09, $333.06, $253.16, $31,626, $33,271, $30,000). The $300 + $300 and $340.21 figures in item 4 are C1.
  • Partner contract tests: none touched.

Out of scope / follow-up

  • Direct consolidations on main. standard_repayment_payment amortizes the Direct Consolidation Loans in federal_student_loan_balance over 10 years. Those that entered repayment after 2006-06-30 use the (b)(2) term (same class as C3). Open an issue.
  • Item 3, tax-dependent borrowers (C17): deferred by the PR to a follow-up that moves the plan calculations to a per-borrower unit.
  • AGI in microsimulation. When microcosm#1246 and microcosm#1357 write the balances, dataset runs will have prior-year AGI. Then read adjusted_gross_income at period.last_year when simulation.is_over_dataset, and keep the current-year default for households. From then on, the default would be a biased microsimulation default.
  • Downstream targets. PolicyEngine/chronicle maps FSA FY2026 targets to federal_student_loan_balance; without uprating, its target year must match the dataset year.
  • Other debts (mortgage and auto loan balances) remain uprated. That is a separate modeling decision.

Validation Summary

Check Result
CI 34/34 pass at cb6363a7fa
Local tests 17 changed YAML files 146/146; full student-loan folder (23 files) 186/186; test_uprating_extensions.py 141/141
Partner contract tests None touched
References 20 hrefs added; all 33 eCFR anchors resolve; PDF pages correct. Non-corroborating or missing: C1, C2, C3. house.gov links 404 (G1)
Regulatory FFEL proration, AGI wiring, order of operations, Parent PLUS bar: correct. Couple cap (C1) and consolidation term (C3): wrong
Code patterns No hard-coded values; vectorized; entities and periods correct; ruff clean; changelog present and top level (G9)
Tests 35 hand checks, all correct. 26/26 sampled rulespec-us#1455 cases match; C17 deferred
Mutation About 110 mutants. Each PR behavior change is pinned (for example, dropping FFEL from the IBR/PAYE/SAVE denominator fails 7 cases; the old per-person cap fails 4). The survivors are the SA1 and SA2 gaps. Each proposed case fails on its mutant
C1 fix run The per-borrower cap moves exactly the 8 cases in the C1 table; 186/186 with the new values. The 9 new cases pass at the head and under the fix
Microsimulation No dataset writes the three balances (policyengine-us-data 42ed5d45, microcosm 5810fce). Every payment is $0 in microdata today, so the population effect is nil. microcosm#1246 and microcosm#1357 are drafts that wait on this PR
Branch 7 ahead, 4 behind main; no overlap in student-loan files; GitHub merge state UNKNOWN
PR body axiom: rulespec-us#1455 queued is present (process note only, not counted). Items 2 and 4 need updates for C2 and C1
Other reviews None on the PR

Review Severity: REQUEST_CHANGES

Three critical items (C1-C3) and three should-address items (SA1-SA3) are open. Under Pavel's criteria, any open C or SA means REQUEST_CHANGES.

Next Steps

To auto-fix issues: run the fix-pr workflow for this PR.

  1. C1 (run SA3's line-number command first if you use it): make income_driven_repayment_standard_payment a Person amount and cap IBR and PAYE after the proration. Update the 8 expected values in the C1 table, the names, comments, headers, docs and references, the changelog and PR body item 4. Correct #10411 item 4.
  2. C2: replace the AGI variable's docs and references as given, and update its test header and PR body item 2.
  3. C3: add the consolidation flag and the (b)(2)(iii) term scale, use them in parent_plus_standard_repayment_payment, add the tier tests, and fix the docs of both files. Open an issue for the same gap in standard_repayment_payment.
  4. SA3: renumber IBR Cases 17-21 (from :423) to 18-22.
  5. SA1: add the five cases (balance share Cases 7-8, RAP payment Cases 24-25, RAP eligibility Case 6).
  6. SA2: add the four cases (standard payment file Case 7 with the fixed-rule value, IBR Cases 23-24, PAYE Case 13).
  7. Take the suggestions that you agree with (G1-G12).
  8. Run make format. Then run policyengine-core test policyengine_us/tests/policy/baseline/gov/ed/student_loans -c policyengine_us (195 cases plus the C3 tests) and pytest policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py.

DTrim99 and others added 2 commits October 27, 2026 09:16
Brings in #10453 (poverty guideline uprating), which also edits
test_uprating_extensions.py and programs.yaml in other places; both
merged automatically.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqCGPgxDukji8Bhs57c5rH
…idated parent PLUS loans by amount

Address the review of #10415:
- 20 U.S.C. 1098e(b)(6)(A) caps the payment "for all loans made to the
  borrower" at that borrower's 10-year standard payment, and 34 CFR
  685.209(e)(2)(i) brings a spouse's loans into the (g) proration only.
  income_driven_repayment_standard_payment is now a Person amount on the
  person's own Direct and FFEL loans; IBR and PAYE prorate first and cap
  at the Direct Loan part of that amount.
- income_driven_repayment_adjusted_gross_income cites 685.209(b)(10) for
  the IBR, SAVE, PAYE and ICR plans and keeps 1087e(q)(4)(A) for RAP.
- A Direct Consolidation Loan that repaid parent PLUS loans and entered
  repayment on or after July 1, 2006 repays over 10 to 30 years
  (685.208(b)(2)(iii); 685.220(i)(2)(ii), (i)(3)(i)): new input
  federal_parent_plus_loan_is_consolidated and parameter
  standard_repayment.consolidation_repayment_period.
- New FFEL, Parent PLUS, dependent, MFS, barred-spouse, final-payment
  and rounding edge cases; IBR cases renumbered 18-22.
- Paragraph-level statute links, tighter cites, a guard test for the
  uprating exemption, programs.yaml notes, and an added changelog type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqCGPgxDukji8Bhs57c5rH
@DTrim99

DTrim99 commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

@PavelMakarchuk thanks for the review. Everything is in fa6ac3a, which also merges main through 14c6413e4a (#10453's poverty guideline uprating; no student loan file overlaps, and both shared files merged cleanly).

Critical

C1, per-borrower cap: fixed. 1098e(b)(6)(A) ("for all loans made to the borrower under part B or D") settles it, and (e)(2)(i) keeps the spouse's debt in (g). income_driven_repayment_standard_payment is now a Person amount on the person's own Direct + FFEL loans (one $50 minimum, capped at the eligible balance), cited to 1098e#b_6_A. IBR and PAYE compute min(rate × DI × share, S_own × Direct / (Direct + FFEL)) as you proposed, then round, the small-payment rule, the Direct-balance cap and the July 2026 bar. The 8 cases moved exactly as in your table, hand-checked:

Case Was Now Arithmetic
IBR 18 (was 17) [300, 300] [250, 300] 600 × ½ = 300 each; min(300, 250), min(300, 430.41)
IBR 19 (was 18) [340.21, 340.21] [250, 400] 800 × ½ = 400 each; min(400, 250), min(400, 430.41)
PAYE 10 [300, 300] [250, 300] as IBR 18 at 10% of 72,000 / 12
PAYE 11 [340.21, 340.21] [250, 400] as IBR 19 at 10% of 96,000 / 12
Standard payment 2 680.41 [250, 430.41] 30,000 / 120; 30,000 × 0.01 / (1 − 1.01^−120)
Standard payment 3 444.08 [111.02, 333.06] 10,000 and 30,000 at 6% over 120 months
Standard payment 4 383.06 [333.06, 50] max(33.31, 50)
Standard payment 5 222.04 [222.04, 166.53] the dependent's own 15,000; their IBR and PAYE stay $0

Unchanged values, rewritten comments: IBR 8 and 20-22, PAYE 3, 4 (now "each spouse's own standard payment caps..."; 783.60 and 195.90 capped at 444.08 and 111.02) and 12. Names, headers, docs, programs.yaml, the changelog and PR item 4 now describe the borrower's own cap. I posted the item 4 correction on #10411.

C2, AGI references: fixed. The docs now split the sources: 1087e(q)(4)(A) for RAP, and 685.209(b)(10)(i) for IBR, SAVE, PAYE and ICR, with (l)(1)-(3), 1098e(c)(2)(A)(i), (e)(1)(i), (e)(1)(ii) for ICR, and (l)(6). They state the direction of the default (it overstates payments when income rose) and, from G3, that family size and RAP's dependent count stay current-year. References: 1087e#q_4_A, 1098e#c_2_A_i, (b)(10), (e)(1)(i), (e)(1)(ii), (l)(1), (l)(6). The test header and PR item 2 match. No values change.

C3, consolidated Parent PLUS term: fixed. New Person input federal_parent_plus_loan_is_consolidated (a Direct Consolidation Loan that entered repayment from July 1, 2006 through June 30, 2026) and parameter gov.ed.student_loans.standard_repayment.consolidation_repayment_period, a single_amount scale from 2006-07-01 with thresholds 0 / 7,500 / 10,000 / 20,000 / 40,000 / 60,000 and 10 / 12 / 15 / 20 / 25 / 30 years. It cites 685.208(b)(2)(iii), 685.208(c) and (j) as of 2016-11-17, and 685.220(i)(2)(ii), (i)(3)(i). The tier base is the Parent PLUS balance plus federal_student_loan_balance + federal_ffel_loan_balance, the latter capped at the Parent PLUS balance. New cases at 6%, all hand-checked against balance × 0.005 / (1 − 1.005^−n):

  • Case 8, consolidated, no other loans: 30,000 → 214.93 (240 months); 19,999.99 → 168.77 (180); 20,000 → 143.29 (240); 59,999.99 → 386.58 (300); 60,000 → 359.73 (360).
  • Case 9: 15,000 consolidated + 50,000 Direct → base 15,000 + 15,000 = 30,000, 20 years, 107.46 (89.93 if the cap were ignored); 15,000 + 5,000 FFEL → 20,000, 20 years, 107.46 (126.58 without the FFEL).
  • Unconsolidated 30,000 stays 333.06 (Case 1).

Docs in both files updated. The same gap in standard_repayment_payment (main code) is #10485.

Should address

SA1, five FFEL and excepted-loan cases: added as proposed. Balance share Case 7 ([1, 0.4]) and Case 8 ([1, 0, 0]); RAP payment Case 24 (eligible [true, false, false], A = 8% × 90,000 / 12 − 50 = 550, P [550, 0, 0]) and Case 25 (January 100, February 0, 2027 total 100); RAP eligibility Case 6 (FFEL-only borrower not eligible; I cited 1087e(d)(1)(F), (q)(1) rather than (q)(1)(A)).

SA2, four IBR and PAYE cases: added.

  • (b) Standard payment Case 7 with the Person values: [333.06, 0] for the standard payment, IBR and PAYE.
  • (c) IBR Case 23: an $8 balance gives 8 for IBR and PAYE.
  • (d) PAYE Case 13: AGI 24,539.52 gives 4.996, which rounds to 5.00 and becomes 10.
  • (a) IBR Case 24, one change: after the merge the projected 2027 guideline for two is 22,272.91 (it was 22,191.28 at your head), so your 166.96 / 111.30 would now be 166.19 / 110.79. To keep the case off the projection, it sets income_driven_repayment_poverty_guideline: 20_000. (60,000 − 30,000) / 12 = 2,500; IBR 15% × 2,500 × ½ = 187.50 and PAYE 10% × 2,500 × ½ = 125; the barred spouse owes 0.

SA3, duplicate Case 17: fixed. This PR's IBR cases are now 18-22, and the new ones 23-24.

Suggestions

  • G1: taken for the references this PR adds or edits. AGI variable → 1087e#q_4_A, Parent PLUS → 1087e#d_1_A, IBR and the standard payment → 1098e#b_6_A (the IBR file's dead house.gov link was replaced). The repo-wide sweep is left for a separate PR, as you suggested.
  • G2: taken. federal_ffel_loan_balance adds (g)(3), (f)(2)(ii) and (f)(3)(ii). income_driven_repayment_eligible adds (c)(3)-(5) and (d)(3). The bar comments in IBR, PAYE and Parent PLUS now cite 1087e(d)(1) and (d)(7)(A). federal_student_loan_interest_rate calls the single rate a simplification and cites 682.202(a).
  • G3: taken (see C2).
  • G4: taken. The RAP payment docs cite 91 FR 4284-85 and the final rule's preamble at 91 FR 23838 (PDF page 71).
  • G5, shared helpers: deferred. Merging the balance and round/cap blocks across RAP, IBR, PAYE and SAVE is a refactor with no value change, and it would grow an already large diff. It fits better in the per-borrower-unit follow-up for item 3, which rewrites those blocks anyway.
  • G6: taken. The comment now says "Federal student loan balances" and notes that other debts keep their uprating. New test_held_balances_are_unuprated_stocks in test_uprating_extensions.py checks that each name exists, has uprating is None and is a stock. It reads the already-imported system, so it adds no model build.
  • G7: taken. The IBR docs have PAYE's $4.995 sentence, and the standard payment docs say one $50 minimum covers Direct and FFEL together.
  • G8: partly taken. The SAVE note now says tax-dependent borrowers are treated as ineligible, and the IBR and PAYE notes describe the borrower's own cap. A standard_repayment registry entry is deferred: the 10-year plan predates this PR, and a new coverage-page entry should be its own change.
  • G9: taken. The fragment is now student-loan-idr-restrictions.added.md, since the PR adds five public variables and a parameter.
  • G10: taken. RAP base payment Cases 10-16 and IBR Case 22 give each single filer their own marital unit.
  • G11: documented. The Parent PLUS docs say the flag is yearly, so a loan made in the second half of 2026 moves all of 2026 to the tiered plan. A parameter gate would also have to cover the 10-year and tiered payments on main, so I left that for a later change.
  • G12, a one-unit couple forced to SEPARATE: deferred. The model never builds this unit. Gating the spouse's balance on a joint return would touch the balance share, RAP and both eligibility paths for an input the household builder doesn't produce. Better handled with the per-borrower unit in the item 3 follow-up.

Other

  • Axiom: the PR line now names After model name change, cannot execute the CLI test capability #1455 (1098e, 1087e(q)) and Improve performance (Core + SNAP EAs) #1711 (685.208, 685.209 for PAYE, ICR and the standard plans), both open and labelled pe-parity. Dispatch payloads for this head, with verbatim law, the per-borrower reading and these companion cases, are on #1455 and #1711.
  • Tests run locally at fa6ac3a, one file group per process: all 23 student loan YAML files, 197 cases passed; test_uprating_extensions.py and test_programs_registry.py, 943 passed. In the code-health files I ran, 269 passed; test_source_conflicts.py::test_unscannable_files_fail_closed fails on local Python 3.11 (a tokenizer difference in the scanner's self-test; the file and source_conflicts/ match main). CI covers the full suites.
  • No partner test files changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LqCGPgxDukji8Bhs57c5rH

#10392 subtracts the 2021-2023 partial pension, annuity, and IRA income
exemption in mt_agi_indiv and mt_agi_joint rather than through
mt_subtractions, so a Hypothesis-drawn pensioner with no other Montana
adjustment reported Montana AGI below federal AGI and failed the pooling
property. Require both exemption amounts to be zero for that property and
for the seeded grid's matching guard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants