Skip to content

Add configurable Social Security COLA means test - #10486

Merged
MaxGhenis merged 82 commits into
mainfrom
ss-cola-means-test-e3
Nov 1, 2026
Merged

MaxGhenis merged 82 commits into
mainfrom
ss-cola-means-test-e3

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Adds an opt-in Social Security COLA means test shared by reported benefits and the supplied-record household calculator. It uses prior-year MAGI excluding Social Security, configurable indexed income lines, cohort or all-beneficiary scope, and statutory or chained CPI-U COLAs. The reform defaults off.

Stack and main merge

Both prerequisites, #10400 and #10401, are merged. The original stack and subsequent main merges remain in this branch's history; no history was rewritten. The earlier main merge retained this reform's changes in the overlapping benefit-lever files and brought in #10484. This round merges main cbe322b51c6c9f791a318a32f0b093abee683fab in merge commit 7f3724a092b7ad4301fc595e12487738be61b5d7. It brings upstream California CAPI/SSI and state LIHEAP changes, SSA YAML routing to other-shard-3, and behavioral-test speedups. The CAPI and SSI changes do not touch the COLA route.

The earlier continuation corrected the calculated-base fixture, preserved the disabled-policy helper path, restored the calculator's legacy exact-year channel, and separated automatic decisions from per-person overrides. This round fixes the overrides' interactions with income-history inference and dataset extension and strengthens the existing isolation and builder coverage.

Policy mechanics and choices

OACT A9, 2026 Trustees Report basis, directs: “Use prior tax year income data for this determination.” A9 withholds COLAs above CPI-U-indexed lines, pays chained CPI-U below them, and uses chained CPI-U before receipt. The annual TaxUnit variable social_security_cola_means_test_magi excludes taxable Social Security and retains applicable deductions and IRMAA addbacks, following the linked memorandum, provision 5 and H.R. 6489 section 103(d). January payment year y reads income and filing status from tax year y−1.

The configurable lever defaults to statutory COLAs below the line and first-eligible cohorts from 2026. Chained CPI-U and all beneficiaries are selectable. A separate pre-receipt index defaults to following the below-line choice; pre-receipt events use that index without an income test. The a9 preset selects all beneficiaries from the December 2028 COLA, chained CPI-U below the line and before receipt, and $85,000/$170,000 lines in 2018 dollars indexed by CPI-U. Its first affected checks are January 2029.

Effective year/month, cohort start, filing groups, lines, dollar base year, CPI-U lag and rounding are parameters. The 2026 first-eligibility boundary is a configurable scenario choice, as documented by first_eligible_year.yaml. Evaluated prior-year decisions require positive scenario lines; the defaults are neutral zero placeholders. Never/always sensitivities and pre-receipt index-only events do not read those lines.

Only income strictly above a line withholds the COLA; equality is paid. Single, head-of-household and separate filers use the single line; joint filers use joint. Surviving spouses default to joint through a configurable mapping because A9 does not name that status. Numeric parameter selectors preserve compatibility with Core's parameter engine.

Each exposed event uses one joint numerator: 1 if withheld, otherwise 1 + adjusted_COLA, divided by 1 + statutory_COLA. An active A5 alternative rate overrides the selected index; the percentage-point change is then added with a zero floor. Withholding overrides the entire increase. It does not reset the index high-water base or create a catch-up COLA. Neither withholding nor a downward adjustment reduces the nominal benefit level. Disabling the policy later preserves earlier effects.

Calculated public routes bypass population factors already applied in the calculator's ledger. Reported routes retain the stable multiplication order. Across-the-board and payable factors apply once, before the calculator's earnings test. Receipt starts at the first check month, and payment outputs avoid unnecessary next-January income reads.

Override behavior and review fixes

The annual Person inputs social_security_retirement_cola_override, social_security_disability_cola_override, social_security_survivors_cola_override, social_security_dependents_cola_override and ss_calculator_cola_override use -1=automatic (default), 0=paid, and 1=withheld. Each component's social_security_X_cola_means_test_withheld variable resolves its automatic decision plus its override. With the reform enabled, the calculator uses its own override, then the component override, then the automatic decision. The legacy boolean inputs remain the A5 channel when the reform is disabled; when it is enabled, automatic/new override decisions take precedence over those booleans.

