Skip to content

A Trustees economic assumption listed after a policy reform can overwrite the policy's payroll cap, wage-indexed amounts and SSI amounts with no error #10787

Open

Description

@MaxGhenis

CountryTaxBenefitSystem(reform=(policy, assumption)) can drop the policy's values for the parameters a Trustees economic assumption rewrites, and it raises no error or warning. Both assumptions do this: trustees_2026_economic_assumptions_reform, and any reform that calls apply_trustees_2025_economic_assumptions. With the assumption first, the same pair keeps the policy's values.

We tell callers in the 2026 module's docstring to list the assumption first, and no code enforces that order. apply_trustees_2025_economic_assumptions has no docstring. Neither assumption runs unless a caller applies it, so default results don't change.

For example, take the 2026 assumption and a policy that sets the payroll cap, gov.irs.payroll.social_security.cap, to $10 million from 2030. A worker earns $500,000 of wages in 2030.

Reform tuple Cap, 2029 Cap, 2030 Cap, 2050 employee_social_security_tax, 2030
(assumption, policy) $209,100 $10,000,000 $10,000,000 $31,000.00
(policy, assumption) $209,100 $217,800 $462,900 $13,503.60

In policy-first order, the system caps the worker's taxable wages at $217,800, the assumption's own cap for 2030. The worker pays 6.2% of $217,800, and under the policy they would pay 6.2% of $500,000.

What the policy-first order loses

  • Both assumptions recompute three parameters for every year after a seed year: the payroll cap, the benefit wage base (gov.ssa.social_security.wage_base) and the owners' draw copy of the cap (gov.contrib.ssa.owners_draw.current_law_cap). The 2026 assumption seeds from 2027 and the 2025 one from 2035. They set each later year to the larger of the previous year's value and the wage-indexed formula, so they erase a policy value that starts after the seed year. Under the 2026 assumption, a $10 million cap from 2028, 2030, 2036 or 2037 reads the assumption's path in every year. Under the 2025 assumption, a $10 million cap from 2030 stands, and one from 2036 reads $261,900 in 2036. A policy value that covers the seed year stands until the formula exceeds it. For example, an owners' draw copy of $200,000 from 2027 reads $201,000 in 2028 under the 2026 assumption and $261,900 in 2036 under the 2025 one.
  • Both assumptions rewrite seven more wage-indexed amounts for every year from 2027: the quarter-of-coverage threshold, both earnings-test exempt amounts, both PIA bend points and both substantial gainful activity amounts. A policy that triples any of the seven from 2027 or from 2030 keeps none of its values. For example, a first bend point of $4,551 from 2030 reads $1,517 in 2030 under the 2026 assumption.
  • The 2026 assumption also re-derives 15 parameters whose uprating metadata names one of its two paths. Thirteen name gov.ssa.uprating, including both SSI federal benefit amounts, and two thresholds under gov.irs.income.exemption.overtime name gov.ssa.nawi. For example, an SSI individual amount of $2,000 a month from 2030 reads $1,105.56 in 2030 and $1,776.57 in 2050. The 2025 assumption writes none of the 15.

With the assumption first, each of these policies keeps its value in every year it sets.

A one-year policy carries forward only from the seed year

A built system applies its reform set twice, so in either order the assumption runs at least once after the policy and reads the policy's value for the seed year. Under the 2026 assumption, a cap for 2027 alone becomes the floor for every later year:

Cap policy 2027 2028 2029 2030 2033 2034
None $192,300 $201,000 $209,100 $217,800 $248,100 $258,900
$250,000 for 2027 only $250,000 $250,000 $250,000 $250,000 $250,000 $258,900
$250,000 for 2028 only, after the assumption $192,300 $250,000 $209,100 $217,800 $248,100 $258,900

A cap of inf for 2027 only removes the cap in every year through 2100. The wage base and the owners' draw copy carry a 2027 value the same way. Under the 2025 assumption, a $350,000 cap for 2035 only holds through 2044, while one for 2034 only or 2036 only moves no other year. With no assumption, a $250,000 cap for 2027 only leaves 2028 at $199,200.

What the code should do

Each policy's values should stand in every year the policy sets, in either order, or the constructor should refuse an order it can't honor.

The carry should follow one rule in every year. Under 42 U.S.C. 430, the Commissioner of Social Security determines each new base as the base "in effect in the year in which the determination is made or, if larger," the wage-indexed product. The assumptions apply that floor to a policy's value only when the value covers the seed year, and a system with no assumption doesn't apply it.

Where the code does this

We listed the carry and the owners' draw copy case among the known limits in #10650's description.

Reproduction

Run this at commit 3f6e604:

from policyengine_core.reforms import Reform

from policyengine_us import CountryTaxBenefitSystem, Simulation
from policyengine_us.reforms.ssa.trustees_2026 import (
    trustees_2026_economic_assumptions_reform as assumption,
)

CAP = "gov.irs.payroll.social_security.cap"
SSI = "gov.ssa.ssi.amount.individual"


def policy(path, period, value):
    return Reform.from_dict({path: {period: value}}, country_id="us")


def show(label, reform, path, years, wages_year=None):
    system = CountryTaxBenefitSystem(reform=reform)
    parameter = system.parameters.get_child(path)
    print(label, [round(float(parameter(f"{year}-01-01")), 2) for year in years])
    if wages_year:
        situation = {
            "people": {
                "worker": {
                    "age": {wages_year: 40},
                    "employment_income": {wages_year: 500_000},
                }
            }
        }
        simulation = Simulation(situation=situation, tax_benefit_system=system)
        tax = simulation.calculate("employee_social_security_tax", wages_year)[0]
        print("  employee Social Security tax on $500,000 of wages:", round(float(tax), 2))


cap_from_2030 = policy(CAP, "2030-01-01", 10_000_000)
show("(assumption, cap):", (assumption, cap_from_2030), CAP, (2029, 2030, 2050), 2030)
show("(cap, assumption):", (cap_from_2030, assumption), CAP, (2029, 2030, 2050), 2030)

ssi_from_2030 = policy(SSI, "2030-01-01", 2_000)
show("(SSI, assumption):", (ssi_from_2030, assumption), SSI, (2029, 2030, 2050))

cap_2027_only = policy(CAP, "2027-01-01.2027-12-31", 250_000)
show("(assumption, cap for 2027 only):", (assumption, cap_2027_only), CAP, range(2027, 2035))

It prints:

(assumption, cap): [209100.0, 10000000.0, 10000000.0]
  employee Social Security tax on $500,000 of wages: 31000.0
(cap, assumption): [209100.0, 217800.0, 462900.0]
  employee Social Security tax on $500,000 of wages: 13503.6
(SSI, assumption): [1079.63, 1105.56, 1776.57]
(assumption, cap for 2027 only): [250000.0, 250000.0, 250000.0, 250000.0, 250000.0, 250000.0, 250000.0, 258900.0]

A possible fix (not run)

We could keep a copy of each parameter the assumptions rewrite from before any reform runs, as the constructor already keeps one of the cap. The extensions would then take their 1994 base and seed-year value from that copy and leave alone any year a policy changed. As a smaller step, the code could raise an error when a policy that sets one of these parameters precedes an assumption, and the carry would stay.

In the same change, we could update the cap docstring of extend_social_security_wage_indexed_parameters, where we suggest passing a copy of the cap. Both assumptions pass the wage base itself, so the call rewrites the wage base with its own values.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions