Skip to content

Fix Social Security secondary earnings threshold reform - #10380

Merged
MaxGhenis merged 4 commits into
mainfrom
fix-secondary-earnings-threshold-donut
Oct 24, 2026
Merged

MaxGhenis merged 4 commits into
mainfrom
fix-secondary-earnings-threshold-donut

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Fixes the CBO Social Security secondary earnings threshold (donut) reform. Closes #10376.

Problem and fix

All four defects were reproduced by executing the upstream formula bodies:

  • Negative SECA: $400,000 wages in 2027 produced -$18,600 Social Security SECA, canceling the additional wage tax. Clamp the baseline's remaining cap room at zero.
  • Overlapping bases: with the cap above the threshold, $400,000 wages produced a $413,100 wage base. Start the secondary base above max(threshold, cap).
  • Missing self-employment earnings: $300,000 wages plus $50,000 gross SE produced -$6,200 SECA instead of taxing $46,175 net SE. Update the reform's SE variable so wages fill the combined base first.
  • Future activation: the factory checked only the default 2024 start instant, missing a threshold beginning in 2026. Scan every dated threshold value for a finite value, following CA Proposition 3's dated-value handling rather than a fixed five-year lookahead. Each calculation still reads its own period's threshold.

For wages W, net SE earnings S, cap C, and U = max(threshold, C):

wage base = min(W, C) + max(0, W - U)
SE base   = min(S, max(0, C - W)) + max(0, S - max(0, U - W))
combined  = min(W + S, C) + max(0, W + S - U)

An infinite threshold preserves current-law results. The baseline clamp also preserves current-law results because baseline taxable wages never exceed the cap.

Sources

CBO's 2024 option 60955, dated December 12, 2024, applies the second alternative to wages and net self-employment earnings above a fixed $250,000 threshold alongside earnings below the current-law cap. It states that “the $250,000 threshold would not change”. Scheduled benefits continue to use the current-law cap. The parameter and both reform variables now cite this option; the SE override also cites 26 USC 1402(b) for wages-first allocation.

OACT E2.5 says to “tax all earnings once the current-law taxable maximum exceeds $250,000.” The model's current-law cap parameter is $254,400 in 2035, from CBO's January 2025 forecast. A fixed $250,000 threshold therefore closes the donut in 2035 in this model, one year before the 2036 date in CBO's 2024 text.

Preserve state paid-leave contribution bases

The reform's additional federal taxable earnings previously leaked into Connecticut employee and Delaware employee/employer paid-leave contributions. Each of these three variables now computes min_(payroll_tax_gross_wages, gov.irs.payroll.social_security.cap) directly. This matches the baseline federal wage formula exactly and continues to follow parameter changes to the federal contribution and benefit base.

Invariant: holding other inputs and parameters fixed, Connecticut employee and Delaware employee/employer paid-leave contributions are identical with and without the secondary-threshold reform, for every input. The existing Hypothesis property now checks these actual state formulas for every generated wage/cap/threshold vector, with varying contribution rates and employee shares, within the same 150-example budget and without additional model construction.

For 2027 wages of $400,000, threshold $250,000, and federal cap $192,300, CT remains $192,300 × 0.5% = $961.50. DE's full rate is 0.4% + 0.08% + 0.32% = 0.8%; at the default 50% employee share, each contribution remains $192,300 × 0.8% × 50% = $769.20. New variable-named contrib YAML cases activate the reform through reforms: and also assert the enlarged federal wage base of $342,300.

The existing baseline YAML tests of all three state variables are in state_paid_leave_premiums_2027.yaml. They supply employment_income; none supplies taxable_earnings_for_social_security, so no input conversion is needed. The existing CT/DE tests in tests/core/test_payroll_contributions.py also derive wages from employment income and do not input the individual taxable-earnings variable. No other reader of that individual wage-base variable exists outside federal payroll/SECA after these three fixes. CO, MA, ME, OR and WA read the separate employer-total taxable-earnings input, which this reform does not change.

Hand calculations and coverage

With a $250,000 threshold:

Case Wage base SE base Employee + employer + SS SECA
2027: $400,000 wages; $192,300 cap $192,300 + $150,000 = $342,300 $0 $342,300 × 12.4% = $42,445.20
2027: $300,000 wages + $50,000 gross SE; $192,300 cap $242,300 $50,000 × 92.35% = $46,175 $288,475 × 12.4% = $35,770.90
2036: $400,000 wages; cap pinned at $263,100 $263,100 + $136,900 = $400,000 $0 $400,000 × 12.4% = $49,600

YAML covers these calculations, combined-earnings boundaries, SE-only income, and the baseline clamp. It now also pins wages below the cap ($100,000), inside the donut ($192,300 taxable from $200,000 wages), and between the threshold and a higher cap ($255,000 wages and taxable earnings in 2036), plus the CT/DE paid-leave invariant. Python covers the default-start 2026 activation while 2025 stays current law, a finite 2036 interval, and an always-infinite threshold.

The Hypothesis property checks total Social Security tax never falls below baseline, SECA stays nonnegative, combined taxable earnings do not exceed wages plus net SE, the allocated bases equal the combined policy base, baseline SECA remains unchanged, and infinite thresholds are a no-op.

Validation and test cost

make format and Ruff lint passed. All final test processes exited successfully with four-thread caps, one file per foreground process.

Local macOS/Python 3.13 measurements include startup and cleanup. Peak RSS is recorded with Python getrusage (bytes on macOS). The wage file was rerun after the initial BSD timing tool could not read a sandboxed kernel-clock value; this table contains the final successful runs.

File group Passed Wall seconds Peak RSS, GiB
Reform wages 4 631.5 4.81
Reform SE 7 802.2 4.77
CT reform 1 931.4 3.10
DE reform 1 759.1 3.11
State baseline 12 595.2 2.07
Contrib Python 4 476.4 1.57

Total: 29 unique cases passed (25 YAML, 4 Python), including the 150-example Hypothesis property. In the Python run, activation took 83.33 seconds and the property call took 23.04 seconds.

The integration test reuses an isolated clone of the existing baseline and verifies that its threshold remains infinite. The property keeps 150 generated examples and shrinking, and now covers the state contribution invariant with actual formula methods and generated rates/shares. It suppresses the input-generation timing diagnostic because Hypothesis initially scans constants from thousands of loaded policy modules.

CI routing is unchanged: Contrib other-shard-1 discovers all four CBO YAML files as a 13-case batch and the existing Python file. The new CT/DE cases reuse the existing 2027 reform/parameter cache combination; the batch still has only two distinct cached model combinations. Expanded-batch Linux timing and memory have not been measured here.

Full CI completed at the previous head da469a15e6965e64f02e72be05b3445181516b97: all 27 checks passed. In Contrib other-shard-1, the Python file took about 14 seconds and did not enter the 25 slowest tests (cutoff 21.5 seconds); the two original contrib YAML files passed as a 10-case batch in 90.8 seconds with 4,920 MB peak RSS. These are Linux CI measurements before this follow-up; the local measurements in this section describe the updated files.

Past scores of this reform understated revenue because negative SECA canceled added wage tax, SE earnings were omitted, and future thresholds could fail to activate. Removing overlapping bases also corrects the wage overstatement once the cap exceeds the threshold.

Hub microsimulation impact (current head)

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

Output 2025 change 2026 change 2025 records changed (weighted) 2026 records changed (weighted)
ct_employee_paid_leave_contribution +0 +0 0 (0) 0 (0)
de_employee_paid_leave_contribution +0 +0 0 (0) 0 (0)
de_employer_paid_leave_contribution +0 +0 0 (0) 0 (0)
employee_social_security_tax +0 +0 0 (0) 0 (0)
employer_social_security_tax +0 +0 0 (0) 0 (0)
self_employment_social_security_tax +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)

No output changed in either year: the weighted sums at the merge base and at the head are identical, and no record changed. The by-state run shows no change in any state for any of these outputs. Under current law the PR changes nothing; it changes results only when the CBO secondary-earnings-threshold reform is applied.

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

impact: none under current law (2025 and 2026 microsimulation at this head: no output or record changed); the PR changes only the CBO secondary-earnings-threshold reform

axiom: n/a: CBO reform option, not enacted law; the baseline clamp changes no current-law result

@MaxGhenis MaxGhenis changed the title WIP: Fix Social Security secondary earnings threshold reform Fix Social Security secondary earnings threshold reform Oct 22, 2026
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 22, 2026 13:49
@DTrim99

DTrim99 commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Fix Social Security secondary earnings threshold reform

Summary

This PR fixes four defects in the CBO "donut" reform (gov.contrib.cbo.payroll.secondary_earnings_threshold). The reform taxes earnings below the current-law cap plus earnings above a secondary threshold such as $250,000.

  1. Negative SECA. Taxable wages above the cap gave negative self-employment room, so the extra wage tax was canceled. The PR fixes this with a zero floor in the baseline variable plus a reform override.
  2. Overlapping bases. Once the cap passed the threshold, the bases overlapped. The PR now uses max(threshold, cap) as the start of the second base.
  3. Missing SE earnings. Self-employment earnings above the threshold were never taxed. A new reform override of social_security_taxable_self_employment_income fills the combined base with wages first.
  4. Future thresholds. The factory read the threshold only at the simulation start instant, so a threshold starting later never activated the reform. It now scans every dated value.

The formulas match CBO's 2024 option and OACT provision E2.5. I hand-checked every new expectation, and the old-code reproductions in the PR body hold. The baseline clamp cannot change any current-law result. The tests activate the reform through the top-level reforms: key. CI is green at the head, and the branch merges cleanly with main.

One issue is open. The reform still raises taxable_earnings_for_social_security above the Social Security contribution and benefit base. Connecticut and Delaware paid-leave contributions read that variable, so they rise under the reform, although both state laws cap them at the federal base. The leak predates this PR, but it sits in the reform this PR is correcting and it changes household net income.

Critical (must fix)

None.

Should address

1. The reform's wage base leaks into Connecticut and Delaware paid-leave contributions

policyengine_us/reforms/cbo/payroll/increase_taxable_earnings_for_social_security.py:5-26 overrides taxable_earnings_for_social_security. Three baseline state variables read it directly and are not overridden:

  • policyengine_us/variables/gov/states/ct/tax/payroll/paid_leave/ct_employee_paid_leave_contribution.py:15: rate * person("taxable_earnings_for_social_security", period)
  • policyengine_us/variables/gov/states/de/tax/payroll/paid_leave/de_employee_paid_leave_contribution.py:18
  • policyengine_us/variables/gov/states/de/tax/payroll/paid_leave/de_employer_paid_leave_contribution.py:18

These flow through employee_state_payroll_tax and employee_payroll_tax into household_tax_before_refundable_credits and spm_unit_payroll_tax. Under the reform they therefore lower household net income and add state payroll revenue to the reform's score.

Example, traced statically: a Connecticut resident in 2027 with $400,000 wages and a $250,000 threshold.

  • Baseline contribution: 0.5% × $192,300 = $961.50. The existing baseline case "Connecticut Paid Leave contribution stops at the 2027 Social Security wage base" pins this value.
  • Reform contribution: 0.5% × $342,300 = $1,711.50, which is $750 more.
  • From 2036, with the cap above the threshold, the reform taxes all $400,000 of wages: $2,000.
  • Delaware's employee and employer contributions each rise by $600 on the same $150,000. That uses the 0.8% full rate and the default 50% employee share.

The law caps these contributions at the federal base, which the CBO option leaves unchanged:

  • C.G.S. 31-49g(b)(1): each employee "shall contribute a percentage of such employee's ... subject earnings that shall not exceed the Social Security contribution and benefit base, as determined pursuant to 42 USC 430" (cga.ct.gov, chapter 557).
  • Delaware: Paid Leave contributions are likewise charged on wages capped at the Social Security wage base (19 Del. C. ch. 37 and program guidance).
  • CBO's alternative leaves that base unchanged: "The current-law taxable maximum would still be used for calculating benefits, so scheduled benefits would not change under this alternative" (CBO budget option 60955).
  • OACT E2.5 likewise "does not provide benefit credit for additional earnings taxed" (OACT TR2025 run 418).

The 42 USC 430 base is still the current-law cap, so state contributions keyed to it should not change.

Fix. Have the three state variables read the federal base directly: min_(person("payroll_tax_gross_wages", period), parameters(period).gov.irs.payroll.social_security.cap).

  • This equals the baseline taxable_earnings_for_social_security exactly, so no current-law result changes.
  • It still tracks parametric reforms to gov.irs.payroll.social_security.cap, which is the 42 USC 430 base these laws reference.
  • The existing state baseline tests input employment_income, so they are unaffected.

Then add a contrib YAML case: CT, 2027, $400,000 wages, threshold $250,000, expecting ct_employee_paid_leave_contribution: 961.50.

The alternative is to leave taxable_earnings_for_social_security capped and move the secondary base into reform overrides of employee_social_security_tax and employer_social_security_tax.

If you prefer to keep this PR narrow, a linked follow-up issue noted in the PR body would also resolve this item.

Suggestions

1. Pin the reform's wage base for wages below or inside the donut in YAML.

All three wage-base outputs in tests/policy/contrib/cbo/payroll/taxable_earnings_for_social_security.yaml have wages above the threshold. Only the Hypothesis property covers wages below the cap, inside the donut, or between the threshold and a higher cap. These additions reuse the existing override combinations, so they add no memory cost:

  • Add taxable_earnings_for_social_security: 100_000 to the case at line 46 of the contrib SE file.
  • Add taxable_earnings_for_social_security: 192_300 to the line 13 case (wages inside the donut).
  • Add a 2036 case with $255,000 wages expecting 255,000. The old code gives 260,000.

2. Refresh the cost section of the PR body with the CI numbers.

The body says draft CI checks are skipped and that no Linux timing exists. Full CI has now run at this head:

  • In Contrib other-shard-1's contrib-Python step, the new Python file finished in about 14 seconds and is not among the 25 slowest tests (cutoff 21.5 s).
  • The two contrib YAML files ran as one 10-case batch: 90.8 s, peak RSS 4,920 MB.

The local 786-second / 1.56 GiB figure seems to predate the baseline-reuse commit. Label it that way or replace it.

3. Align the references.

  • The parameter cites CBO option 58630 (2022). The new reform variables cite 54806 (2018).
  • Option 60955 (2024) has the exact language the code follows: the $250,000 threshold, all earnings taxed once the cap passes it, and benefits unchanged.
  • The reform's SE override could also keep the 26 USC 1402(b) reference, since its wages-first ordering comes from that section.

4. Note when the donut closes in the model.

The model's cap parameter passes $250,000 in 2035 ($254,400, the CBO January 2025 forecast). With a fixed $250,000 threshold, the donut therefore closes in 2035, a year before the 2036 date in CBO's text. One sentence in the PR body or parameter description would help anyone comparing scores.

Verified correct

  • Reform specification.
    • CBO 60955: "all earnings below $176,100 ... would be taxed, as would earnings above $250,000 ... The Congressional Budget Office projects that the taxable maximum would exceed $250,000 in calendar year 2036; after that, all earnings ... would be subject to payroll taxes."
    • OACT E2.5 (TR2025 run 418): "tax all earnings once the current-law taxable maximum exceeds $250,000."
    • max_(threshold, cap) implements both statements exactly.
    • The threshold parameter has no uprating metadata, so it is a fixed nominal amount, which matches the non-indexed $250,000.
  • Employee/employer split. employee_social_security_tax and employer_social_security_tax each apply 6.2% to the overridden wage base. SECA applies 12.4% to the overridden SE base. CBO: "Self-employed individuals generally pay 12.4 percent of their net self-employment income."
  • SE allocation. I checked algebraically in all three regimes (wages ≥ U, cap ≤ wages < U, wages < cap) that wage base + SE base = min(W+S, C) + max(0, W+S−U), where U = max(threshold, cap). Filling the base with wages first matches the 26 USC 1402(b) treatment of the cap. Edge values behave: an infinite cap taxes all earnings, and a zero threshold taxes all earnings.
  • Benefit-formula effects. CBO and OACT give no benefit credit for the added earnings. PolicyEngine takes Social Security benefits as inputs, and no variable reads taxable_earnings_for_social_security for benefits, so there is nothing to model.
  • Baseline clamp is inert.
    • Baseline taxable_earnings_for_social_security = min_(cap, payroll_tax_gross_wages) ≤ cap in every year, whatever the cap's growth, and it never reads the secondary threshold. So max_(0, cap − taxable) always equals the unclamped term.
    • Only a direct input above the cap, or a reform, reaches the floor. No other reform overrides either variable (grep at head).
    • The Hypothesis property also checks that the baseline SE base equals the pre-clamp formula for caps from 0 to 1,000,000.
  • Factory.
    • Scanning values_list follows the CA Proposition 3 and Idaho CTC factories.
    • Activating for the whole run is safe because infinite-threshold years are no-ops: max_(0, W − inf) = 0 and max_(0, S − max_(0, inf − W)) = 0, and the capped SE term equals the baseline term.
    • test_factory_detects_distant_finite_threshold would fail under the old start-instant check.
  • Activation test isolation. When Simulation gets tax_benefit_system=system plus a reform, _prepare_spm_system (spm.py) clones the system before core applies the reform. The shared system is not mutated, as the PR claims.
  • Reform is exercised. All 10 contrib YAML cases carry the top-level reforms: key. The expected values differ from baseline (e.g. 342,300 against 192,300 for $400,000 wages in 2027), so the structural reform is active, not just a flag.
  • Old-code reproductions in the PR body.
    • $400,000 wages in 2027: SE base min(0, 192,300 − 342,300) = −150,000, so SECA = −$18,600.
    • $300,000 wages + $50,000 gross SE: SE base min(46,175, −50,000), so SECA = −$6,200.
    • 2036, $400,000 wages: 263,100 + 150,000 = $413,100 wage base.
  • Python tests justified. YAML reforms: bypasses the factory, so activation needs Python. The factory and Hypothesis tests use stubs and build no models. hypothesis is already a dev dependency.
  • Housekeeping.
    • Changelog changelog.d/fix-secondary-earnings-threshold-donut.fixed.md is top-level, with type fixed.
    • No partner contract files are touched.
    • Test periods are plain years.
    • The new baseline file is variable-named, in the existing gov/irs/self_employment test folder.
  • Source-conflict ratchet. Merge base 86d3ae6 (2026-10-22 05:39 UTC) is after Tie source-conflict markers to the register shared with Axiom #10287, so CI already ran the ratchet. No added or removed comment matches its patterns, neither file is in KNOWN_UNMARKED, and main's five newer commits do not touch these files.