These controls are exact-year decisions. Integers outside -1, 0 and 1 are accepted by Core's input storage and raise ValueError when an active exact-year decision reads them. Core truncates floats to integers first: 0.5→0, 1.5→1, and -1.5→-1. A multi-year input such as year:2027:2 creates no exact annual decision for either year. A yearless scalar uses the simulation's default input year. Calculating a default value does not turn it into supplied input, and people with no explicit control in a situation retain automatic decisions.

Current-law Medicare compatibility. earliest_input_year excludes the four new component controls by their exact names, so supplying a neutral or nonneutral decision in another year does not change IRMAA's income lookback. The calculator control is already excluded by its existing prefix rule. The exclusion does not include the legacy social_security_*_cola_withheld inputs: they retain main's behavior. With the reform at its defaults, adding a new control leaves current-law calculation outputs unchanged. The regression pins the 70-year-old, $500,000-earnings 2027 case at $463,100.31 IRMAA MAGI / $7,790.40 annual Part B premiums, including historical neutral and nonneutral component controls; generated-year/value coverage checks the disabled-policy invariant across the supported routes.

Dataset extension. Single-year dataset extension resets all five new controls to -1 in generated future rows. Every year the source dataset actually supplies keeps its supplied decision, including explicit multi-year datasets; the audit found no generator that extends those supplied multi-year rows. Legacy boolean columns retain main's extension behavior. The 2027-only paid-decision regression pins generated 2028 benefits at $12,352.54 reported / $1,036 for the January calculator check rather than paying the previous year's explicit decision again.

Per-person isolation. The existing override simulations now put A and B in the same tax unit and C in another. B's paid override leaves A's automatic withholding intact at $11,583.01 reported / $1,000 for the January calculator check. The same simulations exercise both within-unit and across-unit isolation, without another model setup.

Legacy calculator compatibility. With the reform disabled, the calculator reads only ss_calculator_withhold_cola, in its exact year, as on main. Component boolean inputs keep their existing carry-forward behavior on the reported route. The regression pins main's January 2027–2030 calculator checks at $1,036, $1,062, $1,086 and $1,111. Literal compatibility expectations come from main; direct calculator expectations and calculated public-source equality remain separate checks because main lacks the new public-source selector.

Test cost. The former one-case social_security_cola_means_test.yaml is removed. Its case remains covered through Core's SimulationBuilder in the Python means-test file, sharing the existing calculated-source reform system. The merged Makefile routes all SSA YAML to other-shard-3, so removal avoids a serial SSA-file invocation there. The review's 65–66-second file runs, including approximately 47 seconds of startup, measured the previous head on its earlier other-shard-1 routing; current savings have not been measured. Compatible retirement cases share one vector simulation with unchanged expected values and no additional reform. No workflow, matrix, concurrency or timeout setting changes.

CPI scoring assumption

The line index reads gov.bls.cpi.cpi_u at December 1 of year y − L and December 1 of the base year. This returns a BLS December figure where one exists, otherwise the latest authored value. It does not construct a separate December projection series. The current authored path returns 332.568 at 2026-12-01, June 2026's BLS observation, and 348.3 at 2028-12-01, CBO's value keyed at 2028-02-01 and annotated as the 2029 value. Tests pin both reads and the A9 preset's 2029 lines of $117,800 single / $235,700 joint. OACT's displayed lines are $117,300 / $234,600 under its CPI projection. No shared BLS CPI parameter file changes.

This December lookup is a scoring assumption: a single filer at $117,500 falls below PolicyEngine's line and above OACT's line, so the projected paths can make different withholding decisions.

Chained CPI-U uses Q3 lookups and half-up rounding to 0.1 percentage point with the high-water/no-decrease rules. The review separately found that chained_cola_rates splices June-2026 BLS levels onto CBO annual averages, producing a 1.2% January 2028 chained COLA. This PR reaches that function when chained CPI-U is selected for an active, in-scope event; default statutory settings do not. A9 begins with January 2029 checks. The Q3 splice remains a separate follow-up.

