Skip to content

Add 2015-2023 Medicare IRMAA brackets and 2015-2020 Part B premiums - #10112

Merged
MaxGhenis merged 10 commits into
mainfrom
irmaa-historical-brackets
Nov 5, 2026
Merged

MaxGhenis merged 10 commits into
mainfrom
irmaa-historical-brackets

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What and why

The Medicare IRMAA schedules (gov.hhs.medicare.part_b.irmaa.* and gov.hhs.medicare.part_d.irmaa.*) had values only for 2024–2026, and the standard Part B premium (gov.hhs.medicare.part_b.base_premium) only from 2021. On main, every year from 2015 to 2023 reads the 2024 IRMAA schedule, and 2015–2020 read the 2021 standard premium of $148.50.

This PR adds each year's schedule for 2015–2023 to all ten IRMAA files (Part B and Part D, each for single, joint, head of household, surviving spouse and separate filers) and the 2015–2020 standard Part B premium. It changes no 2024–2026 value.

Annual amounts for one Medicare enrollee, on main and on this branch. The branch values equal the published table amounts times 12:

Year Filing status MAGI Part B premium Part D IRMAA
2015 single 90,000 1,782.00 → 1,762.80 0 → 147.60
2018 separate 100,000 1,782.00 → 5,143.20 0 → 897.60
2019 single 600,000 6,813.60 → 5,526.00 972.00 → 928.80
2021 joint 200,000 1,782.00 → 2,494.80 0 → 147.60

The schedule each year follows

42 U.S.C. 1395r(i) sets the Part B adjustments and thresholds:

  • 2015–2017. Paragraph (6) holds the threshold and bracket amounts at their 2010 levels: $85,000, $107,000, $160,000 and $214,000, doubled for joint returns.
  • 2018. The table in (3)(C)(i)(II): $85,000, $107,000, $133,500 and $160,000.
  • 2019. Subclause (III), added by Pub. L. 115-123, section 53114, adds an 85 percent tier at "at least $500,000". Under (3)(C)(ii), the joint amount for that tier equals 150 percent of $500,000, or $750,000, instead of twice it. Paragraph (5)(A) skips indexing in 2018 and 2019.
  • From 2020. Paragraph (5) indexes the amounts to CPI-U, except the $500,000 amounts ((5)(C)).
  • Married, filing separately, lived with the spouse. Clause (3)(C)(iii) reduces each dollar amount by the individual threshold. In 2015–2017 that leaves a 65 percent tier from $85,000 to $129,000 and an 80 percent tier above. In 2018 the 65 percent tier ends at $75,000, below the $85,000 threshold, so only the 80 percent tier applies. From 2019 the 85 percent tier starts at $415,000 (2019) down to $403,000 (2023).
  • Part D. 42 U.S.C. 1395w-113(a)(7) applies the Part B thresholds and applicable percentages, and sets each amount at the base beneficiary premium times (p − 25.5) / 25.5.

As on main, a "more than $X" threshold takes the value X + 1, and an "at least $X" threshold takes X. A tier that did not exist yet has threshold .inf and repeats the amount of the tier below, so the schedule never falls. That covers the 85 percent tier before 2019 and the separate schedule's second tier in 2018.

Sources, one per year

Part B. Thresholds, adjustments and standard premiums come from each year's Federal Register notice, "Medicare Part B Monthly Actuarial Rates, Premium Rate(s), and Annual Deductible". Each notice prints its tables three times; the links open the first printing. Each parameter file's reference titles now give these pages:

Year Notice Individual and joint tables Separate-return table Standard premium
2015 79 FR 61314 79 FR 61316 79 FR 61316 $104.90, 79 FR 61314
2016 80 FR 70811 80 FR 70814 80 FR 70814 $121.80, 80 FR 70811
2017 81 FR 80063 81 FR 80066 81 FR 80066 $134.00, 81 FR 80063
2018 82 FR 55370 82 FR 55372 82 FR 55373 $134.00, 82 FR 55370
2019 83 FR 52462 83 FR 52465 83 FR 52465 $135.50, 83 FR 52463
2020 84 FR 61625 84 FR 61627 84 FR 61628 $144.60, 84 FR 61625
2021 85 FR 71904 85 FR 71906-71907 85 FR 71907 $148.50, 85 FR 71904
2022 86 FR 64205 86 FR 64208 86 FR 64208 $170.10, 86 FR 64205
2023 87 FR 59080 87 FR 59083 87 FR 59084 $164.90, 87 FR 59080

Part D. The Federal Register does not print the Part D adjustments. CMS publishes them in these memos and fact sheets, which also restate the thresholds. The Part D files cite them and, for the thresholds, the Part B tables above.

Year CMS source Individual and joint table Separate-return table Base beneficiary premium stated
2015 Annual release of Part D bid information, July 31, 2014 file page 4 file page 4 $33.13
2016 Annual release of Part D bid information, July 29, 2015 file page 4 file page 4 $34.10
2017 Annual release of Part D bid information, July 29, 2016 file page 4 file page 5 $35.63
2018 Annual release of Part D bid information, July 31, 2017 file page 4 file page 5 $35.02
2019 Annual release of Part D bid information, July 31, 2018 file page 4 file page 5 $33.19
2020 Office of the Actuary, 2020 Part D income-related monthly premium adjustment file page 2 file page 3 $32.74
2021 Office of the Actuary, 2021 Part D income-related monthly premium adjustment file page 2 file page 3 $33.06
2022 CMS fact sheet, "2022 Medicare Parts A & B Premiums and Deductibles/2022 Medicare Part D Income-Related Monthly Adjustment Amounts" (linked in the Part D files) on the page on the page not stated
2023 CMS fact sheet, "2023 Medicare Parts A & B Premiums and Deductibles 2023 Medicare Part D Income-Related Monthly Adjustment Amounts" (linked in the Part D files) on the page on the page not stated

How we checked the values

We compared every threshold, adjustment and standard premium in the eleven files with the notices and CMS releases above, for each year from 2015 to 2026, and all 1,240 values match. We transcribed each table from the source text with a script (pdftotext -layout for the PDFs) and applied the files' convention: X + 1 for "greater than $X" and X for "greater than or equal to $X". The .inf placeholder tiers have no printed row to compare. No later Federal Register document corrects any of these notices. The 2026 Trustees Report's Tables V.E3 and V.E4, which the IRMAA files also cite, list the same 2015–2023 adjustments.

The provisions in the schedule section read as described in the 2024 edition of the U.S. Code (42 U.S.C. 1395r, 42 U.S.C. 1395w-113). Each reference's #page= opens the page with its table or premium statement.

A main test this branch makes stale

Main's #10106 added two lookback cases to medicare_irmaa_magi_two_years_prior.yaml (cases 6 and 7) that assert no IRMAA for a single filer with a $100,000 MAGI in 2015 and 2016. At the time, those years read the backdated 2024 schedule, whose first tier starts at $103,001. Under this branch, 2015 and 2016 use the $85,000 threshold, so $100,000 falls in the 35 percent tier, and both cases failed CI. The cases test the lookback, not the tier, so they now use $80,000. The 2015 MAGI stays 80,000, and the lookback scales the 2016 MAGI to 80,000 × 328,964 / 326,078 = 80,708.05, so both stay below $85,000.

Test plan

  • For policy changes: YAML tests whose expected values come from an external source. Every 2015–2023 expectation equals the published table amount, and each case names its table.
  • Source conflicts: not applicable. The values follow the statute and the Federal Register notices, which agree.
  • make format (ruff format and ruff check on the changed Python files)
  • Changelog fragment in changelog.d/ (irmaa-historical-brackets.fixed.md)

