Skip to content

Apply the IBR and PAYE 10-year cap before spousal proration - #10570

Merged
MaxGhenis merged 7 commits into
mainfrom
idr-cap-before-proration
Nov 3, 2026
Merged

MaxGhenis merged 7 commits into
mainfrom
idr-cap-before-proration

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Income-Based Repayment and Pay As You Earn now take the lesser of the income-based amount and each borrower's own 10-year standard payment first, then prorate that lesser amount by the balance share. This reverses the order #10415 used (prorate, then cap) and keeps its per-borrower cap.

The law

Numbers

Joint filers in Texas, 2026, $30,000 of Direct Loans each, $600 income-based amount unless stated. S is each spouse's own 10-year standard payment.

Couple Each spouse's S Main This PR
0% and 12% $250.00, $430.41 $250 + $300 $125.00 + $215.21
6% each $333.06 each $300 + $300 $166.53 + $166.53
0% and 12%, $800 income-based $250.00, $430.41 $250 + $400 $125.00 + $215.21
6% each, $100 income-based $333.06 each $50 + $50 $50 + $50
6% each, IBR 10% and 15% of $3,000 discretionary income $333.06 each $150 + $225 $150.00 + $166.53
$10,000 Direct + $10,000 FFEL and $20,000 + $20,000, 6% $222.04, $444.08 $100 + $200 $37.01 + $148.03

Arithmetic: S = B × i / (1 − (1 + i)^−120), i = rate / 12, or B / 120 at 0%. Row 1: min($600, $250) × 50% = $125.00; min($600, $430.41) × 50% = $215.21. Row 5: min($300, $333.06) × 50% = $150; min($450, $333.06) × 50% = $166.53, so the cap binds on one spouse. Row 6: balance shares 10,000 / 60,000 and 20,000 / 60,000 ((g)(1)(i) × (g)(1)(ii)); min($600, $222.04) / 6 = $37.01 and min($600, $444.08) / 3 = $148.03.

Consequence

A couple whose income-based amount exceeds both standard payments pays each spouse's standard payment times their balance share. At equal balances and rates the couple pays half their combined standard amount, however high their income. 20 U.S.C. 1098e(b)(6)(A) sets a maximum, and this stays under it. The Department's 2026 preamble (91 FR 23768) explains proration as avoiding counting a spouse's income twice, which doesn't reach the cap, but the regulation's text multiplies the whole amount.

A borrower with no spouse debt doesn't move: main's min(A × d, S × d) equals min(A, S) × d when the balance share equals the Direct fraction d.

Invariants

Checked by policyengine_us/tests/test_student_loan_idr_cap_order_property.py (Hypothesis, 10 examples of up to 8 couples, each couple in five variants, one vectorized simulation per example), on the final payment, compared with references passed through the same rounding, small-payment rule and balance cap (all nondecreasing, so equalities, bounds and monotonicity carry through):

  • Differential: with no spouse debt, the payment equals an independently written min(A × d, S × d).
  • Identity: P_i = min(A_i, S_i) × own Direct / couple's (Direct + FFEL), and a borrower whose spouse has no eligible debt pays the single-filer amount.
  • Bound: P_i ≤ S_i × own Direct / own (Direct + FFEL) and P_i ≤ A_i × balance share.
  • Monotonicity: P_i never falls as income rises and never rises as the spouse's balance rises.

Tests

Run locally, one process each:

  • income_based_repayment/income_based_repayment_payment.yaml: 28 passed.
  • pay_as_you_earn/pay_as_you_earn_payment.yaml: 14 passed.
  • student_loans/integration.yaml: 3 passed, unchanged (no case has spouse debt).
  • test_student_loan_idr_cap_order_property.py: 2 passed; pytest reports 7.3 s, 1.5 s of it the 10 Hypothesis examples; about 52 s wall with interpreter start and model import; peak RSS 0.89 GiB. Shrinking is off, so a failure reports the example as drawn instead of running hundreds more simulations.
  • code_health/test_source_conflicts.py: 103 passed.

Mutants, one process per test file, code restored after each:

Mutant IBR YAML PAYE YAML Property tests
Restore main's order (prorate, then cap at S × Direct fraction) 9 failed 6 failed 2 failed
Cap at the couple's combined standard payment 9 failed 6 failed 2 failed
Drop the balance share 11 failed 6 failed 2 failed
Direct fraction alone in place of the balance share 10 failed 6 failed 2 failed

integration.yaml passes under every mutant, as expected, since none of its cases has spouse debt.

Register: source_conflicts/candidates.yaml gains us:idr-10-year-cap-before-spousal-proration (state ruled, pe_us.follows: gap-fill). No # Source conflict marker until the shared register holds the entry.

impact: none at baseline (the population file holds no student loan balance, plan or rate input, and no variable reads these two payments)

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

34 CFR 685.209(f)(2)-(3) make the payment the lesser of the income-based
amount and the borrower's own 10-year standard payment, and (g)(1) adjusts
the amount calculated under (f), so the balance share multiplies the
lesser-of result. The model prorated first and capped second. Each
borrower's cap stays on their own loans.

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

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub, taking over from the earlier session that opened this draft. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

Keep the branch current with main before finishing the item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roperty test

Record us:idr-10-year-cap-before-spousal-proration in the source-conflict
backlog: the regulation's order (cap under 685.209(f)(2)-(3), then the (g)(1)
proration) against capping after proration, under 20 U.S.C. 1098e(b)(6)(A)'s
maximum. Skip Hypothesis shrinking in the property test, since each example
is a simulation and shrinking a failure ran for more than ten minutes.

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

Copy link
Copy Markdown
Contributor Author

Ready for hub review

HEAD: f7b65447afdefcb02190c9bc025cb231276275f9 (still a draft)

Taken over from the earlier session. The head was faa27ad9c1 when I started, with no pushes since 2026-11-01 19:09Z. I read both of its commits against the item and kept them: the formula change and the docs were right. I added a merge of origin/main (13 commits, a normal merge commit) and one commit for the register entry and a property-test bound.