Validation

For reform scoring, the focused -k inventory guard passed, matches registered COLA overrides to invariance and dataset reset coverage, verifies the exact component exclusions and calculator prefix, and rejects a scratch social_security_auxiliary_cola_override registration.

The current head is b33afe8dba0ac6a948500d1ba096c4e62528e61d. All 260 required local cases passed, along with formatting and all six mutation checks. Current-head CI is green. The required wide differential remains incomplete; the partial results below do not establish full equivalence. Historical comparisons remain separate from this round's evidence.

Required check This round
test_ss_cola_means_test.py, disjoint split runs 49 passed across four processes; collection audit confirms all 49 cases ran exactly once, with no overlap
test_social_security_inputs.py 126 passed in 834.66 pytest seconds; measured peak RSS 3,112,501,248 bytes
test_social_security_lever_properties.py 24 passed in 186.08 pytest seconds; 510.5 seconds since plugin import; measured peak RSS 909,737,984 bytes
New IRMAA/default-invariance tests 13 passed in a clean full-file run, 307.64 pytest seconds / 574 process seconds / peak RSS 1,505,673,216 bytes
New dataset-extension tests 6 passed in 92.50 pytest seconds
Retirement lever fixture, using completed-case cleanup evidence hook 9 collected cases passed, covering all 10 original scenarios after vector consolidation; 198.74 pytest seconds; measured peak RSS 2,932,948,992 bytes
Six #10484 lever files 33 passed: dependents 4 / disability 4 / survivors 2 / retirement 8 / claiming 5 / eligibility 10; measurements below
make format Passed, three fix-round runs
Default-route differential: 30 Python cases and 42 YAML cases, plus IRMAA and extended-dataset cases Incomplete: Python head 12 / main 16 of 32 required cases; YAML head 37 / main 25 of 42. The 12 paired Python cases have no unexpected differences; 25 paired YAML cases have only the disclosed retirement-case difference. Four serial captures reached their time caps. Details below
CI at the new head, including the latest Pull request run's conclusion Green: latest exact-head Pull request run 41175342483, attempt 1 concluded success; 35 checks / 33 jobs terminal, none failed, cancelled or pending, confirmed at 07:14:45 UTC on October 31

The initial default-invariance run exposed a Medicare fixture-date error. After explicitly dating the head status to 2027, the clean full-file rerun passed all 13 cases. Earlier partial/focused passes are excluded from the 260-case primary total.

The split-run audit confirms that the tested commit, current commit and working test file agree. Runtime logs record the source path and collected node IDs; they do not themselves print a test-file hash. Local split measurements include process startup:

Means-test partition Passed Pytest seconds Process seconds Peak RSS
Shared-system cases 17 165.14 403 3.332 GiB
Calculated-source and legacy-channel cases 5 272.65 475 1.983 GiB
Sensitivity, pre-receipt and preset cases 8 349.85 486 1.858 GiB
Initial mechanics and income cases 19 204.68 286 2.569 GiB

Completed #10484 file runs use the same completed-case cleanup hook, with one release per passed case:

Lever file Passed Pytest seconds Process seconds Peak RSS bytes
Dependents 4 88.86 700 2,937,176,064
Disability 4 103.18 808 2,939,666,432
Survivors 2 182.33 660 2,936,160,256
Retirement 8 442.32 633 2,932,457,472
Claiming 5 313.42 523 2,989,473,792
Eligibility 10 364.68 501 2,931,671,040
Mutant against real simulations This round
Revert the disabled-policy helper fix (M4) Killed: one test fails on the incorrectly read, irrelevant entitlement month; 92.61 pytest seconds
Restore component-input carry-forward in the disabled-policy calculator (M7 inverse) Killed: one test finds January 2027 at $1,000 instead of $1,036; 277.47 pytest seconds
Restore shared automatic/override input storage (F1) Killed: two isolation tests fail, one per route; 93.03 pytest seconds
Broadcast a person's override within the tax unit Killed: two isolation tests fail, one per route; 82.78 pytest seconds
Omit one component control from the earliest_input_year exclusion Killed: both neutral and nonneutral retirement controls change the inferred year to 2020 instead of 2027; 2 failed / 8 passed in 67.52 pytest seconds
Carry an override into a generated future dataset row Killed: the generated-row test finds copied 0 and 1 instead of automatic -1; 7.94 pytest seconds