Tests

  • YAML. The new gross_medicare_part_b_premium.yaml has 33 cases. Cases 6 and 7 of medicare_irmaa_magi_two_years_prior.yaml now use a MAGI below the 2015–2016 threshold (see above). income_adjusted_part_d_premium_surcharge.yaml gains 33 cases and base_part_b_premium.yaml six. For 2015–2023 the cases reach the first threshold itself and each tier's lowest MAGI: all five filing statuses in 2016, 2019 and 2021, and single, joint and separate filers in the other years. The single and joint cases also reach each tier's highest MAGI. At each "more than $X" bound, a person whose MAGI equals $X pays the lower tier's amount. From 2019, a person $1 under the $500,000 bound ($750,000 joint) pays the 80 percent tier's amount. The cases include a MAGI of $2,000,000 before 2019, which must land in the 80 percent tier. Each case names its source table, with its page where the source has pages.
  • Properties. test_irmaa_schedule_properties.py checks the invariants below (560 cases), with one stated provision per property. The lookback test's monotonicity and zero-below-threshold Hypothesis properties now draw from 2015–2026, with the same number of examples.
  • CI: the latest Pull request run, at db993a8229 (https://github.com/PolicyEngine/policyengine-us/actions/runs/42021620614), concluded success: all 36 jobs passed.

Invariants

For every year from 2015 to 2035, both parts and every filing status:

  • each schedule starts at zero;
  • thresholds rise strictly until any +inf brackets, which come last;
  • amounts never fall from one tier to the next;
  • head of household and surviving spouse schedules equal the single schedule;
  • Part D thresholds equal Part B's.

For the published years (2015–2026):

  • joint thresholds equal twice the individual ones, except that the $500,000 amount becomes $750,000, 150 percent of it;
  • above the individual threshold T, the separate schedule charges what the single schedule charges at MAGI + T. The test checks this at every boundary ±1 and at Hypothesis-drawn MAGIs;
  • each Part B adjustment comes within $0.25 of (p − 25) / 25 times the standard premium. The printed adjustments fall as much as $0.20 below that product (2017–2019 and 2023), so the YAML cases, not this property, pin each amount;
  • the Part D adjustments rise in proportion to (p − 25.5), within $0.10;
  • before 2019, a MAGI of $1 billion pays the 80 percent amount.

Not modeled

The hold-harmless provision. The 2016 notice says that with no cost-of-living adjustment "the majority of Part B enrollees can pay no increase in their monthly premium" (80 FR 70813). The 2017 notice says the $134.00 premium "will be paid by (or on behalf of) approximately 30 percent of beneficiaries" (81 FR 80067). The model charges every enrollee the standard premium, as before. Both notices say the provision doesn't cover beneficiaries who pay an income-related adjustment, so the model charges IRMAA payers the right amount.

Related

impact: none from 2024 on. The PR adds values dated 2015 to 2023 and changes no 2024-2026 value, and base_part_b_premium, gross_medicare_part_b_premium and income_adjusted_part_d_premium_surcharge read those parameters at their own period. For 2015-2023 the PR changes Part B premiums and Part D IRMAA in household calculations. The default dataset holds 2024 records and no earlier data, so we report no aggregate for those years.
axiom: TheAxiomFoundation/rulespec-us#1677 queued

🤖 Generated with Claude Code

The Part B and Part D income-related monthly adjustment schedules started in
2024, so the backdating floor applied 2024 thresholds and amounts to benefit
years 2015-2023, and the standard Part B premium applied its 2021 value to
2015-2020. Encode each year's schedule from the Federal Register Part B
premium notices and CMS's Part D income-related adjustment releases: thresholds
frozen through 2017, the MACRA brackets for 2018, the 85% tier from 2019 and
CPI-U indexing from 2020, with the shorter married-filing-separately schedule
(a single tier in 2018).

Add YAML tests for every tier boundary in 2015-2023 and property tests for the
statutory structure (ordering, joint and separate schedules, Part B
adjustments relative to the standard premium, Part D tier proportions).

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

Copy link
Copy Markdown
Contributor Author

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


Generated by Claude Code

Brings the branch up to date with 513 commits on main. Main has since
dropped the stale Illinois AABD entry from the person-adds ratchet
(#10092), which was the only failing check on this branch, and merged
the IRMAA lookback fallback (#10106), whose test comment this branch now
makes stale. The merge had no conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each title now gives the page of the notice that prints the table it
sources, not just the notice's first page, and the Part D files cite the
same Part B tables for their thresholds, which 42 U.S.C.
1395w-113(a)(7)(A) takes from 1395r(i). The base premium file also cites
the 2021-2023 notices, which state the standard premiums already in it.
Every value cited was checked against the text of these pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reviewer can check each expected value against one page: the Federal
Register table for Part B and the standard premium, and the CMS memo or
fact sheet table for Part D.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lookback test's comment said years before 2024 are backdated from
2024, which stops being true once these brackets land, so its
monotonicity and zero-below-threshold properties now draw from 2015-2026.
They keep 25 examples each, so this adds no run time. Each schedule
property also states the provision it checks.

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

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub. I continue an earlier session's work on this PR: that session stopped before it could hand off, and I will check each of its commits before building on it. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub. I continue an earlier session's work on this PR, checking each of its commits before building on them. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

Main's #10106 cases 6 and 7 assert no IRMAA for a $100,000 MAGI in 2015
and 2016. They were written when 2015-2023 read the backdated 2024
schedule, whose first threshold is $103,001. With this branch's
schedules, 2015 and 2016 use the $85,000 threshold that 42 U.S.C.
1395r(i)(6) holds at its 2010 level, so $100,000 lands in the 65 percent
tier and both cases failed in CI. The cases test the lookback, not the
tier, so lower the income to $80,000 to keep each name true: 2015 MAGI
is 80,000 and 2016 MAGI deflates to 80,000 x 328,964 / 326,078 =
80,708.05, both below $85,000.

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

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 6232f6c64cb25df12c2f7db32b9a32ad69fb4a83

This continues an earlier session's work (it pushed 8452a089b6…9832fd5eeb and stopped before handing off). I checked each of its commits and kept all four. One part of the item is blocked: this session couldn't reach the Federal Register or CMS sources, so I couldn't redo the value-by-value check against the notices (see "Source verification").

Earlier session's commits

Commit What it does Verdict
8452a089b6 Merges main (513 commits, no conflicts). Kept.
9d95e32169 Cites the Federal Register page of each table and premium in the reference titles; the Part D files cite the Part B tables for their thresholds; base_premium.yaml cites the 2021–2023 notices. Kept. The titles follow the repo's reference rules: one source per entry, #page= in the href, and the 2021 two-page table quoted with #page 3-4. Whether each page holds what its title says is unverified here (blocked).
4da94d3d27 Names the source table in each 2015–2023 YAML case. Kept. Comments only.
9832fd5eeb Widens the lookback test's Hypothesis draws to 2015–2026 at the same example count, and states each schedule property's provision. Kept.

What the item asked

1. The failing check: fixed

  • What failed. At 9832fd5eeb, "Baseline tests / Full Suite - Baseline (contrib-hhs)" failed on two of main's cases in tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_irmaa_magi_two_years_prior.yaml. Fall back to the earliest input year's MAGI for Medicare IRMAA, and add a same-year income switch #10106 added those cases (6 and 7) when 2015–2023 read the backdated 2024 schedule, whose first threshold is $103,001. Each asserts no IRMAA at a $100,000 MAGI in 2015 or 2016.
  • Why. Under this branch, 2015 and 2016 use the $85,000 threshold, so $100,000 is in the 65 percent tier. CI's 1,762.80 is 12 × (104.90 + 42.00), and its 152.40 Part D amount is 12 × 12.70. The branch's values were right and the test inputs were stale.
  • Fix (medicare_irmaa_magi_two_years_prior.yaml:68 and :83). The cases test the lookback, not the tier, so each now uses $80,000 of income and keeps its name. Hand values:
    • Case 6: 2015 MAGI = 80,000 (≤ 85,000), so the premium is 12 × 104.90 = 1,258.80.
    • Case 7: 2016 MAGI = 80,000 × 328,964 / 326,078 = 80,708.05 (≤ 85,000), so Part D IRMAA is 0.
  • Mutants (one process each, restored after):
    • Lowering 2015's Part B single first threshold to $75,001 fails case 6.
    • Lowering 2016's Part D single first threshold to $80,001 fails case 7.

2. Every bracket and premium against the CMS notices, by script: blocked

The session's network policy denies govinfo.gov, federalregister.gov, cms.gov, ssa.gov, law.cornell.edu, uscode.house.gov and web.archive.org; only GitHub is reachable. I couldn't read any notice, memo, fact sheet or the statute, so I can't vouch for any value against its source. The PR body's 981-value comparison is the earlier session's, and I couldn't rerun it.

What I could check is an independent structural script over the raw YAML (it doesn't use the model or the PR's tests). It checks 2015–2023, 210 checks:

  • the single thresholds: 2015–2017 at $85,001, $107,001, $160,001, $214,001; 2018 at $133,501 and $160,001 for tiers 3 and 4; 2019 adding $500,000; and 2020–2023 at $1,000 steps plus one;
  • joint = 2 × single, except the $750,000 tier;
  • head of household and surviving spouse = single, for both parts;
  • Part D thresholds = Part B's;
  • the separate schedule charges what the single schedule charges at MAGI + T;
  • each .inf tier repeats the amount below it.

All 210 pass. The amount checks:

  • Part D. For each year, one base premium B reproduces every tier as B × (p − 25.5) / 25.5, rounded to the dime. The implied B ranges are 2015 $33.10–33.15, 2016 $34.09–34.12, 2017 $35.63–35.65, 2018 $34.98–35.02, 2019 $33.15–33.19, 2020 $32.73–32.76, 2021 $33.06–33.06, 2022 $33.36–33.38 and 2023 $32.73–32.76. For 2015–2021 each contains the memo base premium the PR body states.
  • Part B. One unrounded base U reproduces every adjustment as U × (p − 25) / 25, rounded to the dime, in 2015, 2016 and 2020–2023. In 2017, 2018 and 2019 no single U fits, by $0.03 at most. For example, 2017's tier-1 $53.50 needs U ≤ 133.875, while tier 3's $214.30 needs U ≥ 133.906. So CMS computes those adjustments from something other than one rounded base, and only the notices can confirm the printed values. All amounts are within the $0.25 tolerance in test_irmaa_schedule_properties.py.

For the hub: rerun the body's source comparison from a host that can reach govinfo.gov and cms.gov, or allow those hosts in the cloud environment's network settings (environment menu → Edit → Network access; https://code.claude.com/docs/en/cloud-environments#network-access).

Tests run (one file per process)

File Result
eligibility/medicare_irmaa_magi_two_years_prior.yaml 21 passed
eligibility/medicare_irmaa_magi.yaml 4 passed
eligibility/gross_medicare_part_b_premium.yaml 33 passed
eligibility/base_part_b_premium.yaml 11 passed
eligibility/income_adjusted_part_d_premium_surcharge.yaml 45 passed
test_irmaa_schedule_properties.py 560 passed
test_irmaa_lookback_and_switch.py 23 passed
tests/unit/test_ss_calculator_differential.py -k irmaa 1 passed (the only test main added since the last merge that reads Part B or IRMAA)

CI

The Pull request run at this head, 41717469233, concluded success, with all 36 check runs passing. That includes "Baseline tests / Full Suite - Baseline (contrib-hhs)", the job that failed at 9832fd5eeb.

Merge

I merged main at 6145ad4513; the merge had no conflicts. Its message is git's default and lacks the co-author line.

Judgment calls

  1. Changing main's test inputs instead of its expected values. Cases 6 and 7 are named for "income below the first threshold". Lowering the income keeps that true; keeping $100,000 and expecting the IRMAA would change what the cases test. The new 2015–2016 tier amounts are covered in gross_medicare_part_b_premium.yaml and income_adjusted_part_d_premium_surcharge.yaml.
  2. The statute cites in the new case comments are the PR body's. Cases 6 and 7 cite 42 U.S.C. 1395r(i)(6) and 1395w-113(a)(7), as the PR body does. I couldn't read either provision, so check them with the sources.
  3. Reporting the 2017–2019 Part B rounding gap instead of changing values. I can't read the notices, so I left the values as the earlier session transcribed them.

Overlapping PRs

Proposed follow-up issue

Model the Part B hold-harmless premium for 2016–2017.
The model charges every enrollee the full standard premium, but the PR body quotes the 2016 and 2017 notices saying many enrollees paid less, which this session couldn't confirm. Add the held-harmless premium for enrollees without IRMAA, citing the statute and both notices once they've been read.

impact: the hub will measure
axiom: (hub files pe-parity before landing)


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Hub review in progress

Reviewing head 6232f6c64cb25df12c2f7db32b9a32ad69fb4a83 (unchanged since the brief). Interim findings, to be replaced by the final review:

  • Sources blocked. This review session can't reach govinfo.gov, federalregister.gov, cms.gov, ssa.gov, law.cornell.edu, uscode.house.gov, ecfr.gov or web.archive.org (the proxy refuses each one). So no value has been checked against its notice in this round either.
  • CI. Pull request run 41717469233 at this head: 36 of 36 jobs succeeded.
  • Reproduced. The body's main-vs-branch table reproduces at origin/main and at the head. Cases 6 and 7 match by hand: 80,000, and 80,000 × 328,964 / 326,078 = 80,708.05.
  • Overlap not in the body. Fill CBO total AGI for 2015-2018 with IRS SOI actuals #10114 (open) conflicts with this PR in medicare_irmaa_magi_two_years_prior.yaml at cases 6 and 7 (git merge-tree), and it changes the deflator those cases' comments quote.
  • Stale comment made false. medicaid_optional_senior_or_disabled_countable_income.py:77 says base_premium starts in 2021. After this PR, it starts in 2015.
  • Still running: the changed test files, mutants, and a reader sweep.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: REQUEST_CHANGES
6232f6c
METHODOLOGY_CHOICE: no

Independent review, round 1. The head is the one the brief named. Nothing was pushed after 6232f6c64c. The PR merges cleanly with origin/main at 25c4253483 (git merge-tree); main is 6 commits past the PR's last merge and touches no Medicare file since then.

Bottom line. The schedules are internally consistent. Every structural property I could derive holds. CI is green, the changed tests pass, and the body's main-vs-branch table reproduces. But no value in this PR has yet been checked against its Federal Register notice or CMS memo by a session that could read them: this session's network policy blocked every primary source, as it did for the author. The changes I ask for are small: a code comment this PR makes false, an overstated coverage claim in the body backed by a surviving mutant, and an undisclosed conflict with #10114. The source comparison still has to be run before landing.

Findings

P2-1. The body says every tier boundary is covered, but no test pins the top MAGI of tiers 2 and up, and a mutant survives

policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/gross_medicare_part_b_premium.yaml and income_adjusted_part_d_premium_surcharge.yaml. The body says: "Every tier boundary in 2015–2023 is covered".

Each case tests the first threshold itself (e.g. 85,000) and the lowest MAGI of each tier (X + 1). It never tests the highest MAGI of a tier above the first (X for "more than $X"). I parsed both files by script and listed every (year, status, threshold) the cases reach:

gross_medicare_part_b_premium.yaml: 33 year/status combos
  missing lowest MAGI in tier: []
  missing MAGI one below a threshold: 72 [(2015, 'joint', 214000.0), (2015, 'joint', 320000.0), (2015, 'joint', 428000.0), (2015, 'single', 107000.0), (2015, 'single', 160000.0), (2015, 'single', 214000.0), ...]
income_adjusted_part_d_premium_surcharge.yaml: same, 72 missing

Failure scenario. Suppose a "more than $107,000" threshold is stored as 107,000 instead of 107,001, consistently across the files. Then a MAGI of exactly $107,000 pays the 50 percent amount instead of the 35 percent one. Mutant M3 below does exactly this for 2017, tier 2, in Parts B and D (single, head of household, surviving spouse at 107,000; joint at 213,999). It survives every test:

== M3 (8 files changed)
  -- eligibility/gross_medicare_part_b_premium.yaml           33 passed
  -- eligibility/income_adjusted_part_d_premium_surcharge.yaml  45 passed
  -- test_irmaa_schedule_properties.py                        560 passed

Replacement. In each year's single and joint case, add a person at X for each "more than $X" threshold above the first (e.g. 2017 single: 107,000, 160,000, 214,000; 2019: also 499,999 against the "at least $500,000" tier). Alternatively, change the body sentence to: "Each tier's lowest MAGI and the first threshold itself are covered; the highest MAGI of tiers 2 and up is not."

P2-2. #10114 conflicts with this PR, and the body doesn't mention it

policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_irmaa_magi_two_years_prior.yaml:68-96. #10114 ("Fill CBO total AGI for 2015-2018 with IRS SOI actuals", open, not a draft) rewrites the same cases 6 and 7 and the deflator their comments quote:

$ git merge-tree --write-tree --name-only HEAD pr-10114
CONFLICT (content): Merge conflict in .../eligibility/medicare_irmaa_magi_two_years_prior.yaml

Under #10114, case 7's factor becomes 31,312.48 / 31,085.28 (#10114's own header values), not 27,704.54 / 27,461.49. So whichever PR lands second must use 80,000 × 31,312.48 / 31,085.28 = 80,584.71 for case 7. That is still below $85,000, so the Part D IRMAA stays 0. Replacement: add #10114 to the body's "Related" list, naming that conflict and the case 7 value it needs.

The other overlaps check out: #10103 conflicts in exactly the eight files the body names (re-run with git merge-tree). #10111, #10118, #10119, #10634 and #10657 merge cleanly with this head.

P3-1. A main comment this PR makes false

policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.py:77-78:

        # NOTE: gov.hhs.medicare.part_b.base_premium starts in 2021, so a
        # person-paid Part B before 2021 reads the 2021 premium.

After this PR, base_premium.yaml starts at 2015-01-01: 104.90. Replacement: delete both lines.

P3-2. The Part B property tolerance can't catch a dime-level transcription error; only the YAML cases do

test_irmaa_schedule_properties.py:53 (PART_B_TOLERANCE = 0.25). Each adjustment's distance from (p − 25) / 25 × the standard premium, computed from the YAML:

2017 [-0.1, -0.1, -0.1, -0.2]   2018 [-0.1, -0.1, -0.1, -0.2]   2019 [-0.1, -0.1, -0.1, -0.2, -0.2]
2021 [0, 0, 0, 0, 0]            2023 [-0.06, -0.1, -0.14, -0.18, -0.16]

For 2021, where every distance is 0, a $0.20 typo still passes. Mutant M4 shows this. The property file passes it, and only the YAML kills it:

== M4: 2021 Part B tier-1 amount 59.40 -> 59.60 in single, head of household, surviving spouse, joint
  -- test_irmaa_schedule_properties.py               560 passed
  -- eligibility/gross_medicare_part_b_premium.yaml  4 failed, 29 passed
``` This isn't a wrong result: the YAML expectations, which the author transcribed from the tables, are what guard each amount. But the body's list of invariants reads as if the property test checks the amounts, so say in the body that it checks them only to ±$0.25.

### P3-3 (follow-up; main has the same behavior). The separate schedule applies to every separate filer, not only those who lived with their spouse

`gross_medicare_part_b_premium.py:29-44` and `income_adjusted_part_d_premium_surcharge.py:33-42` select `p.separate` for every `SEPARATE` filer. The body's own description of the schedule is headed "Married, filing separately, lived with the spouse", and the property test docstring says the same. Nothing in the model reads whether the spouse lived apart. Main already does this for 2024–2026, and for 2015–2023 it read the 2024 separate schedule, so this isn't this PR's defect. Witness: a 2018 `SEPARATE` filer with $100,000 MAGI pays `5,143.20` (12 × (134.00 + 294.60)) whatever their living arrangement. Proposed follow-up: **"Apply the Medicare IRMAA separate-return schedule only to spouses who lived together."** It would add an input for living apart at any time in the year, and give separate filers who lived apart the single schedule.

## Checks the brief asked for

### Values against the notices: not confirmed (blocked)

Every source host was refused from this session: `curl` to www.govinfo.gov, www.federalregister.gov, www.cms.gov, uscode.house.gov, www.law.cornell.edu, web.archive.org, www.ecfr.gov and www.ssa.gov returned no connection (proxy `403` to CONNECT), and the fetch tool couldn't resolve federalregister.gov or govinfo.gov. **I confirmed no year against its source, and I quote no law.** The body's "981 values, no mismatches" claim still rests only on an earlier session's run that nobody has reproduced. The hub has to run the comparison from a host that reaches govinfo.gov and cms.gov, or allow those hosts.

What I could check, by script on the YAML (`dump.py` → all ten files, 2015–2026):

- **Part B: one unrounded base U per year that reproduces every adjustment to the dime.** U exists for 2015 [104.886, 104.906], 2016 [121.795, 121.841], 2020 [144.594, 144.604], 2021 [148.479, 148.521], 2022 [170.094, 170.104] and 2023 [164.812, 164.841]. No U exists for 2017 [133.906 > 133.875], 2018 (same) or 2019 [135.406 > 135.375]. The hand-off says the same. Main's 2026 also has none (202.896 > 202.886), so the gap isn't unique to the years this PR adds. In 2017–2019 every adjustment sits $0.10–0.20 *below* (p − 25) / 25 × the standard premium. That pattern is consistent with CMS computing the adjustments from a base other than the printed standard premium. Only the notices can settle it, so **check those three years' printed tables first**.
- **Part D: one base beneficiary premium B per year reproduces every tier.** B exists for every year 2015–2026. Each base premium the body states falls inside its year's interval: 2015 33.13 ∈ [33.103, 33.150], 2016 34.10 ∈ [34.087, 34.118], 2017 35.63 ∈ [35.630, 35.648], 2018 35.02 ∈ [34.975, 35.022], 2019 33.19 ∈ [33.150, 33.193], 2020 32.74 ∈ [32.734, 32.763], 2021 33.06 ∈ [33.056, 33.064]. The 2017 and 2018 values sit at the edge of their intervals.
- **Standard premium.** Each year's 50 percent adjustment equals its standard premium in 2015, 2016 and 2020–2022; in 2017–2019 and 2023 it is $0.10 below.

### The fixed cases, by hand

- **Case 6** (2015, employment income 80,000, inputs only for 2015). The year two back is 2013, before the first input year, so the formula reads 2015's MAGI and scales it by index(2013) / index(2015). The index has no value before 2015, so `index_at` holds it at its 2015 value, and the factor is exactly 1. MAGI = 80,000, which is not above 85,000, so there is no IRMAA. The case supplies `base_part_b_premium: 1_258.8` directly: 12 × 104.90 = 1,258.80. Because of that input, case 6 doesn't test the new 2015 standard premium; `base_part_b_premium.yaml`'s 2015 case does.
- **Case 7** (2016). The year two back is 2014, and the source year is 2016. Factor = index(2014) / index(2016) = (9,033,840,000,000 / 326,078,000) / (9,033,840,000,000 / 328,964,000). The AGI cancels, because CBO total AGI has no 2016 value and is held at 2015's. So the factor is the Census population ratio 328,964,000 / 326,078,000 (`calibration/gov/census/populations/total.yaml:12-13`, read through `LOOKBACK_DEFLATOR = calibration.gov.cbo.income_by_source.adjusted_gross_income_per_capita` in `medicare_irmaa_magi_two_years_prior.py`). Result: `80000*328964/326078 = 80708.0514`. That is not above 85,000, so Part D IRMAA is 0. The test asserts 80,708.05 ± 0.01.
- **Should the lookback apply the factor?** For 2016 it scales income *up* by 0.885%, only because population grew while the held-flat AGI didn't. The statute wants the actual MAGI from two years prior. With no 2014 inputs, the model's documented convention is to run the dataset-extension index backward, which main set in #10106. This PR doesn't change it, and #10114 would replace the held-flat AGI with IRS SOI totals (factor 31,312.48 / 31,085.28). Either way, case 7 tests the convention and stays below the threshold.

### The defect at main

Reproduced, one enrollee, MAGI from two years prior supplied directly (`repro.py`, run against `origin/main` and against the head):

                   main: Part B   Part D  |  head: Part B   Part D  | hand (head)

2015 single 90,000 1782.0 0.0 | 1762.8 147.6 | 12×(104.90+42.00)=1762.80, 12×12.30=147.60
2018 separate 100,000 1782.0 0.0 | 5143.2 897.6 | 12×(134.00+294.60)=5143.20, 12×74.80=897.60
2019 single 600,000 6813.6 972.0 | 5526.0 928.8 | 12×(135.50+325.00)=5526.00, 12×77.40=928.80
2021 joint 200,000 1782.0 0.0 | 2494.8 147.6 | 12×(148.50+59.40)=2494.80, 12×12.30=147.60
2016 single 100,000 1782.0 0.0 | 2046.0 152.4 | 12×(121.80+48.70)=2046.00, 12×12.70=152.40
2020 single 50,000 1782.0 0.0 | 1735.2 0.0 | 12×144.60=1735.20


All match the body's table. The hand column uses the PR's parameter values; their agreement with the notices remains unconfirmed.

### Properties: what the hand-off checked by script versus what the PR's tests assert

| Hand-off check | Asserted by `test_irmaa_schedule_properties.py`? |
|---|---|
| Absolute single thresholds 2015–2019 (85,001 / 107,001 / 160,001 / 214,001; 2018's 133,501 / 160,001; 500,000) and 2020–2023 at $1,000 steps + 1 | **No.** Every property is relative. The YAML cases pin each tier's lowest MAGI, which catches a threshold set too high but not one too low by 1 (P2-1, M3). Nothing asserts the $1,000 rounding. |
| joint = 2 × single, except the $750,000 tier | Yes: `test_joint_thresholds_double_the_individual_ones`, 2015–2026, and joint amounts = single amounts. |
| head of household and surviving spouse = single | Yes: thresholds and amounts, both parts, 2015–2035. |
| Part D thresholds = Part B's | Yes, all statuses, 2015–2035. |
| separate = single at MAGI + T | Yes: at every edge ± 1 and at Hypothesis draws, 2015–2026. |
| each `.inf` tier repeats the amount below | **No.** Only non-decreasing amounts and, before 2019, `calc(1e9)` = the 80 percent amount. A wrong amount on an unreachable tier doesn't change any output, so this is harmless. |
| Part D: one base reproduces every tier | **Weaker.** `test_part_d_adjustments_are_in_proportion` scales each tier from the top one within $0.10, and never checks the base beneficiary premium itself. |
| Part B: one U per year | **Weaker.** It compares against the standard premium within $0.25 (P3-2). |

### Readers

- **Parameters.** `gov.hhs.medicare.part_b.irmaa.*` is read only by `gross_medicare_part_b_premium.py:27`. `gov.hhs.medicare.part_d.irmaa.*` is read only by `income_adjusted_part_d_premium_surcharge.py:31`. `gov.hhs.medicare.part_b.base_premium` is read only by `base_part_b_premium.py:14`. Tests that read them: `test_irmaa_schedule_properties.py`, `test_irmaa_first_modeled_year.py` and `tests/core/test_premium_input_uprating.py`.
- **`base_part_b_premium`** is read by `gross_medicare_part_b_premium`, `msp_part_b_premium_coverage`, `msp_benefit_value` (→ `msp`, `msp_cost`, `msp_federal_cost`) and `il_smib_person`.
- **`gross_medicare_part_b_premium`** is read by `medicare_part_b_premium`, `gross_medicare_part_b_premium_if_enrolled` (→ `medicare_cost`) and CA's `medicaid_optional_senior_or_disabled_countable_income`.
- **`medicare_part_b_premium`** is read by `spm_unit_health_insurance_premiums`, `medical_expense_health_insurance_premiums` and 23 state LIHEAP and energy-assistance income variables. `medical_expense_health_insurance_premiums` in turn feeds the federal itemized medical deduction, SNAP and HUD medical expenses, medically needy Medicaid, and the OH, ID, MT, PA and UT readers.
- **`income_adjusted_part_d_premium_surcharge`** is read by `spm_unit_health_insurance_premiums`.

**So results other than IRMAA and the Part B premium do move for 2015–2023 at baseline.** Witness, 2018, Illinois, one enrollee (`readers.py`): `msp_benefit_value` and `msp_part_b_premium_coverage` drop from 1,782.00 (main) to 1,608.00 (12 × 134.00). With $100,000 lag MAGI, `spm_unit_health_insurance_premiums` rises from 0 to 798.00, and `medical_expense_health_insurance_premiums` from 0 to 642.00. These moves are what the law implies, not defects. But the population check needs to cover MSP cost and the SPM poverty rate for 2015–2023, as well as IRMAA revenue, plus any deduction or LIHEAP output in a calibrated year in that range. None of these readers is made worse by this PR. The only text it makes wrong is P3-1.

### Tests run (one file per process, at the head)

| File | Result |
|---|---|
| `eligibility/medicare_irmaa_magi_two_years_prior.yaml` | 21 passed |
| `eligibility/gross_medicare_part_b_premium.yaml` | 33 passed |
| `eligibility/base_part_b_premium.yaml` | 11 passed |
| `eligibility/income_adjusted_part_d_premium_surcharge.yaml` | 45 passed |
| `eligibility/medicare_irmaa_magi.yaml` (unchanged, same variables) | 4 passed |
| `test_irmaa_schedule_properties.py` | 560 passed |
| `test_irmaa_lookback_and_switch.py` | 23 passed |
| `test_irmaa_first_modeled_year.py` (unchanged, reads the IRMAA scales) | 57 passed |

The Python files need `hypothesis`, which a plain `uv sync` leaves out. I installed it with `uv sync --extra dev`.

On generated ranges: the two lookback Hypothesis properties now draw years from 2015–2026, but stay at `max_examples=25` across 5 statuses × 12 years, so each year gets about two draws per run. The body says the count is unchanged, which is accurate; it is a thinner sample per year. `test_surcharges_are_zero_below_the_first_threshold` draws on both sides of the first threshold, including the threshold itself as a float32 lower bound, so that boundary is reached.

### Mutants (each in its own process, on a scratch worktree, restored after)

| Mutant | Result | Killed by |
|---|---|---|
| M1 (hand-off): 2015 Part B single first threshold 85,001 → 75,001 | killed | `medicare_irmaa_magi_two_years_prior.yaml` case 6: `gross_medicare_part_b_premium@2015: [1762.8] differs from 1258.8` |
| M2 (hand-off): 2016 Part D single first threshold 85,001 → 80,001 | killed | case 7: `income_adjusted_part_d_premium_surcharge@2016: [152.4] differs from 0.0` |
| M3 (mine): 2017 "more than $107,000" stored as 107,000 in single, head of household and surviving spouse, and 213,999 in joint, Parts B and D | **survives** | none (P2-1) |
| M4 (mine): 2021 Part B tier-1 amount 59.40 → 59.60 in the four non-separate files | killed | `gross_medicare_part_b_premium.yaml`, 4 cases. The property file passes it (P3-2). |
| M5 (mine): 2015 standard premium 104.90 → 105.00 | killed | `base_part_b_premium.yaml` 2015 case (1 failed). The property file passes it (560 passed), and case 6 passes too, since it supplies the premium as an input. |

`git status` in the scratch worktree is clean after each mutant.

### CI

Pull request run [41717469233](https://github.com/PolicyEngine/policyengine-us/actions/runs/41717469233) at `6232f6c64c`: **success**, 36 of 36 jobs `success` (none cancelled or skipped), including "Baseline tests / Full Suite - Baseline (contrib-hhs)" and "Household API Partners".

### Docs and body

- `impact: the hub will measure` and `axiom: (hub files pe-parity before landing)` are both present.
- The changelog fragment `changelog.d/irmaa-historical-brackets.fixed.md` is a top-level file and matches the change.
- The body's main-vs-branch table: reproduced (above).
- "It changes no 2024–2026 value": confirmed. `git diff origin/main...HEAD` on the ten IRMAA files and `base_premium.yaml` adds only 2015–2023 dates, references and comments, and removes one trailing-whitespace line. No `2024-01-01` or later value changes.
- "Each Part D table's thresholds equal that year's Part B thresholds": true in the YAML (asserted by test).
- The statutory structure the body describes (2015–2017 at $85,000 / $107,000 / $160,000 / $214,000; 2018's table; the 2019 85 percent tier at "at least $500,000", with joint at $750,000; the separate schedule reduced by the threshold) is what the YAML encodes, checked by script. **I couldn't read 42 U.S.C. 1395r(i) or 1395w-113(a)(7)**, so I can't confirm that the statute says it.
- The "Not modeled" section's quotes from the 2016 and 2017 notices: unverified (blocked).
- Scope: nothing under `.github/` or `policyengine_us/tests/policy/baseline/partners/`.

## Checked and sound

- The encoding: "more than $X" → X + 1 and "at least $X" → X. The 2019+ tier at 500,000, and the separate tier at 415,000 / 413,000 / 412,000 / 409,000 / 403,000, are each 500,000 minus that year's individual threshold, with no + 1. Unreachable tiers are `.inf` and come last.
- 2018 separate: a single reachable tier at the 80 percent amount from 85,001. That matches the threshold-reduction arithmetic the body describes (133,500 − 85,000 = 48,500 and 160,000 − 85,000 = 75,000, both below 85,000).
- Joint thresholds: j − 1 = 2 × (s − 1) for every finite tier below the 85 percent one, and 750,000 = 1.5 × 500,000, 2015–2026 (script and property test).
- Both formulas use `select` over all five statuses, so every status reaches its own schedule. Every new case gives `medicare_irmaa_magi_two_years_prior` directly, so the lookback doesn't touch the schedule cases.
- The PR merges cleanly with current main. Overlaps with open PRs: see P2-2.

## What I couldn't check

- Every value against the Federal Register notices and CMS memos and fact sheets, for every year 2015–2023, and whether each `#page=` opens the cited table. All of these hosts were blocked from this session.
- The text of 42 U.S.C. 1395r(i) and 1395w-113(a)(7), and the hold-harmless quotes.
- Whether `https://www.cms.gov/oact/tr/2026#page=215` (an HTML landing page with a `#page=` fragment) opens Table V.E3.
- Population impact: the hub measures that on data this session can't reach.


---
_Generated by [Claude Code](https://claude.ai/code)_

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

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


Generated by Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 2015-2023 single and joint cases tested each tier's lowest MAGI
(X + 1) but never X itself, so a "more than $X" bound stored as X
survived every test. Add a person at each X above the first threshold,
expecting the amount of the tier below, in Parts B and D.

base_premium.yaml now starts in 2015, so the Medicaid comment saying it
starts in 2021 is no longer true.

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

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: eaf0867ddaae37b1315be0fa74afffe752a1ade1

This round answers round 1's P2-1 and P3-1. It's built on 6232f6c64c, the head the brief named; nobody else pushed to the branch while I worked (checked before the push). I didn't touch the PR body or the impact: / axiom: lines.

Commits

Items

1. Pick-up and merge. I posted the pick-up comment and merged main without conflicts. Afterwards I reran all eight files (see Tests).

2. Sources. No parameter value changed. I took every new expected amount from the monthly amount the case's own comments already list, so the new people use the same figures as the hub's checked tables.

3. P2-1: the highest MAGI of each tier above the first. In the 2015–2023 single and joint cases of both files, I added a person at X for each "more than $X" bound above the first, expecting the amount of the tier below. Each one has a comment line in the case's own style, e.g. # MAGI 107,000, highest income in the 35% tier: $187.50 (35%) a month.

  • gross_medicare_part_b_premium.yaml: +54 people in 18 cases (still 33 cases).
  • income_adjusted_part_d_premium_surcharge.yaml: +54 people in 18 cases (still 45 cases).
  • MAGIs added, single (joint is twice each): 2015–2017 at 107,000 / 160,000 / 214,000; 2018–2019 at 107,000 / 133,500 / 160,000; 2020 at 109,000 / 136,000 / 163,000; 2021 at 111,000 / 138,000 / 165,000; 2022 at 114,000 / 142,000 / 170,000; 2023 at 123,000 / 153,000 / 183,000.
  • 499,999 and 749,999 were already covered. Every 2019–2023 single and joint case in both files already had a person one dollar under the "at least" tier, expecting the 80 percent amount (e.g. Part B gross_medicare_part_b_premium.yaml:968, Part D income_adjusted_part_d_premium_surcharge.yaml:1247). My script checks that each one is there, so I added none.
  • How I made the edit: a script parsed each case and asserted three things: all persons identical; each comment's MAGI equal to its tax unit's MAGI; each annual value equal to 12 × the comment's monthly amount. It then inserted the new people in MAGI order and renumbered the persons and tax units. No existing MAGI, comment or value changed; the only removed lines in the diff are existing tax units that moved down one slot.
  • Hand checks:
    • 2017 single, Part B (gross_medicare_part_b_premium.yaml:510): 12 × 187.50 = 2,250; 12 × 267.90 = 3,214.80; 12 × 348.30 = 4,179.60. These match the brief's example.
    • 2017 single, Part D (income_adjusted_part_d_premium_surcharge.yaml:810): 107,000 → 12 × 13.30 = 159.60.
    • 2023 single, Part B: 123,000 → 12 × 230.80 = 2,769.60, the case's own 35 percent line.

4. P3-1. I deleted the two-line NOTE that said base_premium starts in 2021, at former lines 77–78 of policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.py. base_premium.yaml:38 reads 2015-01-01: 104.90. ruff format --check reports the file as already formatted, and ruff check passes.

Mutants

Each mutant ran with one process per test file, at the new tests. After each, I restored the parameters with git checkout; git status was clean every time.

Mutant Result Killed by
M3: 2017 "more than $107,000" stored as 107,000 in Part B and D single, head of household and surviving spouse; 213,999 in Part B and D joint (8 files) killed (it survived round 1) Part B: 2 failed, 31 passed. The 2017 single case (MAGI 107,000 gives 3,214.8, expected 2,250) and the 2017 joint case (MAGI 214,000), gross_medicare_part_b_premium.yaml:510/:585. Part D: 2 failed, 43 passed. The same two people (410.4, expected 159.6), income_adjusted_part_d_premium_surcharge.yaml:735/:819. The property file still passes it (560 passed).
M6: 2019 Part B single "at least $500,000" stored as 499,999 (part_b/irmaa/single.yaml:158) killed Part B: 1 failed, 32 passed. In the 2019 single case, the 499,999 person gets 5,526, expected 5,200.8. The property file also fails 5 of 560, since single then differs from head of household and surviving spouse.
M1: 2015 Part B single first threshold 85,001 → 75,001 killed Lookback file: 1 failed, 20 passed (case 6, 1,762.8 against 1,258.8). Part B file: 1 failed, 32 passed.
M2: 2016 Part D single first threshold 85,001 → 80,001 killed Lookback file: 1 failed, 20 passed (case 7, 152.4 against 0). Part D file: 1 failed, 44 passed.

Tests (one file per process, after the merge, on the head's content)

File Result
eligibility/medicare_irmaa_magi_two_years_prior.yaml 21 passed
eligibility/gross_medicare_part_b_premium.yaml 33 passed (+54 people)
eligibility/base_part_b_premium.yaml 11 passed
eligibility/income_adjusted_part_d_premium_surcharge.yaml 45 passed (+54 people)
eligibility/medicare_irmaa_magi.yaml 4 passed
test_irmaa_schedule_properties.py 560 passed
test_irmaa_lookback_and_switch.py 23 passed
test_irmaa_first_modeled_year.py 57 passed

I installed hypothesis with uv sync --extra dev. I ran no full suites or microsimulations.

CI

Pull request run 41856781101 at eaf0867dda: not finished 60 minutes after the push (16:21Z), so I'm handing off with it still running. It had 28 of 36 jobs success and none failed. Among the successes were "Baseline tests / Full Suite - Baseline (contrib-hhs)", which runs the changed Medicare files, and "Household API Partners". Eight Contrib jobs were still queued or running (states shards 1–4, other shards 1, 2b and 3, and congress); they were held up by runner contention. The hub should read the run's final conclusion.

Judgment calls

  • For the 499,999 and 749,999 people, I kept the files' existing comment wording ("just below the 85% tier"), since every one was already there.
  • I extended only the single and joint cases, as the item asked. Head of household and surviving spouse share the single thresholds, and the property test asserts that equality. Separate filers have their own reduced schedule, and I didn't extend those cases.
  • Nothing changed under .github/ or policyengine_us/tests/policy/baseline/partners/.

Left for the hub

The PR body's Tests section (counts above), the impact: and axiom: lines, the population check, and the #10114 case 7 value when that PR lands.


Generated by Claude Code

@MaxGhenis

MaxGhenis commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Hub review in progress

Round 2 (delta) review of head eaf0867ddaae37b1315be0fa74afffe752a1ade1, unchanged since the brief. Interim findings, to be replaced by the final review:

  • CI. Pull request run 41856781101 at this head finished: 36 of 36 jobs success (attempt 1).
  • Delta. 7d858c4b08 (merge of main, no conflicts) → eaf0867dda touches three files: the two IRMAA YAML files and the deleted two-line Medicaid note (P3-1: done, and no other comment in the repo says base_premium starts in 2021).
  • The 108 added people. A script compared every case at 6232f6c64c and at the head. Every original person keeps its MAGI, filing status, age, expected value and comment, in order. The source lines and other outputs are unchanged, and the 12 older Part D cases are byte-identical. It found 54 added people per file, each at a tier top, each with value = 12 × its comment's monthly amount.
  • Coverage. For every finite threshold t above zero in each 2015–2023 single and joint case, both t − 1 and t are now present: 82 thresholds per file, none missing.
  • Body Tests sentences. Each one checks out against both files (case counts 33 / +33 / +6, statuses per year, $2,000,000 before 2019, sources named).
  • Sources. Still blocked from this session: govinfo.gov, federalregister.gov, cms.gov, ssa.gov, uscode.house.gov, ecfr.gov, law.cornell.edu, congress.gov and web.archive.org all refuse the connection.
  • Still running: the eight test files, mutants M1, M2, M3, M6 and M7 (mine), and a merge with Fill CBO total AGI for 2015-2018 with IRS SOI actuals #10114.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: APPROVE
eaf0867
METHODOLOGY_CHOICE: no

Independent review, round 2: a delta review from 6232f6c64c (round 1's head) to eaf0867dda. The head is the one the brief named, and nothing has been pushed since. The delta is a merge of origin/main at 25c4253483 (7d858c4b08, no conflicts) and one commit, eaf0867dda, which touches three files: gross_medicare_part_b_premium.yaml, income_adjusted_part_d_premium_surcharge.yaml and the Medicaid comment. No parameter changed. The head still merges cleanly with origin/main (git merge-tree).

Bottom line. Round 1's P2-1 and P3-1 are fixed, and P2-2 and P3-2 are answered in the body. The 108 added people leave every existing MAGI, comment and value unchanged. Every tier top in 2015–2023 single and joint cases is now pinned in both files. M1, M2, M3, M6 and my own M7 all die, each at the person it targets. CI at the head is 36 of 36 success. What remains is P3 only. The primary sources were still unreachable from this session, so the values against the notices still rest on the hub's comparison.

Findings

P3-1. Deleting the Medicaid note creates a new conflict with #10634, which the body doesn't list

policyengine_us/variables/gov/hhs/medicaid/income/medicaid_optional_senior_or_disabled_countable_income.py:75-77. #10634 ("Charge the Medicare Part B premium only to people who pay it", open, not a draft) rewrites the comment lines just above the note this PR deletes, and keeps the note. At round 1's head the two PRs merged cleanly; at this head they conflict:

$ git merge-tree --write-tree --name-only 6232f6c64c pr-10634   -> no conflict
$ git merge-tree --write-tree --name-only eaf0867dda pr-10634
CONFLICT (content): Merge conflict in .../medicaid_optional_senior_or_disabled_countable_income.py
$ git diff origin/main...pr-10634 -- <that file>
-        # enrollment, as in medical_expense_health_insurance_premiums.
+        # Only a Part B enrollee (medicare_part_b_enrolled) has a Part B
+        # premium to disregard.
         # NOTE: gov.hhs.medicare.part_b.base_premium starts in 2021, so a
         # person-paid Part B before 2021 reads the 2021 premium.

Failure scenario. Whoever merges second resolves the conflict by taking #10634's side. That keeps a note that is false once this PR lands. #10146 also conflicts in this file now (it deletes the same note itself, so taking either side drops it). #10146 already conflicts with main in ca_wdp_ssi_ssp_income_eligible.py, so it needs a merge anyway.

Replacement. Add to the body's "Related" list:

P3-2 (follow-up, carried from round 1's P3-3; main does the same). The separate schedule applies to every separate filer

Unchanged in this round. gross_medicare_part_b_premium.py and income_adjusted_part_d_premium_surcharge.py give every SEPARATE filer the reduced schedule, though the body heads it "Married, filing separately, lived with the spouse". Round 1's witness and proposed follow-up issue still stand.

The round 2 checks

The 108 added people

A script parsed every case in both files at 6232f6c64c and at the head and checked each case:

  • the name, period, source lines, other outputs and non-person inputs are unchanged;
  • every original person (filing status, MAGI, age, expected value and comment line) appears at the head in its original order;
  • MAGIs are ascending, each comment's MAGI equals its tax unit's MAGI, each expected value equals 12 × the comment's monthly amount, and the household lists every person.
gross_medicare_part_b_premium.yaml cases 33 added people 54
income_adjusted_part_d_premium_surcharge.yaml raw-identical 12 cases 45 added people 54
total 108

The 12 older Part D cases (2024–2026, in another form) are byte-identical. Each added person is a tier top, in a single or joint case, in the 35, 50 or 65 percent tier: for example, 2018 single 107,000 / 133,500 / 160,000 and 2023 joint 246,000 / 306,000 / 366,000.

Coverage. For each 2015–2023 single and joint case, against each finite threshold t above zero in the case's own parameter file:

gross_medicare_part_b_premium.yaml thresholds 82 missing []
income_adjusted_part_d_premium_surcharge.yaml thresholds 82 missing []

Both t − 1 (the highest MAGI of the tier below) and t (the lowest MAGI of the tier) are present for all 82 thresholds in each file. That includes 499,999 and 749,999 from 2019. Those were already present at 6232f6c64c: the script found none among the added people, which matches the hand-off.

Tier tops by hand, two years in each file. Part B is the standard premium plus (p − 25) / 25 × the standard premium, rounded to the dime. Part D is the base beneficiary premium × (p − 25.5) / 25.5, rounded to the dime. Bases are the PR's: $104.90 and $148.50 for Part B, and $33.13 and $33.06 for Part D.

Year Tier Part B: adj. → monthly → annual Expected in YAML Part D: adj. → annual Expected in YAML
2015 35% (single 107,000; joint 214,000) 41.96 → 146.90 → 1,762.80 1,762.8 12.343 → 147.60 147.6
2015 50% (160,000; 320,000) 104.90 → 209.80 → 2,517.60 2,517.6 31.831 → 381.60 381.6
2015 65% (214,000; 428,000) 167.84 → 272.70 → 3,272.40 3,272.4 51.319 → 615.60 615.6
2021 35% (111,000; 222,000) 59.40 → 207.90 → 2,494.80 2,494.8 12.316 → 147.60 147.6
2021 50% (138,000; 276,000) 148.50 → 297.00 → 3,564.00 3,564 31.764 → 381.60 381.6
2021 65% (165,000; 330,000) 237.60 → 386.10 → 4,633.20 4,633.2 51.211 → 614.40 614.4

All twelve match. I couldn't read the printed tables (see below), so this confirms the arithmetic and the parameter values, not the notices.

The deleted Medicaid comment (round 1's P3-1)

Deleted at eaf0867dda (two lines, nothing else in the file changed). base_premium.yaml:38 reads 2015-01-01: 104.90. ruff format --check reports "1 file already formatted" and ruff check reports "All checks passed!". A repo-wide search found no other comment saying base_premium starts in 2021, or that years before 2024 read the 2024 schedule. One test comment mentions "first applies in 2021" (medicaid_optional_senior_or_disabled_countable_income.yaml:352), but it is about the disregard's start date and is still true. The conflict this deletion creates is P3-1 above.

The body's new Tests sentences

Each one, checked by script against both files at the head:

Sentence Result
gross_medicare_part_b_premium.yaml has 33 cases (new file) true: 0 on main, 33 at the head
income_adjusted_part_d_premium_surcharge.yaml gains 33 cases, base_part_b_premium.yaml six true: 12 → 45 and 5 → 11
Cases reach the first threshold itself and each tier's lowest MAGI true for every 2015–2023 case, all statuses, both files (no misses)
All five filing statuses in 2016, 2019 and 2021; single, joint and separate in the other years true, both files
Single and joint cases reach each tier's highest MAGI; at "more than $X" a MAGI of X pays the lower tier true (82 of 82 per file, above). The sentence sits after the single-and-joint clause, so it reads as limited to them, which is accurate
From 2019, $1 under $500,000 ($750,000 joint) pays the 80 percent amount true, every 2019–2023 single and joint case, both files
A MAGI of $2,000,000 before 2019 true: every 2015–2018 case in both files has one
Each case names its source table, with its page where the source has pages true. The 2022 and 2023 Part D cases cite the CMS fact sheets, web pages without pages
Properties: 560 cases; Part B amounts only within $0.25, "so the YAML cases, not this property, pin each amount" true: 560 passed, and this answers round 1's P3-2. M3 and M7 below also pass the property file, which bears that out

The body's Related list now names #10114 and its case 7 value (round 1's P2-2).

Overlap with #10114: case 7's value

#10114 is still open and unmerged (head 7ef425eccd), so at this head case 7 still expects 80,000 × 328,964 / 326,078 = 80,708.0514. The two PRs still conflict in medicare_irmaa_magi_two_years_prior.yaml. I made the merge in a scratch worktree, took #10114's side, and set cases 6 and 7 back to this PR's $80,000 with case 7 at 80,584.71:

80000 * 31312.48 / 31085.28                                         = 80584.714
80000 * (10,210,310,102,000/326,078,000) / (10,225,938,479,000/328,964,000) = 80584.705
merged file: 23 passed

The rounded per-capita ratio gives 80,584.71 and the unrounded totals give 80,584.70. Both fall within the case's 0.01 margin. Case 6 stays at 80,000, because 2013 and 2015 both read the 2015 index value and the factor is exactly 1. Part D IRMAA stays 0, because 80,584.71 is not above $85,000. So the body's case 7 value is right.

CI

Pull request run 41856781101 at eaf0867dda (attempt 1) completed with conclusion success: 36 of 36 jobs success, none cancelled or skipped.

Verify by running

1. The defect

Round 1 reproduced it at main, and this round changes no parameter or formula. The delta is tests and one comment, so I didn't rerun it.

2. The rule

This round adds expectations only. The hand values above match the parameter files and the case comments. The formulas didn't change.

3. The tests, one file per process at the head

File Result
eligibility/medicare_irmaa_magi_two_years_prior.yaml 21 passed
eligibility/gross_medicare_part_b_premium.yaml 33 passed
eligibility/base_part_b_premium.yaml 11 passed
eligibility/income_adjusted_part_d_premium_surcharge.yaml 45 passed
eligibility/medicare_irmaa_magi.yaml 4 passed
test_irmaa_schedule_properties.py 560 passed
test_irmaa_lookback_and_switch.py 23 passed
test_irmaa_first_modeled_year.py 57 passed

These match the hand-off's counts. The new people assert the claim itself: each sits exactly at X for "more than $X", and expects the lower tier's exact amount.

4. Mutants

I ran each mutant on a scratch worktree at the head, one process per test file, and restored the files after each (git status --porcelain showed 0 changed files every time).

Mutant Lookback Part B YAML Part D YAML Properties Killed by
M1: 2015 Part B single first threshold 85,001 → 75,001 1 failed 1 failed 45 passed 5 failed case 6 ([1762.8] differs from 1258.8), and the 2015 single Part B case at MAGI 85,000
M2: 2016 Part D single first threshold 85,001 → 80,001 1 failed 33 passed 1 failed 5 failed case 7 ([152.4] differs from 0.0), and the 2016 single Part D case at MAGI 85,000
M3: 2017 "more than $107,000" stored as 107,000 in single, head of household and surviving spouse, and 213,999 in joint, Parts B and D (8 files) 21 passed 2 failed 2 failed 560 passed the new 107,000 single and 214,000 joint people. Part B: [1608. 2250. 3214.8 …] differs from [1608. 2250. 2250. …]; Part D: [0. 159.6 410.4 …] differs from [0. 159.6 159.6 …]. It survived round 1
M6: 2019 Part B single "at least $500,000" stored as 499,999 21 passed 1 failed 45 passed 5 failed the 499,999 person in the 2019 single case: … 5200.8 5526. 5526.] differs from … 5200.8 5200.8 5526.]
M7 (mine): 2020 "more than $163,000" stored as 163,000 in single, head of household and surviving spouse, and 325,999 in joint, Parts B and D (8 files). The joint value keeps j − 1 = 2(s − 1), so the doubling property holds 21 passed 2 failed 2 failed 560 passed the new 163,000 single and 326,000 joint people (Part B 4512. 5552.4 vs 4512. 4512.; Part D 608.4 840. vs 608.4 608.4)

M3 and M7 pass the property file. So the new YAML people are the only guard against an off-by-one at a tier top, which is what P2-1 asked for.

5. Readers

The delta changes no variable or parameter, so the readers are the ones round 1 listed. A grep at the head confirms them: part_b.irmaa is read only by gross_medicare_part_b_premium.py and part_d.irmaa only by income_adjusted_part_d_premium_surcharge.py. base_part_b_premium is read by gross_medicare_part_b_premium, msp_benefit_value, msp_part_b_premium_coverage and il_smib_person. gross_medicare_part_b_premium is read by medicare_part_b_premium, gross_medicare_part_b_premium_if_enrolled and medicaid_optional_senior_or_disabled_countable_income. income_adjusted_part_d_premium_surcharge is read by spm_unit_health_insurance_premiums. The main merge brought in no new Medicare reader: the only Medicare-related files in 6232f6c64c..eaf0867dda are the three this round edits, plus CHANGELOG.md and source_conflicts/candidates.yaml, neither of which changes a Medicare entry.

6. Docs and body

impact: the hub will measure and axiom: TheAxiomFoundation/rulespec-us#1677 queued are both present. This round can't move any baseline output: it changes only test files and a comment. The PR as a whole moves 2015–2023 Part B premiums, IRMAA, MSP value and cost, and SPM premiums, as round 1 listed. The population check should cover those for 2015–2023.

7. Scope

Nothing under .github/ or policyengine_us/tests/policy/baseline/partners/ (git diff --name-only origin/main...eaf0867dda). Open-PR overlap at this head (git merge-tree):

PR Conflict with this head
#10103 (draft) the eight files the body names, unchanged from round 1
#10114 medicare_irmaa_magi_two_years_prior.yaml, in the body; resolution checked above
#10634 new: the Medicaid file (P3-1)
#10146 the Medicaid file, new, but #10146 deletes the same note. It also conflicts with main in ca_wdp_ssi_ssp_income_eligible.py
#10532 ca_state_supplement.yaml, which it also conflicts with on main; not this PR's
#10111, #10141, #10631 none

Checked and sound

  • Round 1 P2-1: fixed, with the 82 of 82 coverage per file above, M3 killed and M7 killed.
  • Round 1 P2-2: the body's Related list names Fill CBO total AGI for 2015-2018 with IRS SOI actuals #10114 with the right case 7 value. The merge passes (23 cases).
  • Round 1 P3-1: the note is deleted, and ruff is clean.
  • Round 1 P3-2: the body says the property checks amounts only within $0.25 and the YAML pins them.
  • The hand-off's claims: the merge had no conflicts; +54 people per file with case counts unchanged; 499,999 and 749,999 already present; the cited line numbers (gross_medicare_part_b_premium.yaml:510, :585, :968; income_adjusted_part_d_premium_surcharge.yaml:735, :810, :819, :1247) point where it says; the mutant results reproduce, including the property file passing M3 and failing M6 5 of 560.
  • The author brief's items 1 and 3–6 are done, and item 2 changed no value. Item 7 was left to the hub. The hand-off extended only single and joint cases, which is what item 3 asked for.

What I couldn't check

  • Sources. This session reached none of them: curl to www.govinfo.gov, api.govinfo.gov, www.federalregister.gov, uscode.house.gov, www.ecfr.gov, www.cms.gov, www.ssa.gov, www.law.cornell.edu, www.congress.gov and web.archive.org each failed with CONNECT tunnel failed, response 403, and the fetch tool couldn't resolve www.govinfo.gov. So I confirmed no tier top or amount against its printed table. I quote no statute, and the body's "all 1,240 values match" rests on the hub's comparison. My hand values check the arithmetic and the parameter files only.
  • The text of 42 U.S.C. 1395r(i) and 1395w-113(a)(7).
  • axiom: TheAxiomFoundation/rulespec-us#1677: that repository is outside this session's access.
  • Population impact: the hub measures that on data this session can't reach.

Generated by Claude Code

Cases 6 and 7 keep the $80,000 MAGI below the 2015-2016 threshold, on the
per-capita AGI deflator that main now uses. Case 7 expects 80,584.71.

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

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#10112 at db993a82298314f10d756a13f823b767208e1bd5 (squash): Add Medicare's 2015-2023 IRMAA brackets and premiums.

🤖 Generated with Claude Code

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