Item steps

  1. Order. income_based_repayment_payment.py:76-79 and pay_as_you_earn_payment.py:71-77 compute min_(rate × discretionary_income, standard_payment) × balance_share, each spouse at their own rate. direct_fraction and eligible_balance are gone, and nothing else became unused. Rounding, the small-payment rule, the balance cap and the July 2026 bar are unchanged. The comment above the calculation cites (f)(2)-(3) and (g)(1)(i)(B), (g)(1)(ii): IBR :60-65, PAYE :55-61.
  2. Descriptions. The documentation strings in both files, income_driven_repayment_standard_payment.py:11-31 and income_driven_repayment_balance_share.py:20-23 now describe the order. A search for "prorat" and "cap" in variables/gov/ed/student_loans/ and parameters/gov/ed/student_loans/ found no other text describing the old order. SAVE, ICR and RAP mention proration but have no 10-year cap.
  3. Expected values. The arithmetic is below. integration.yaml needed no change: none of its cases has spouse debt, and it passes unchanged. No case without spouse debt moved (IBR Case 22, a single filer with their own FFEL loans, keeps 50.19 and 111.02).
  4. Pinned cases. IBR Cases 18, 19, 25 and 26 hold the four table rows; IBR Case 27 is the 10% / 15% mixed-rate case; IBR Case 28 and PAYE Case 14 have FFEL loans for both spouses; IBR Case 24 is the July 2026 bar case, recomputed.
  5. Property tests. policyengine_us/tests/test_student_loan_idr_cap_order_property.py. The cost is under Tests.
  6. Mutants. All four killed; table below.
  7. Register. source_conflicts/candidates.yaml:10400-10436 adds us:idr-10-year-cap-before-spousal-proration (program student-loans, state ruled, pe_us.follows: gap-fill, prs → Apply the IBR and PAYE 10-year cap before spousal proration #10570). It has no marker, and test_source_conflicts.py passes (103).
  8. Changelog. changelog.d/idr-cap-before-proration.fixed.md, from the earlier session, in Stop rebuilding dated parameter values for each pinned system #10550's shape.
  9. Body. Rewritten in the order the item asks for, with impact: pending (hub) and Count FFEL debt in student loan proration and caps, use the latest return's AGI, and add the parent PLUS standard payment #10415's axiom: line copied exactly.

Changed expected values

S = B × i / (1 − (1 + i)^−120), i = rate / 12; B / 120 at 0%. At 6%: 10,000 → 111.0205, 20,000 → 222.0410, 30,000 → 333.0615, 40,000 → 444.0820. At 12%: 30,000 → 430.4128.

Case Old New Arithmetic
IBR 8 445.69, 148.56 445.69, 55.51 A = 15% × 47,540 / 12 = 594.25; min(594.25, 666.12) × 75% = 445.69; min(594.25, 222.04) × 25% = 55.51
IBR 18 250, 300 125, 215.21 A = 600; min(600, 250) × 50%; min(600, 430.4128) × 50% = 215.2064
IBR 19 250, 400 125, 215.21 A = 800; same caps × 50%
IBR 20 250, 333.06 166.53, 166.53 DI 5,000: min(500, 333.0615) × 50%; min(750, 333.0615) × 50%
IBR 21 86.06, 0 27.76, 0 A = 15% × 27,540 / 12 = 344.25; min(344.25, 111.0205) × 10,000 / 40,000 = 27.755
IBR 24 187.50, 0 166.53, 0 min(375, 333.0615) × 30,000 / 60,000; PAYE min(250, 333.06) × 50% = 125 (unchanged)
IBR 25 (new) — 166.53, 166.53 A = 600; min(600, 333.0615) × 50% (main: 300, 300)
IBR 26 (new) — 50, 50 A = 100; min(100, 333.06) × 50% (main: 50, 50)
IBR 27 (new) — 150, 166.53 DI 3,000: min(300, 333.06) × 50%; min(450, 333.0615) × 50% (main: 150, 225)
IBR 28 (new) — 37.01, 148.03 A = 600; shares 10,000 / 60,000 and 20,000 / 60,000; min(600, 222.041) / 6 = 37.007; min(600, 444.082) / 3 = 148.027 (main: 100, 200)
PAYE 3 264.11, 132.06 264.11, 74.01 A = 396.17; min(396.17, 444.08) × 2/3 = 264.11; min(396.17, 222.041) / 3 = 74.014
PAYE 4 444.08, 111.02 355.27, 22.20 A = 979.50; 444.082 × 80% = 355.266; 111.0205 × 20% = 22.204
PAYE 10 250, 300 125, 215.21 as IBR 18
PAYE 11 250, 400 125, 215.21 as IBR 19
PAYE 12 59.46, 0 27.76, 0 A = 237.83; min(237.83, 111.0205) × 25%
PAYE 14 (new) — 37.01, 148.03 as IBR 28 at A = 600

Tests

  • IBR YAML 28 passed, PAYE YAML 14 passed, integration.yaml 3 passed, after the merge with main.
  • Property test: 2 passed. Pytest reports 7.3 s, 1.5 s of it the 10 Hypothesis examples. Wall time is about 52 s with interpreter start and model import, and peak RSS is 0.89 GiB (local, not CI). It runs in the Rest Python remaining group, picked up automatically, and the Makefile is unchanged.
  • code_health/test_source_conflicts.py: 103 passed.

Mutants (one process per test file, code restored after each)

Mutant IBR YAML PAYE YAML Property file integration.yaml
Main's order 9 failed 6 failed 2 failed passes
Couple's combined standard payment 9 failed 6 failed 2 failed passes
Drop the balance share 11 failed 6 failed 2 failed passes
Direct fraction alone 10 failed 6 failed 2 failed passes

Judgment calls

  • Shrinking off. Under the first mutant, Hypothesis shrinking ran more than ten minutes, because every example is a simulation of up to 40 people. I set phases to explicit, reuse and generate, so a failure reports the example as drawn and fails in about 6 s.
  • Post-processing in the properties. The properties compare the model's final payment with references passed through the same rounding, small-payment rule and balance cap. That post-processing is nondecreasing, so equalities and bounds carry through. Reading the pre-rounding amount would need a new variable.
  • Register. program: student-loans is a new program slug; the file has no student loan entries. years.from: 2026 with type: calendar.
  • Sources not re-fetched. This session's network refuses ecfr.gov, law.cornell.edu, uscode.house.gov, govinfo.gov, federalregister.gov and web.archive.org. So the quotes in the body come from the brief and are not verified against the source in this session. The 91 FR 23768 preamble point is paraphrased from the brief and not checked.
  • CI. CI hasn't run: the Pull request run at the head (41482758189) is completed / skipped, because the PR is a draft.

Generated by Claude Code

@codecov

codecov Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff            @@
##              main    #10570   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            1         4    +3     
  Lines           15        78   +63     
=========================================
+ Hits            15        78   +63     
Flag Coverage Δ
unittests 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.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: APPROVE
f7b6544
METHODOLOGY_CHOICE: yes

The PR's head is still f7b65447af, the head the brief names. No earlier **Hub review in progress** comment exists on this PR, so this review started from the beginning. The code does what the ruling says for both plans, every changed and new expected value matches my own computation from the rule, and CI is green at the head. The findings below are about coordination and wording, not the result.

Findings

P2. #10602 is a second open PR that makes the same change, and the two conflict

P3. programs.yaml still describes the old order

  • Where: policyengine_us/programs.yaml:1793 (IBR) and :1817 (PAYE). The /us/metadata API serves these notes.
  • What: Both say "spousal and FFEL proration, cap at the borrower's own 10-year standard payment …", which reads as proration first and then the cap. The brief limited the build to named files, so this is a gap in the brief, not an error by the session.
  • Replacement, the same wording Apply the 10-year cap before the spousal proration in IBR and PAYE #10602 uses. On both lines, replace
    spousal and FFEL proration, cap at the borrower's own 10-year standard payment on their current Direct and FFEL balances, small payment adjustment
    with
    cap at the borrower's own 10-year standard payment on their current Direct and FFEL balances, then spousal and FFEL proration, small payment adjustment.

P3. The body's description of where the invariants are checked contradicts itself

  • Where: PR body, "Invariants" paragraph: "on the payment before the small-payment rule and the balance cap, passed through the same rounding, small-payment rule and balance cap".
  • What the test does: check() compares the model's final income_based_repayment_payment and pay_as_you_earn_payment with references passed through post_process (test_student_loan_idr_cap_order_property.py:69-76, 205-207). It never reads the amount before the small-payment rule. The hand-off states this correctly; the body doesn't.
  • Replacement: "on the final payment, compared with references passed through the same rounding, small-payment rule and balance cap (all nondecreasing, so equalities, bounds and monotonicity carry through)".

P3. The register entry treats the statutory maximum as covering PAYE

  • Where: source_conflicts/candidates.yaml:10417 (readings.statute) and :10434 (note).
  • What: The note says "The model follows the regulation's order, which stays under the statute's maximum." 20 U.S.C. 1098e is the IBR statute, so PAYE has no statutory cap; the code comment at pay_as_you_earn_payment.py:55-56 already says so ("as 20 U.S.C. 1098e(b)(6)(A) does for the Income-Based Repayment plan"). The guard passes either way.
  • Replacement for the note's first sentence: "The model follows the regulation's order for both plans. For Income-Based Repayment it stays under the statute's maximum; Pay As You Earn has no statutory cap."

Checked and sound

  • The order, in code. Both formulas compute np.round(min_(rate × discretionary_income, standard_payment) × balance_share, 2), at income_based_repayment_payment.py:76-79 and pay_as_you_earn_payment.py:71-77. IBR picks each spouse's own rate with where(new_borrower, …). standard_payment is income_driven_repayment_standard_payment, built from the person's own Direct plus FFEL balance only, with the $50 minimum and the balance bound. The small-payment rule (< 5 → 0, else max(·, 10)), the cap at the Direct balance and the July 2026 bar follow in the same order and with the same code as on main. direct_fraction and eligible_balance are gone, and nothing else uses them. ruff format --check and ruff check are clean on the 5 changed Python files.

  • Balance share and FFEL. income_driven_repayment_balance_share is unchanged apart from its docstring: own Direct ÷ the couple's (Direct + FFEL), limited to head and spouse. That is (g)(1)(i) × (g)(1)(ii), and it is the same FFEL treatment Count FFEL debt in student loan proration and caps, use the latest return's AGI, and add the parent PLUS standard payment #10415 shipped. Its only other reader is save_plan_payment, whose results don't move.

  • Expected values. I computed every changed and new case from the rule before reading the code. S = B·i/(1−(1+i)^−120), with i = rate/12 (B/120 at 0%), at least $50 and at most B; DI = (AGI − 1.5 × 21,640)/12; P = post(min(rate × DI, S) × own Direct / couple's (Direct + FFEL)). I also computed main's order for each case.

    Case My value YAML Main's order
    IBR 8 445.69, 55.51 same 445.69, 148.56
    IBR 18 125.00, 215.21 same 250, 300
    IBR 19 125.00, 215.21 same 250, 400
    IBR 20 166.53, 166.53 same 250, 333.06
    IBR 21 27.76, 0 same 86.06, 0
    IBR 24 166.53, 0 (spouse barred) same 187.50
    IBR 25 166.53, 166.53 same 300, 300
    IBR 26 50, 50 same 50, 50
    IBR 27 150.00, 166.53 same 150, 225
    IBR 28 37.01, 148.03 same 100, 200
    PAYE 3 264.11, 74.01 same 264.11, 132.06
    PAYE 4 355.27, 22.20 same 444.08, 111.02
    PAYE 10 125.00, 215.21 same 250, 300
    PAYE 11 125.00, 215.21 same 250, 400
    PAYE 12 27.76, 0 same 59.46, 0
    PAYE 14 37.01, 148.03 same 100, 200
    PAYE in IBR 24 125, 0 same 125

    Standard payments: 30,000 at 0% is 250.0000, at 12% 430.4128 and at 6% 333.0615; at 6%, 10,000 is 111.0205, 20,000 is 222.0410, 40,000 is 444.0820 and 60,000 is 666.1230. These match the YAML comments and the hand-off's table. No case without spouse debt changed: IBR 22 is still 50.19 and 111.02, and integration.yaml is unchanged.

  • Payments can only fall. Since balance share ≤ own Direct fraction, min(A, S) × share ≤ min(A × share, S × fraction), and the post-processing is nondecreasing. So for every input the new payment is no higher than main's, and it is equal when the spouse has no eligible debt.

  • The defect reproduces at main. I ran the PR's two YAML files against main's formulas, on a scratch worktree at origin/main with the PR's test files copied in. IBR gave 9 failed, 19 passed (Cases 8, 18, 19, 20, 21, 24, 25, 27, 28) and PAYE 6 failed, 8 passed (Cases 3, 4, 10, 11, 12, 14). IBR 26 passes on main, as it should, because both orders give $50.

  • Tests at the head, one process each: IBR YAML 28 passed, PAYE YAML 14 passed, integration.yaml 3 passed, the property file 2 passed in 9.50s (about 45 s wall with imports, 0.88 GiB peak RSS, local, not CI), and code_health/test_source_conflicts.py 103 passed.

  • Property tests.

    • The references come from the rule. The rates and the $50, $5 and $10 constants are literals, and S and the share are recomputed from the generated balances, not read from the model. The identity reference has the same algebraic form as the code because the rule does.
    • Nothing passes trivially. The borrower always has Direct debt (direct ≥ 1). The spouse is drawn with no debt, Direct only, FFEL only or both. Rates include 0% and 12%, and income runs to 15,000 a month, so the cap binds often. A model that returned all zeros fails the identity.
    • The brief's five properties are all present: the differential against main's form for no spouse debt and for a single filer, the identity, both bounds, and monotonicity in income and in the spouse's balance.
    • With shrinking off, a failure still prints the full drawn couples=[…] list, with each failing couple and spouse named in the assertion message. I checked this in the logs of the main-order mutant.
    • hypothesis is in the dev extra, and CI installs --extra dev. The file uses importorskip, so it skips without the dev extra (it skipped for me until I synced it). In CI it ran: both tests show PASSED in Rest Python shard 1 of run 41488313567, taking 6.57 s and 0.17 s.
  • Mutants. Each ran in its own process on the working tree, followed by git checkout -- policyengine_us/variables, and git status was clean after each.

    Mutant (applied to both plans) IBR YAML PAYE YAML Property file integration.yaml
    Main's order 9 failed 6 failed 2 failed 3 passed
    Couple's combined standard payment 9 failed 6 failed 2 failed 3 passed
    Drop the balance share 11 failed 6 failed 2 failed 3 passed
    Direct fraction alone in place of the share 10 failed 6 failed 2 failed 3 passed
    Combined standard payment for the tax-unit head only (added) 8 failed 5 failed 2 failed (identity) 3 passed
    FFEL dropped from the balance share's denominator (mine) 3 failed 2 failed 1 failed, 1 passed (identity and bound) 3 passed

    The first four match the hand-off's counts. integration.yaml survives every mutant because none of its cases has spouse debt, as the hand-off says.

  • Register. The id, program: student-loans (the file's first student loan item), state: ruled and pe_us.follows: gap-fill pass the guard's vocabularies (FOLLOWS, YEAR_TYPES, STATES in test_source_conflicts.py:766-768). gap-fill fits the protocol: 1098e(b)(6)(A) sets a maximum and is silent on proration, and the regulation fills that gap. years: {from: 2026, type: calendar} matches the programs registry's verified_start_year: 2026 for both plans. There's no # Source conflict marker, as the protocol requires before landing, and pe_us.prs points here.

  • Readers. No variable or parameter reads income_based_repayment_payment or pay_as_you_earn_payment: grep -rn over policyengine_us (tests excluded) finds only programs.yaml:1790, 1814. The model has no student-loan forgiveness, tax-on-forgiveness, plan-choice or household-total variable, and the registry notes say both payments are "not subtracted from household net income". So the only outputs that move are these two variables, and only for joint filers whose spouse holds eligible Direct or FFEL loans. Those outputs fall or stay the same.

  • Order with Apply the standard plan's $50 minimum once across a borrower's Direct and Parent PLUS loans #10599. Apply the standard plan's $50 minimum once across a borrower's Direct and Parent PLUS loans #10599 adds combined_standard_repayment_payment and helper functions. It doesn't change amortized_monthly_payment or anything the IDR formulas read. The only file both PRs touch is source_conflicts/candidates.yaml, and git merge-tree of the two heads is clean. Neither PR changes the other's results, so either can land first.

  • Main has moved 17 commits past this PR's merge base. None touches the student-loan folders, and git merge-tree against origin/main is clean.

  • Body lines. impact: pending (hub) is present, and the axiom: line is byte-identical to Count FFEL debt in student loan proration and caps, use the latest return's AGI, and add the parent PLUS standard payment #10415's (diffed). The PR changes nothing under .github/ or policyengine_us/tests/policy/baseline/partners/.

  • Paragraph cites, within the repo. The PR's mapping (PAYE and new-borrower IBR under (f)(2), other IBR under (f)(3)) agrees with the repo's own parameter references: pay_as_you_earn/rate.yaml and income_based_repayment/new_borrower_rate.yaml cite (f)(2)(i), and income_based_repayment/rate.yaml cites (f)(3)(i).

  • CI at the head. The PR is no longer a draft. Pull request run 41488313567 at f7b65447af completed success: 34 jobs, all success, none skipped or cancelled, including the Microsimulation suite, Household API Partners and all Baseline, Contrib and Rest shards. The earlier run 41482758189 was skipped while the PR was a draft.

What I couldn't check

  • The law text. This session's network refuses ecfr.gov, govinfo.gov, uscode.house.gov, federalregister.gov, law.cornell.edu, web.archive.org, studentaid.gov and ed.gov. So I couldn't quote 34 CFR 685.209(e)(2)(i), (f)(2), (f)(3) or (g)(1), check that the body's paragraph cites land on that text, or confirm that each plan's cap is the borrower's own 10-year standard payment. I also couldn't read 20 U.S.C. 1098e(b)(6)(A) or the 91 FR 23768 preamble point the body paraphrases. The hand-off says its session couldn't reach them either, so the body's quotations remain unverified against the source. The repo holds no captured copy under source_conflicts/sources/. A check from a machine that can reach ecfr.gov should confirm the quoted words before the register entry moves to rulespec-us.
  • Population impact. The microsimulation is the hub's: how many joint filers in the data have both spouses holding eligible loans, and how much the two payments fall. Because nothing reads these variables, household net income, taxes and benefits shouldn't move. The impact pair should confirm both points.
  • Memory in the remaining Python group. The new file joins it, and the Makefile records that group at 14.4 GB. I measured 0.88 GiB peak for the file alone in a fresh process. I didn't measure what it adds inside the shared process, but CI's Rest shard 1 passed at this head.

Generated by Claude Code

MaxGhenis and others added 3 commits November 2, 2026 19:16
The metadata notes still read as proration first, then the cap. The candidate's note also treated the statute's maximum as covering PAYE, which 20 U.S.C. 1098e doesn't reach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit 7eabf87 into main Nov 3, 2026
36 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#10570 at c97512892cbbdeab72f0ca2e4caf79cdd541c3e9 (squash): Apply the IBR and PAYE 10-year cap before spousal proration.

  • Review: cloud review r1 (Opus 5.5, independent) approved at f7b6544; the hub applied its P3 notes at the head (programs.yaml notes, candidate note, invariants sentence) and read 34 CFR 685.209(f) and (g), whose quotes hold.
  • Branch update: GitHub's branch updates merged main 6fa4eaf6d3, then 0a87d1f6b0, into the reviewed head 36b214cf4f; after each update, every file's changes match the reviewed head's.
  • 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: populace_us_2024.h5 at populace-us-2024-spm-20260909 has no column for federal_student_loan_balance, federal_ffel_loan_balance, federal_parent_plus_loan_balance, federal_student_loan_interest_rate or either payment (read directly), and no variable reads income_based_repayment_payment or pay_as_you_earn_payment. The hub withdrew the impact pair for that reason.
  • Related: open Apply the 10-year cap before the spousal proration in IBR and PAYE #10602 computes the same IBR and PAYE payments. Four of its test cases have no identical case on main: IBR cases 20 and 26 and PAYE cases 14 and 15.
  • 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

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