All six mutant runs completed with ordinary test failures under the 2 GB / 900-second cap; none hit a memory or time limit. Each mutation ran in an isolated source tree; the fixed branch source remained unchanged and hashes were checked.

This round's differential against merged main cbe322b51c6c9f791a318a32f0b093abee683fab remains incomplete. Python captured 12 head / 16 main cases out of 32 required, leaving 20 head / 16 main cases missing. The 12 paired cases / 1,470 outputs / 347,367 main cached arrays have zero unexpected differences, with 14 allowed new raw override-input cache entries (4 IRMAA / 10 extended dataset). Both new integration cases are included; their separate pair matches 62 outputs / 18,204 arrays. YAML captured 37 head / 25 main cases out of 42, leaving 5 head / 17 main cases missing. Its 25 paired cases / 301 outputs / 150,581 main cached arrays have only retirement case 9's 120 allowed differences: 3 outputs / 117 cached arrays. Both literal pins were checked: main $8,800, head $10,000. The strict final verifier returns failure solely for missing coverage, with zero unexpected differences or unchanged-output exceptions. Full 32-case Python / 42-case YAML equivalence is not established.

Four serial differential capture processes reached the existing 1,800-second time cap. No cap was raised, and all model processes have ended; the remaining head catch-up runs were not started. Saved captures and hub resume instructions are available to complete the missing cases without changing the fixed source. These incomplete comparisons are separate from the 260 passing local cases, six killed mutants, and green CI.

The round-2 review previously compared the old head against main 37dd5099ed7fad694419fb652eb3ba7bd14122ba: 30 Python cases / 4,100 outputs / 1,003,099 cached arrays matched exactly, with no new cache entries. Its 42 YAML builder cases / 466 outputs / 254,321 cached arrays differed only in the already disclosed retirement case 9. That nondefault case supplies a complete calculated base of $10,000, an across-the-board rate of 0.1 and payable share of 0.8: main returns $8,800, while this branch returns the complete supplied $10,000. Its three output differences and 117 cached differences accounted for all 120 YAML differences. Those historical results do not complete the current rerun.

Earlier composition with #10484 matched the reported route's reported benefit × PIA ratio × joint COLA factor: $23,695.91 withheld / $24,584.51 paid. The calculator applied the factors once, producing annual amounts of $12,660 / $13,140, with population factors equal to 1. Those composition amounts remain prior-review evidence; the current-head test results appear above.

The previous-head Pull request run 41067837131 concluded cancelled when other-shard-1 reached its existing 60-minute limit. Its Contrib YAML phase passed and the Python phase reached 79% with no recorded failures before cancellation; that run was not green. A zero exit status from gh pr checks does not establish that every check succeeded.

The current-head run above concluded success. other-shard-1 completed successfully in 1,743 seconds (29m03s), from 06:40:50 to 07:09:53 UTC. The watcher confirmed the latest Pull request run's exact head and terminal conclusion after 14 polls / 3,947 seconds, respecting the five-minute polling minimum and 100-minute limit.

Local Python checks run one foreground process at a time, with four numeric threads and load1 at or below 100 before startup. Tests retain the 4 GB / 1,800-second cap; mutants retain 2 GB / 900 seconds. The retirement rerun uses the evidence-only completed-case cleanup hook because earlier ordinary retirement runs exceeded 4 GB: it releases completed simulations and reform-cache references after assertions without changing situations, formulas, assertions or expected values. It released all nine completed cases and passed in 552 process seconds (04:44:21–04:53:33 UTC), with 198.74 pytest seconds and a 195.12-second plugin timer. This local hook does not cover cross-case reform-cache reuse. No population microsimulation is run locally. The clean 13-case default-invariance run took 574 process seconds (05:57:38–06:07:12 UTC), with 307.64 pytest seconds and a 529.56-second plugin timer. Local measurements are not a controlled before/after comparison.