Hand-checks (threshold $250,000; cap $192,300 in 2027, from the parameter and input; cap $263,100 in 2036, from input; $168,600 in 2024):

# Case Hand calculation Test
1 2027, $400,000 wages 192,300 + (400,000 − 250,000) = 342,300; × 6.2% = 21,222.60 each; SECA 0 342,300 / 21,222.60 / 21,222.60 / 0 ✓
2 2027, $300,000 wages + $50,000 gross SE wages 192,300 + 50,000 = 242,300 (× 6.2% = 15,022.60); net SE 50,000 × (1 − 0.5 × 0.153) = 46,175, all above U; SECA 46,175 × 12.4% = 5,725.70 242,300 / 46,175 / 46,175 / 15,022.60 / 5,725.70 ✓
3 2036, cap above threshold, $400,000 wages 263,100 + (400,000 − 263,100) = 400,000; × 6.2% = 24,800 each 400,000 / 24,800 / 24,800 / 0 ✓
4 2027, W 180,000, S 100,000 min(100,000, 12,300) + max(0, 100,000 − 70,000) = 12,300 + 30,000 42,300 ✓
5 2027, W 200,000, S 40,000 (inside donut) min(40,000, 0) + max(0, 40,000 − 50,000) 0 ✓
6 2027, W 200,000, S 50,000 / 50,001 50,000 − 50,000 = 0; 50,001 − 50,000 = 1 0 / 1 ✓
7 2027, W 100,000, S 100,000 (below wage base) min(100,000, 92,300) + max(0, 100,000 − 150,000) 92,300 ✓
8 2027, SE only, S 300,000 min(300,000, 192,300) + (300,000 − 250,000) 242,300 ✓
9 2036, threshold below cap, W 240,000, S 40,000 U = 263,100; min(40,000, 23,100) + max(0, 40,000 − 23,100) = 23,100 + 16,900 40,000 ✓
10 Baseline 2024: W 100,000, S 100,000 / W 168,600, S 50,000 / W 400,000, S 50,000 168,600 − 100,000 = 68,600; room 0; wage base 168,600, room 0 68,600 / 0 / 168,600 and 0 ✓
11 Baseline 2024, direct input of $400,000 taxable wages, S 50,000 (clamp) max(0, 168,600 − 400,000) = 0, so SE base 0 and SECA 0 (unclamped: −231,400) 0 / 0 ✓
12 Python activation, $400,000 wages + $50,000 net SE 2025 (threshold infinite): 176,100 / 0; 2026: 184,500 + 150,000 = 334,500 / 50,000 asserted as cap + extra wages ✓

The PR body table also checks out: $42,445.20 (342,300 × 12.4%), $35,770.90 (288,475 × 12.4%) and $49,600.

Axiom line assessment

axiom: n/a: CBO reform option, not enacted law; the baseline clamp changes no current-law result is valid:

  • The policy change is confined to a contributed CBO reform option.
  • The only baseline edit, the zero floor in social_security_taxable_self_employment_income, cannot change any baseline result computed from formulas, since baseline taxable wages never exceed the cap.
  • No enacted provision's output changes.

The Should address item does not affect this either: the recommended fix leaves every baseline state value identical.

CI status

  • All 27 checks pass at head da469a1 (full run started 2026-10-22 13:49 UTC).
  • The three "skipping" entries (Baseline, Contrib, Python tests) come from an earlier draft-gated run at the same head. They are not failures.
  • Contrib other-shard-1 ran both contrib YAML files (10 passed) and the new Python file (passed).
  • Baseline (irs) ran the new baseline YAML (6 passed).
  • Nothing is pending.

Branch status

  • The branch is 3 commits ahead of main and 5 behind.
  • git merge-tree --write-tree against current main is clean, with no conflicts.
  • Main's newer commits touch Minnesota's Social Security subtraction, Georgia LIHEAP, school meals and version files, none of which overlap this PR.

