Repository navigation
Conversation
Chain the 2026 Trustees intermediate AWI and adjusted CPI growth onto the published NAWI(2025) and the 2027 COLA index, then re-derive the SOI income totals, the taxable maximum from 2028, the wage-indexed amounts and the parameters whose uprating names either path. Off by default. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pins from separate arithmetic on the Trustees' workbook, the statutory re-derivation of the wage-indexed amounts, the readers core uprated at build, published years, neutrality of the default tree, composition with the core-thresholds reform in either order, household checks on the cap, PIA, veterans' benefits and the COLA lever, and Hypothesis properties. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each order now builds one system and checks every IRS parameter the core-thresholds reform wage-indexes against the formula on the Trustees' NAWI, so each fits in its own process; the COLA lever case reads through 2040 to keep the at-instant trees it builds few. Co-Authored-By: Claude Opus 5.5 <[email protected]>
MaxGhenis
marked this pull request as ready for review
November 1, 2026 02:50
A built system applies its reform set twice. On the second pass the payroll cap extension read a policy's 2027 cap as its no-decrease floor, and the assumption copied that path into the owners' draw copy: removing the cap from 2027 left the copy at infinity in 2060. The cap extension now takes a keyword-only cap= (default: the live cap, so its other callers keep their behaviour), and the assumption runs it on the copy itself, from the copy's own values through 2027. The reused SOI helper holds each total at its end_year value from end_year on, so an end_year of 2036 or earlier overwrote CBO's later values. The assumption now calls it only when end_year exceeds 2036. The module docstring now says why a Microsimulation ages inputs on the default paths (system.py omits system=), through which year (2036), what a later year carries, and what the order rule does and doesn't re-derive. Tests: both passes replayed with a cap removal and a $10 million cap from 2027 in both tuple orders, and end_year 2028, 2030 and 2036, on the file's cached default-tree copy; the one-pass cap-policy test is renamed to what it covers. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
trustees_2026_economic_assumptions_reform, an opt-in reform that puts Social Security's wage and price paths on the intermediate assumptions of the 2026 OASDI Trustees Report, for long-range reform scoring. Long-range reform scoring has to match the Trustees' taxable payroll, and the taxable maximum and bend points run ongov.ssa.nawi. Main loads CBO's February 2026 NAWI through 2034 and then extends it at 3.2335% a year; the Trustees' AWI ends 2100 32% above that path, and chained onto the published NAWI(2025) it ends 34% above (1,076,751.82 against 804,773.88). Moving wages alone would leave real wage growth near 1.3% a year against the Trustees' 1.13%, so the reform moves prices too.The reform stays off by default, and nothing registers it in a default list, so current-law output doesn't change. The 2025 Trustees functions keep their behaviour, and their tests pass unedited.
extend_social_security_payroll_capgains a keyword-onlycap=None, which defaults togov.irs.payroll.social_security.cap, so its other callers keep their behaviour.Source: 2026 OASDI Trustees Report, single-year tables, Table VI.G1, intermediate assumptions, columns "Adjusted CPI" (2026 = 100) and "AWI", 2025 to 2100 (https://www.ssa.gov/oact/TR/2026/SingleYearTRTables_TR2026.xlsx, workbook sha256
38d8c5fe…ba361). The module carries both series as Python dicts, astrustees_2025.pydoes, and a test pins their sha256.Chain rule
The module chains the Trustees' growth onto published values. It never writes the Trustees' levels.
gov.ssa.upratingat Y-01-01 holds the third-quarter CPI-W of Y − 1, and 2027-01-01 (328.746, the 2026 third quarter) is the last actual. For each Y from 2028, U(Y) = U(Y − 1) × ACPI(Y − 1) / ACPI(Y − 2).end_yearvalue (default 2100) afterend_year.Then, in order: the SOI income projection (
apply_trustees_2025_soi_income_projection, reused; it reads the active NAWI), only whenend_yearexceeds 2036;extend_social_security_payroll_cap(last_projected_year=2027), on the live cap and then on its owners' draw copy;extend_social_security_wage_indexed_parameters; and the readers below.Values read from a built reformed system and from main:
gov.ssa.nawigov.ssa.nawigov.ssa.nawigov.ssa.upratinggov.ssa.upratinggov.ssa.upratingReal wage growth from 2050 to 2100 comes to 1.127% a year under the reform; the Trustees' wages over main's prices would give 1.27%.
Reach
Core's
uprate_parameterswrites each reader's values into the parameter when the system builds, so a later change to a path doesn't reach them. The module finds every parameter whoseupratingmetadata namesgov.ssa.nawiorgov.ssa.upratingby walking the tree (with_iter_updatable_parameters, as the core-thresholds reform does) and derives each again as core would from the new path: its last value stated in YAML, times path(Y) / path(that date), with its own rounding, for every year after the path's last unchanged year (2025 for NAWI, 2027 for the COLA index). Values the YAML states stay as stated. It raises for a reader that states a value after that year or uprates on a cadence, so a new reader that doesn't fit stops the build instead of staying on CBO's path. A test pins the list below.The assumption also extends
gov.contrib.ssa.owners_draw.current_law_capfrom 2028 throughend_year. It runsextend_social_security_payroll_cap(..., cap=<the copy>)on the copy itself, so the rule reads the copy's own values through 2027 as its anchor and no-decrease floor. A policy reform that sets the payroll cap changes only the live cap, so reform scoring keeps the assumption's cap path in the copy when a policy raises or removes the payroll cap, from 2027 or later, in either tuple order. Before this fix the assumption copied the live cap's path into the copy. On a built system's second pass, the live cap's extension read a policy's 2027 cap as its floor: removing the cap from 2027 left the copy at infinity in 2060, and a $10 million cap from 2027 left it at $10 million, in either order.gov.ssa.nawigov.ssa.upratingcalibration.gov.irs.soi.*(17 income totals)end_yearexceeds 2036gov.irs.payroll.social_security.capgov.contrib.ssa.owners_draw.current_law_capgov.ssa.social_security.wage_basegov.ssa.ssi.amount.individual,.couplegov.ssa.ssi.income.exclusions.blind_or_disabled_working_student.amount,.capgov.states.ia.hhs.ssa.rcf_personal_needsgov.states.md.tax.income.agi.subtractions.pension.max_amountgov.states.nc.ncdhhs.sa.rate.basic,.enhancedgov.states.ok.dhs.ssp.child_allocation,.ineligible_spouse_standardgov.states.sd.dss.ssp.assisted_living_daily_rategov.irs.income.exemption.overtime.hce_salary_threshold,.salary_basis_thresholdReaders that read a path when they run need nothing:
ss_aimeindexes earnings bygov.ssa.nawi, the COLA levers intools/social_security_benefit_levers.pyreadgov.ssa.uprating, and the per-capita SOI series rebuild after every reform.Anchor. For the four rounded readers whose YAML stops at 2026 (Iowa, both Oklahoma amounts, South Dakota), the module anchors at the 2026 value, as core does, rather than at the rounded 2027 value. That keeps core's arithmetic: where the new path equals the old one, the reader keeps its default value. Anchoring at 2027 would move these by one rounding step ($1, or $0.01 for South Dakota) in 5 to 59 of the years from 2028 to 2100. For the other readers the two anchors agree.
veterans_benefitsveterans_benefitsnamesgov.ssa.upratingas its variableuprating. Core reads that index from the simulation's own system when a later year asks for an input given for an earlier one, so in a householdSimulationunder the reform the benefit follows the Trustees' path: $30,000 entered for 2026 reads $68,034 in 2060 under the reform and $65,158 without it (a test checks both against the two paths). Nothing needed changing. AMicrosimulationbuilt from a single-year dataset is different.Microsimulation.__init__builds the reformed system first (_prepare_spm_system,system.pyline 478), but then callsextend_single_year_datasetwithoutsystem=(lines 515 and 527), so that function falls back to the module-level default system. With nodataset_end_year, the extension stops at the last year in the CPI-U YAML:_get_default_end_yearreadsgov.bls.cpi.cpi_u, whose last entry falls on 2036-02-01. So inputs age on the default paths through 2036. For a later year, Core uprates each input whose variable names anupratingparameter from its latest stored year, 2036, on the simulation's own system, the reformed one (Simulation._calculatein policyengine-core 3.33.2). So a later year carries only the gap accumulated by 2036. Under this reform the SOI totals keep their CBO values through 2036, so that gap falls on the one input variable that readsgov.ssa.upratingdirectly,veterans_benefits: the index reads 403.287 on the default path against 407.218 on the reform's at 2036, a ratio of 0.990347. For a dataset year of 2027 or earlier, every later year ofveterans_benefitsin such aMicrosimulationreads 0.990347 times its value on a dataset thatextend_single_year_datasetextended with the reformed system. No other input variable names a path the assumption changes before 2037. To age microdata on these paths, extend the dataset with the reformed system (extend_single_year_dataset(dataset, system=CountryTaxBenefitSystem(reform=...))) and pass the result. This applies to the 2025 Trustees scenario too.Composition with
trustees_core_thresholds_reformIn a built system either order gives the same tree: the IRS thresholds after 2034 follow the 2026 Trustees NAWI under both
(trustees_2026, trustees_core_thresholds)and(trustees_core_thresholds, trustees_2026).CountryTaxBenefitSystem.__init__applies the reform set twice, so on the second pass the core-thresholds reform reads the Trustees' NAWI whichever comes first. One test per order builds that system and checks every IRS parameter the core-thresholds reform wage-indexes, for 2027 to 2100, against the wage-indexing formula on the built tree's NAWI, which in turn matches the Trustees' path; two of them also differ from the formula on main's NAWI.Policy reforms that set a parameter this assumption derives should come after it in the reform tuple; on the second pass the later reform wins. That order keeps the values a later reform sets directly, but nothing re-derives their downstream readers after it: an override of
gov.ssa.upratingplaced after the assumption movesveterans_benefits, which reads the index when it runs, and leaves the SSI amounts the assumption materialized from the index alone. The assumption extends the owners' draw copy from the copy's own values, so the copy holds the assumption's path in either order.Invariants
Each holds for every input the tests draw; the Hypothesis tests use at most 50 examples, with years drawn over one built tree.
Chain identities. NAWI(Y) / NAWI(Y − 1) = AWI(Y) / AWI(Y − 1) for 2026–2100, and U(Y) / U(Y − 1) = ACPI(Y − 1) / ACPI(Y − 2) for 2028–2100; over any window, the ratios telescope the same way. Both paths hold their 2100 value after 2100.
Published values hold. NAWI through 2025, the cap, its owners' draw copy and the COLA index through 2027, the wage-indexed amounts for 2027, every reader's YAML values and the SOI totals through 2036 match main entry for entry.
Statute. The cap, wage base, both bend points, the QC threshold, both earnings-test amounts and both SGA amounts equal a formula written in the test on the Trustees' NAWI for every year 2028–2100, and differ from main at 2028, 2036, 2037, 2060 and 2100.
Monotone. The cap, QC threshold and bend points never fall.
Readers. Each reader equals core's formula on the new path; the same formula on the default path reproduces main (a differential check on the test itself). Exact rounding ties, which the two-decimal CPI makes possible, accept either neighbor.
Idempotent. The function applied once to a copy of the default tree equals the built system, which applies the reform twice, for every touched parameter, entry for entry.
Neutral. Building the reform or applying the function leaves
system.parametersunchanged, and main still reads NAWI 2036 104,988.75, the cap 2100 1,995,300, its owners' draw copy 2060 558,600 andgov.ssa.uprating2036 403.287.Real wages. Real wage growth from 2050 to 2100 is 1.127% a year, within 0.02 points of the Trustees' 1.13%.
Owners' draw cap. The current-law cap copy equals the assumption's payroll cap for every year from 2028 to 2100 (654,900 in 2060 against main's 558,600), and keeps that path after one pass of a policy that raises the payroll cap from 2028.
Owners' draw cap across both passes. With a policy that removes the payroll cap or sets it to $10 million from 2027, replaying both passes of the reform set on the default-tree copy (assumption, policy, assumption, policy, and the reverse tuple order) leaves the copy on the statutory path for every year 2028–2100, 654,900 in 2060, with its entries through 2027 equal to main's cap; the live cap reads the policy's value for every year 2027–2100.
Short horizons keep the CBO income window. With
end_year2028, 2030 or 2036, all 17 SOI totals match main entry for entry through 2036, and at 2040.Limits
gov.irs.uprating, SNAP and HHS uprating. Inputs uprated by CPI-U keep the default path too.calibration.gov.cbo.income_by_source.adjusted_gross_incomeor its per-capita series keep the default path: 348 variables, including 16 stored in the default dataset. From 2036 to 2100, that path grows 10.393 times per capita, versus 9.418 for wages.Microsimulationbuilt from a single-year dataset ages its inputs on the default paths through 2036, where the default extension ends, becausesystem.pycallsextend_single_year_datasetwithoutsystem=. Core uprates later years on the reform's paths from 2036, so they carry the gap accumulated by then: forveterans_benefits, a ratio of 0.990347 (see above).end_year=2060, reading COLA events through 2061 raisesRequested COLA year 2061 exceeds the frozen Q3 horizon 2060(read_parameter_eventsintools/social_security/cola.py), because the annual calculator (tools/social_security/engine.py) asks for the following January to price December entitlement. The scalar thatgov.ssa.upratingcarries afterend_yeardoesn't extend that horizon.Mutation checks
At 60b99ee, the reviewer ran each of the six mutants in its own process under the 2 GB and 900 s cap, against a small stub tree with Core's parameter operations and the real extension functions. Assertion equivalents of the listed tests killed all six; the mutants didn't run the named tests on the full model. Each row names the assertion that killed its mutant.
test_pins(NAWI 2026)last_projected_year=2036for the captest_pins(cap 2028)test_wage_base_equals_the_captest_readers_follow_the_new_pathtest_pins(gov.ssa.uprating2028)test_reform_leaves_the_default_tree_aloneThe two fixes' new tests haven't run against 60b99ee as mutants. The local run included an in-process replay of two of them on the reviewed head's module, after the new tests, and its time guard skipped the replay because startup had pushed the run to 1,381 s. On 60b99ee the reviewer read the values these tests reject: the owners' draw copy at infinity and at $10 million in 2060, and the 2036 employment-income total at $12.862 trillion with
end_year=2028, against main's $17.326 trillion.Tests
policyengine_us/tests/core/test_trustees_2026_economic_assumptions.py, 106 tests, in the Rest Pythoncoregroup. It builds one reformed system and copies the default tree once (both cached, freed at module teardown); the two composition tests run last, free those, and build one system each.This update, after the first review, adds seven parameter-tree cases and renames one test.
test_owners_draw_current_law_cap_survives_both_passes_of_a_2027_cap_policyreplays both passes of the reform set with a policy that removes the payroll cap or sets it to $10 million from 2027, each in both tuple orders (four cases).test_a_short_end_year_keeps_the_cbo_income_totalsapplies the assumption withend_year2028, 2030 and 2036 (three cases).test_owners_draw_current_law_cap_survives_later_cap_policybecomestest_owners_draw_current_law_cap_holds_after_one_cap_policy_from_2028, since it covers one pass from 2028.The new cases reuse the file's cached default-tree copy. A context manager puts the copy's 42 touched parameters on the default tree's values, the case applies its steps, and the copy then gets its own values back; the context manager checks the copy against a snapshot and the default tree against its import-time snapshot. They add no model build or tree copy; they apply the assumption to the copy eleven times.
The local run selected those eight with
-kin one process, on a host at load 80 to 125 on 18 CPUs: all passed (98 deselected). Pytest reported 455.92 s after collection; the whole process took 1,491.6 s wall and 48.8 s user CPU, with a peak RSS of 1.63 GB, or 1.52 GiB (/usr/bin/time -l). In CI run 41299903587 at 608c43b, Rest Python shard 1 ran all 106 tests in the file and they passed; the seven new cases took 3.79 s of call time together, against 1.74 s for the renamed one-pass test.The owners' draw update (60b99ee) added three parameter-tree regression checks for the owners' draw cap copy. They reuse the file's cached reformed system and default-tree copy, adding no model builds or tree copies to the full-file run. The local filtered run selected only those three checks: all passed (96 deselected), with 750.42 s reported by pytest and 1,349.32 s total process wall time including startup. Peak RSS was unavailable because the sandbox denied the resource reporter's sysctl call after the tests passed; the process remained under the 4 GB monitor. Nobody has measured before/after runtime or Linux CI memory for those three.
Local runs, on a host at load 50 to 160 on 18 CPUs, so wall times are inflated several-fold (CPU time is the steadier figure):
The first row's peak predates trimming the COLA-lever case from 2060 to 2040 on the reformed tree alone, which cut the full-tree at-instant builds it triggers from 70 to 15. CI's Rest Python shards and Quick Feedback passed at 85f851c (run 41266782916), and the Pull request workflow passed at 60b99ee (run 41276700022); per-file timings come with each run's reports.
impact: none under current law (opt-in, off by default); the hub will measure
axiom: n/a: economic assumption paths, not enacted law
🤖 Generated with Claude Code