Scoring limits

Population scoring uses a static-cross-section prior-income proxy: the same record's dataset-extended y−1 copy, not observed longitudinal income. Complete supplied histories override it; MAGI/source and filing-status arrays must supply every TaxUnit's value for each supplied variable/year. The engine does not carry a person's income, cohort or composition across projected rows. Use income_sensitivity=1 and 2 for the never/always envelope while retaining automatic timing, cohort and receipt restrictions.

Missing entitlement and worker information retains A5's documented exposure proxies. Identified records can supply component worker, eligibility, entitlement and first-COLA fields. Integer-age uncertainty calls for adjacent-cohort sensitivities. Auxiliary PIA inversion without worker/share information remains unidentified.

The calculator retains its existing scope of own retirement and restricted survivors, with its source, history and scope assertions. Disability, spouse, child, minimum and dual-entitlement calculations remain outside that route. See the calculator guide, benefit-lever guide, and means-test guide.

No population impact or OACT solvency result is inferred from these mechanics. Population scoring requires pinned policy settings, data and sensitivity results.

impact: none under current law (contributed reform); the hub pair measured no change in 2026 or 2030

axiom: n/a: contributed reform, not enacted law

Generated with Codex.

Hub microsimulation impact (current head)

A real microsimulation on the default dataset, main at the merge base cbe322b51c against this PR's head b33afe8dba, for 2026 and 2030. The runs went one at a time under the shared heavy-job lock. Sums are weighted.

Output 2026 change 2030 change 2026 records changed (weighted) 2030 records changed (weighted)
social_security +0 +0 0 (0) 0 (0)
state_income_tax +0 +0 0 (0) 0 (0)
income_tax +0 +0 0 (0) 0 (0)
household_net_income +0 +0 0 (0) 0 (0)
household_benefits +0 +0 0 (0) 0 (0)

The reform stays off by default. The pair ran at b33afe8; the later commit changes one test file.

Comparison files are in fixes/10486-impact/ on the hub host.

# Conflicts:
#	policyengine_us/tests/code_health/test_rest_python_groups.py
@MaxGhenis MaxGhenis changed the title WIP: Add configurable Social Security COLA means test Add configurable Social Security COLA means test Oct 29, 2026
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 29, 2026 11:33
@MaxGhenis
MaxGhenis merged commit 0aa80e3 into main Nov 1, 2026
36 checks passed
@MaxGhenis
MaxGhenis deleted the ss-cola-means-test-e3 branch November 1, 2026 07:08
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#10486 at 0a6dedd02abec864969f0444f612d216989733af (squash): add the COLA means-test reform.

  • Reviews: GPT-6.1 Sol on Subfleet hard-tier lanes reviewed this PR in four rounds. Round 3 resolved all round 2 findings at b33afe8dba and left one P3, a guard against future controls entering input-history inference. Round 4 approved this head with no findings. The delta since round 3 adds 39 lines to one test file. The guard reads names ending _cola_override, so a future control with another suffix would escape it; all five current controls are covered.
  • Gates checked live at merge:
    • gh pr checks exits 0;
    • the latest Pull request run at the head (41277704272) concluded success;
    • MERGEABLE;
    • not a draft;
    • no reviewer's latest review requests changes;
    • head pinned.
  • Methodology: Sol marks one methodology choice, which the body discloses: PE's premium lines use the latest authored December CPI-U lookup, so they read $117,800 and $235,700 against OACT's $117,300 and $234,600. This applies only inside the off-by-default reform. The third-quarter splice stays outside this PR.
  • Impact: none under current law. The full-file pair at b33afe8dba against cbe322b51c for 2026 and 2030 changed no record on any measured output. The later commit changes one test file. The description carries the table.
  • Axiom: n/a, a contributed reform and not enacted law.

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.

1 participant