Verdict: REQUEST_CHANGES

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formulas match CBO option 60955 and OACT E2.5 (max(threshold, cap), a fixed nominal threshold, 6.2% + 6.2% and 12.4% SECA, wages-first SE, no benefit credit), the baseline clamp is inert, every contrib YAML uses reforms:, and 12 hand-checks pass. One should-address item: under the reform, taxable_earnings_for_social_security still rises above the 42 USC 430 base, and CT and DE paid-leave contributions read it, though C.G.S. 31-49g(b)(1) caps them at that base (CT 2027, $400k wages: $1,711.50 vs $961.50). It predates this PR, but since the PR fixes this reform, please cap those state variables at min(wages, cap) or file a linked follow-up. Four suggestions. Full report in the comment above.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

David, thanks. This answers your 10-22 review at a7b70c85.

Should address: the CT/DE paid-leave leak. Fixed in this PR, as you suggested.

  • ct_employee_paid_leave_contribution, de_employee_paid_leave_contribution and de_employer_paid_leave_contribution now read gross payroll wages capped at gov.irs.payroll.social_security.cap directly. They no longer read taxable_earnings_for_social_security.
  • They cite C.G.S. 31-49g(b)(1), 19 Del. C. §3701(24) and regulation 1401 §5.5.
  • Contrib cases with the reform on: CT 2027, $400,000 wages, threshold $250,000 → $961.50. The Delaware employee and employer contributions → $769.20 each.
  • The Hypothesis property (150 examples) now checks that these contributions are unchanged by the reform.
  • Those three were the only readers outside the federal payroll and SECA variables. The CO, MA, ME, OR and WA variables that read employer_total_taxable_earnings_for_social_security are untouched, and no baseline fixture needed changing.

Suggestions

  1. The three wage-base pins are added: 100,000, 192,300, and 255,000 in 2036.
  2. The cost section uses the CI figures you gave: about 14 s for the Python file, and 90.8 s / 4,920 MB for the YAML batch. The 786 s figure is gone.
  3. References are aligned on CBO option 60955, and the SE override keeps 26 USC 1402(b).
  4. The parameter description and the body note that the model's cap reaches $254,400 in 2035, so the donut closes a year before CBO's 2036.

29 cases pass. Fresh CI is running, and an independent delta review is queued.

@MaxGhenis
MaxGhenis requested a review from DTrim99 October 23, 2026 04:08
@DTrim99

DTrim99 commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 2): Fix Social Security secondary earnings threshold reform

PR #10380 (author: MaxGhenis), head a7b70c85f8. Round 1 reviewed da469a15e6 and requested changes (0 critical, 1 should, 4 suggestions). One commit has landed since: a7b70c85f8, "Preserve state paid-leave caps under the CBO donut reform", which changes 336 lines. There are no merge commits and no hand-resolved conflicts. Line numbers below refer to a7b70c85f8.

Summary

The open should-address item, the CBO donut reform's wage base leaking into Connecticut and Delaware paid-leave contributions, is fixed the way we recommended.

I found no new critical or should-address issues. Two small optional polish items are below.

Prior findings

Finding Status Evidence
S1: the reform's wage base leaked into CT and DE paid-leave contributions, which C.G.S. 31-49g(b)(1) and Delaware cap at the 42 USC 430 base Fixed (in this PR, using the recommended approach) Each of ct_employee_paid_leave_contribution.py:14-20, de_employee_paid_leave_contribution.py:20-30 and de_employer_paid_leave_contribution.py:20-30 now reads min_(person("payroll_tax_gross_wages"), gov.irs.payroll.social_security.cap) instead of taxable_earnings_for_social_security. New contrib cases with the reform active: contrib/cbo/payroll/ct_employee_paid_leave_contribution.yaml gives 961.50, and de_employee_paid_leave_contribution.yaml gives 769.20 for each side. Both also assert the enlarged federal wage base of 342,300, which shows the reform is active. Traces are below.
G1: pin the reform's wage base for wages below or inside the donut Fixed social_security_taxable_self_employment_income.yaml:22 adds 192,300 (wages inside the donut), and line 56 adds 100,000 (below the cap). taxable_earnings_for_social_security.yaml:57-65 adds a 2036 case with $255,000 of wages expecting 255,000.
G2: refresh the cost section with the CI numbers Fixed The body now cites the CI figures from the prior head (Python file about 14 s; YAML batch 90.8 s and 4,920 MB) and drops the 786 s figure. The local runs for the new files are labelled as macOS measurements. See N2 for the CI numbers at this head.
G3: align the references on CBO 60955 Fixed Both the parameter and the two reform variables now cite cbo.gov/budget-options/60955. The SE override also keeps 26 USC 1402(b).
G4: note when the donut closes in the model Fixed The parameter description and the body's Sources section both say that the model's $254,400 cap in 2035 closes the donut a year before CBO's 2036 date.

New findings

Critical: none.

Should address: none.

Suggestions (optional polish):

N1. Consider moving the dated sentence out of the parameter description.

  • Where: secondary_earnings_threshold.yaml:1. The description now adds: "With a fixed $250,000 threshold, the model's current-law cap of $254,400 closes the donut in 2035, one year before the 2036 date projected in CBO's 2024 option."
  • Why move it:
    • The parameter's own value is .inf, so the $250,000 is a hypothetical.
    • The $254,400 figure comes from the CBO January 2025 forecast in cap.yaml and will go stale when that forecast is refreshed.
    • The house template keeps descriptions to the single sentence that defines the parameter.
  • Suggestion: a YAML comment, or the PR body (which already says this), would keep the note without putting forecast figures in the description users see. Round 1 offered the description as one option, so this is optional.

N2. Optionally refresh two lines of the body.

  • Cost: the body says "Expanded-batch Linux timing and memory have not been measured here." CI at this head now has the numbers, from Contrib other-shard-1, run 39898145042:
    • YAML batch: the four CBO YAML files ran as one 13-case batch in 98.7 s with a 4,970 MB peak RSS. At the prior head the 10-case batch took 90.8 s and 4,920 MB, so the new cases add about 8 s and 50 MB.
    • Python file: it ran in about 13 s and is not among the 25 slowest tests (cutoff 23.82 s).
  • Axiom reason: the n/a reason names only the baseline clamp. It could add that the CT/DE paid-leave refactor gives the same baseline results.

Verified

Hand-checks of every new or changed expectation (threshold $250,000; cap $192,300 in 2027, supplied as an input and matching cap.yaml; cap $263,100 in 2036, supplied as an input):

# Case Hand calculation Test
1 CT, 2027, $400,000 wages, reform on Federal wage base = 192,300 + (400,000 − 250,000) = 342,300. CT = 0.5% × min(400,000, 192,300) = 961.50. Before the fix it was 0.5% × 342,300 = 1,711.50. 342,300 / 961.50 ✓
2 DE, 2027, $400,000 wages, headcount 25, reform on Headcount 25 is not below 10 or 25, so rate = 0.0008 + 0.004 + 0.0032 = 0.008. Employee = 0.008 × 0.5 × 192,300 = 769.20; employer = 0.008 × (1 − 0.5) × 192,300 = 769.20. Before the fix each was 0.004 × 342,300 = 1,369.20. 342,300 / 0.008 / 769.20 / 769.20 ✓
3 2027, W 200,000, S 40,000 (inside the donut) min(200,000, 192,300) + max(0, 200,000 − 250,000) = 192,300 192,300 ✓
4 2027, W 100,000, S 100,000 (below the cap) min(100,000, 192,300) + 0 = 100,000 100,000 ✓
5 2036, W 255,000, cap 263,100 U = max(250,000, 263,100) = 263,100; min(255,000, 263,100) + max(0, 255,000 − 263,100) = 255,000. The round-1 base code gives 255,000 + 5,000 = 260,000. 255,000 ✓
6 Hypothesis property The CT/DE formulas now read only wages, the cap, rates and shares, so the reform and baseline runs give identical contributions for any generated rates in [0, 1] and any wage/cap/threshold vector. The assertion guards against a future revert to the reform-overridden wage base. ✓ (structural)

Further reform trace (not tested): in 2036, with $400,000 of wages, Connecticut is now 0.5% × 263,100 = 1,315.50 rather than the $2,000 round 1 found.

Baseline results are unchanged:

  • Same expression. Baseline taxable_earnings_for_social_security is min_(p.cap, person("payroll_tax_gross_wages", period)). The new state expression is min_(payroll_tax_gross_wages, cap), which is identical for every input.
  • Baseline cases still give the same values:
    • state_paid_leave_premiums_2027.yaml: CT 50,000 → 250; CT 250,000 → 0.5% × 192,300 = 961.50; DE 50,000 → 200 and 200. All of these feed in employment_income.
    • tests/core/test_payroll_contributions.py: CT 5,000 gross wages after pre-tax deductions → 25, plus the CT/DE parametrized cases. These build wages from employment_income and read the wage base without inputting it.
  • No test in the repo feeds in taxable_earnings_for_social_security to drive CT or DE.
  • Remaining readers. After this change, taxable_earnings_for_social_security is read only by employee_social_security_tax, employer_social_security_tax and social_security_taxable_self_employment_income (grep at the head).
  • Other states are not affected. The CO, MA, ME, OR and WA wage bases already use their own min_(…, gov.irs.payroll.social_security.cap). Their employer-total variables read the employer_total_taxable_earnings_for_social_security input, which the reform doesn't override. The new CT/DE code follows the existing pattern.

Law and references:

  • CT. The reference cga.ct.gov chapter 557, 31-49g holds the round-1 quote: subject earnings "shall not exceed the Social Security contribution and benefit base, as determined pursuant to 42 USC 430".
  • DE statute. 19 Del. C. 3701(24) reads: "'Wages' means remuneration for employment as determined for purposes of old-age, survivors, and disability insurance ... under the Federal Insurance Contribution Act".
  • DE regulation. 19 DE Admin. Code 1401, PDF page 11 reads: "5.5 FICA Limits. ... Once the employee's PFML eligible wages reach the FICA limit in a calendar year, the employer and employee's required contributions will cease". The anchor is correct.
  • The cap the reform leaves alone. CBO 60955 keeps the current-law taxable maximum for benefits. The 42 USC 430 base, and so the "FICA limit" these programs reference, is the current-law cap that the reform leaves unchanged. Capping both states at gov.irs.payroll.social_security.cap also follows any parametric reform of that base.
  • Reference format. References are single, tuple-form and without titles. One reference entry per source.

Other checks:

Axiom

Line: axiom: n/a: CBO reform option, not enacted law; the baseline clamp changes no current-law result. It is valid.

  • Where the policy change sits: in the contributed CBO reform.
  • Baseline edits:
    • The SECA zero floor can't change a formula-computed baseline result.
    • The CT/DE paid-leave refactor gives the same baseline results (same expression as the baseline wage base).
  • No enacted provision's output changes. N2 suggests adding the second baseline edit to the reason.

CI

27 pass, 0 failed, 0 pending. I re-checked gh pr checks 10380 at the end of this review.

  • Run: run 39898145042 (pull_request, created 2026-10-23 04:02 UTC) tested head a7b70c85f8.
  • What passed:
    • Baseline: all four states shards, plus contrib-hhs, household, irs, reform, rest-a, rest-b and ssa-usda.
    • Contrib: all nine shards.
    • Python: Microsimulation and both Rest shards.
    • Other: Household API Partners, PR validation, Package and compatibility, and Quick Feedback.
  • Contrib other-shard-1:
    • YAML: the CBO batch passed 13 of 13: CT 1, DE 1, SE 7 and wage 4. It took 98.7 s with a 4,970 MB peak RSS.
    • Python: all 4 tests in test_increase_taxable_earnings_for_social_security.py passed, including the expanded Hypothesis property.
  • States shards: the baseline CT/DE state tests passed there.
  • Local runs: none. The traces above are static.

Branch

0 critical, 0 should and 2 optional suggestions are open at this head.

Verdict: APPROVE

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2: S1 is fixed. CT and DE paid-leave contributions now use min(payroll_tax_gross_wages, SS cap), so baseline is unchanged and under the reform CT 2027 at $400k is $961.50 and DE is $769.20 each, both pinned. All four suggestions are fixed. Two small suggestions. Branch note: #10285 conflicts in both DE files; keep the capped wages, add its deduction share, and extend this PR's test stub. Full report in the comment above.

@MaxGhenis
MaxGhenis merged commit b32fa44 into main Oct 24, 2026
27 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#10380 at a7b70c85f8a1af0d98f9916d94b383c50e611ed3 (squash): Fix Social Security secondary earnings threshold reform.

  • Review: GPT-6.1 Sol shell-backed delta review APPROVE at a7b70c8 (reviews-gen/us-10380/review-dtrim1.md; no findings; CT/DE sources and hand figures checked; its methodology flag is the reform's wages-first allocation in the upper band, which follows 26 USC 1402(b)(1) and is the hub's call for a non-enacted CBO option), after the r1 Sol APPROVE at da469a1; DTrim99 APPROVED at a7b70c8 (2026-10-23).
  • 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 under current law: 2025 and 2026 microsimulation at merge base 86d3ae6 vs head a7b70c8, all ten outputs (CT/DE paid-leave contributions, Social Security taxes, state and federal income tax, household net income and benefits) identical in weighted sums, zero records changed, zero change in every state; base sums real (CT leave $643.5M, employee Social Security tax $589.5B in 2025).
  • Axiom: n/a: CBO reform option, not enacted law; the baseline clamp changes no current-law result

@MaxGhenis
MaxGhenis deleted the fix-secondary-earnings-threshold-donut branch October 24, 2026 15:19
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.

Secondary-earnings-threshold (donut) reform: negative SECA tax cancels its revenue; double counting above the cap; SE income untaxed

2 participants