Skip to content

Backdate parameters before applying reforms - #9628

Merged
MaxGhenis merged 7 commits into
mainfrom
fix-backdate-before-reforms
Oct 6, 2026
Merged

MaxGhenis merged 7 commits into
mainfrom
fix-backdate-before-reforms

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

CountryTaxBenefitSystem.__init__ backdated every parameter to 2015 after applying the reform. backdate_parameters (policyengine_us/tools/parameters.py) copies each parameter's earliest value back to 2015-01-01, so for a parameter first dated after 2015 it copied whatever the reform had just made earliest. 44,424 parameters are first dated after 2015-01-01 (715 of them later in 2015), counted at a50e1e3a7c. They include most parameters OBBBA added, such as the tip deduction cap. OBBBA switches with a 0000-01-01 sentinel or an earlier value, such as gov.irs.deductions.itemized.limitation.obbb.applies (first dated 2009), are never backdated. Two things went wrong:

  1. A reform starting on or before a parameter's first dated value was copied back to 2015. The web app's default payload (2026-01-01.2100-12-31) does this to every one of the 1,863 gov parameters first dated on or after 2026-01-01.
  2. A reform ending before the first dated value left the years in between undefined. Parameter.update writes None after the reform's stop date when no earlier value exists. Scales lost that bracket, and formulas reading the parameter raised ParameterNotFoundError. When the reform started after 2015-01-01, its value was also copied back to 2015.

This PR backdates the baseline tree immediately after the parameter pipeline, before the reform is applied. Parameters a reform adds or replaces are still backdated after it is applied, as before. Reforms are still applied after uprating and interpolation, so #9075 stays fixed, and structural-reform detection still reads reformed values.

Side effect: CountryTaxBenefitSystem(start_instant=...) and Simulation(reform=..., start_instant=...) now build with any start from 2015-01-01 through 2023-12-31. On main they raise ParameterNotFoundError for every such start, even with no reform. The cause is that detection read contrib parameters (a switch, a threshold and a rate) first dated after the start before they were backdated. Starts before 2015 still raise under both orderings.

Found while reviewing #9604, which documents the ordering as pre-existing and out of scope.

Repro

from policyengine_us.system import CountryTaxBenefitSystem

prefix = "calibration.gov.irs.soi.agi.number_of_returns"  # amounts first dated 2021-01-01
s = CountryTaxBenefitSystem(
    reform={
        f"{prefix}[0].amount": {"year:2021:1": 123},
        f"{prefix}[2].amount": {"year:2019:1": 789},
    }
)
sc = s.parameters.get_child(prefix)
Year Bracket 0 (main) Bracket 0 (this PR) Bracket 2 (main) Bracket 2 (this PR) Brackets (main) Brackets (this PR)
2015–2018 123 8,487,025 789 10,056,377 18 18
2019 123 8,487,025 789 789 18 18
2020 123 8,487,025 None 10,056,377 17 18
2021 123 123 10,056,377 10,056,377 18 18

The change

The order in __init__ was: pipeline (load, homogenize, interpolate, uprate, default uprating) → apply reform → detect structural reforms → backdate → reset modified flags → apply reform and structural reforms again.

It is now: pipeline → backdate → apply reform → backdate → detect structural reforms → reset modified flags → apply reform and structural reforms again.

Both passes backdate to FIRST_MODELED_YEAR (2015, from #9626).

  • The first pass fills [2015-01-01, first dated value) with each baseline parameter's first dated value. Uprating has already run, so a backdated value cannot act as a "next defined value" and freeze uprating the way Reforms starting at a future date freeze uprating of the reformed parameter for intervening years #9075 did. The existing Reforms starting at a future date freeze uprating of the reformed parameter for intervening years #9075 tests pass.
  • The second pass reaches only parameters the reform added or replaced, counting a reassigned values_list as a replacement. A parameter the reform only updated already has a value dated by 2015, and Parameter.update never removes values dated before the period it sets, so the second pass leaves it alone.
    • At a50e1e3a7c, I wrapped backdate_parameters to record what each pass reaches:
      • Under the exhaustive tests' reform of 42,489 parameters, the first pass reached 44,424 parameters and the second reached 0.
      • Under a Reform class that adds one parameter, the second pass reached exactly that parameter.
    • Round 2 of the review found two other cases the second pass reaches, both with results identical to main:
      • an existing parameter whose object a Reform class replaces (gov.irs.deductions.tip_income.phase_out.rate);
      • an existing parameter whose values_list a Reform class reassigns (gov.irs.deductions.auto_loan_interest.phase_out.increment).
    • Without the second pass, a reform that adds a parameter (guarded, because __init__ applies a reform twice) left that parameter undefined before its first date. On main it was backdated, so a 2024 formula reading it worked; without the second pass it raised ParameterNotFoundError.

Invariants

These hold for every reform that sets values of existing parameters, other than the 19 derived <national total>_per_capita series, and switches on no gov.contrib structural reform. That covers dict, app and API reforms that leave the contrib switches off, which Reform.from_dict applies through Parameter.update.

  • Construction rebuilds the derived series from their totals as its last step, replacing any value a reform sets on one directly.
  • A contrib reform that is switched on is installed only where structural-reform detection runs: in CountryTaxBenefitSystem.__init__, and again in policyengine_us.Simulation.__init__ and Microsimulation.__init__. It is not installed when Reform.from_dict or the YAML runner applies a reform to a finished system. So building with the reform and applying it afterwards can differ in what that contrib reform installs; the tests leave gov.contrib out for this reason.
  • A Reform class that adds a parameter or bracket, or replaces a parameter object or its values_list, has its first dated value copied back to 2015 by the second pass, exactly as on main, so 1 and 3 don't hold for what it adds or replaces.
  1. Locality. From 2015 on, a reformed parameter equals the reform value inside the reform's period and its baseline value everywhere else.
  2. No gaps. From 2015 on, wherever a parameter's baseline value is defined, so is its reformed value, unless the reform sets None. This follows from 1.
  3. Construction matches post-construction. CountryTaxBenefitSystem(reform=r) gives the same values as Reform.from_dict(r) applied to the finished system. The YAML runner (through set_parameter) and household-api likewise apply reforms to a built system through Parameter.update. The exception is the derived <national total>_per_capita series: construction rebuilds them from the reformed totals (add_per_capita_uprating), while Reform.from_dict applied afterwards sets values through Parameter.update alone. Under the exhaustive tests' reform, 1 of the 19 derived series differs on Jan 1 of 2015–2035: calibration.gov.cbo.income_by_source.employment_income_per_capita in 2018 is 37,207.11 at construction and 24,804.74 afterwards. That gap predates this PR and has nothing to do with backdating; see "Related" below.
  4. Baseline unchanged. With no reform, the built system is identical to main's.

Three tests check 1 and 3. They share one reform covering the 42,489 numeric and boolean parameters outside gov.contrib that look backdated, meaning their 2015-01-01 value repeats their next value.

  • 42,313 of these are backdated. The other 176 are dated 2015-01-01 in their YAML with the same value as their next date, so they look the same: 139 state sales tax table cells, 36 Arkansas low-income tax table entries and the SSA PIA threshold. 1 and 3 hold for them too.
  • Each parameter gets one of three windows: the year from the first dated value; the year before it (a bounded reform ending before the first value); or from a year before it to 2100 (the app's format).
  • The first two tests check each reformed parameter on Jan 1 and Jul 1 of 2015–2035 and at both window edges. The whole-tree test compares each parameter's full value list and, where those differ, its values on Jan 1 and Jul 1 of 2015–2035.

The three tests:

  • test_every_backdated_parameter_changes_only_in_its_reform_period checks 1.
  • test_reform_at_construction_matches_reform_applied_afterwards checks 3 for the reformed parameters.
  • test_reform_at_construction_matches_afterwards_for_every_parameter (new in this round) checks 3 for every parameter in the tree, apart from the 19 derived series: 104,727 of the 104,746 leaves.

For dict reforms, 1 also follows from Parameter.update's code once every parameter has a value from 2015.

For (4), the first round replayed main's __init__ verbatim in a subclass at e8bf23a958. Across all 102,627 parameter leaves at Jan 1 and Jul 1 of 2010–2035 there were 0 differences. There were also 0 differences in any variable's formulas, defined_for, adds or subtracts. This round compared every parameter's full value list between main and this branch on the default-dataset runs: see "Impact" below.

Impact

Four microsimulations on the default dataset (populace_us_2024.h5@populace-us-2024-spm-20260909), run one at a time: main (ea79c39fb7, this branch's merge base) and this branch, each with no reform and with the Georgia reform from the first row of "Results that change" below, for 2025 and 2026. Each run saved every record's value of eight variables (household_net_income, household_tax, household_benefits, income_tax, state_income_tax, spm_unit_net_income, child_care_subsidies, ga_child_care_subsidies) and every parameter's full value list, and the runs were compared record by record and leaf by leaf. The branch's baseline run was at 743b6dfd3d and its reform run at 81e427cf85; the only change between those commits is the changelog fragment's wording.

Baseline: no change.

  • All eight variables are identical for every record in both years: all 57,240 households, 59,900 SPM units and 79,729 tax units.
  • All 104,746 parameters have identical value lists.

Reform: the Georgia CAPS family fee rate for income above 100% FPL, 7% → 3% as 2026-01-01.2100-12-31. The parameter is first dated 2026-03-02.

  • Only that parameter's value list differs between main and this branch. Its 2015-01-01 entry is 0.03 on main, the reform value copied back, and 0.07 here, the baseline value.
  • In 2025, before the reform starts, the reform changes one household, one SPM unit and one tax unit on main, and nothing on this branch:
2025, reform minus baseline main This PR
ga_child_care_subsidies and child_care_subsidies (SPM unit) +$1,508.00 $0
household_benefits +$1,508.00 $0
income_tax (tax unit) +$221.26 $0
state_income_tax (tax unit) +$173.42 $0
household_tax +$394.68 $0
household_net_income +$1,113.32 $0
spm_unit_net_income −$394.68 $0
  • In 2026, the reform's first year, main and this branch give the same value for every record of every variable. The same SPM unit gets +$1,612.00 of ga_child_care_subsidies, and its household +$1,443.98 of net income, under both.
  • Weighted, the 2025 leak is small on this dataset. 11 of its 59,900 SPM units have Georgia child care subsidies, and the one the reform reaches has a weight of 0.0026. So main added $3.99 to 2025's weighted Georgia child care subsidies of $72.1 million. The household example under "Results that change" shows the size for one family.

Other things that could change, and what I checked

These checks ran in round 1, on e8bf23a958 (then main) plus the first version of this change, and were not re-run on later heads. The exception is the "Related" reproductions at 743b6dfd3d, which re-confirm the five EITC crashes and ut_hb210_s2 on this branch. This round re-ran the tests and the counts at a50e1e3a7c, and the impact runs at 743b6dfd3d and 81e427cf85. The two later commits change only comments and docstrings in the test file and the changelog fragment's wording.

  • Structural-reform detection:
    • create_structural_reforms_from_parameters calls 140 creators, which read 143 parameters, starting at start_instant (some also read the next four years). 121 have a 0000-01-01 sentinel, which backdating never touches.
    • 16 are first dated after 2015 without a sentinel. 15 start false or 0, and cbo.payroll.secondary_earnings_threshold starts at infinity, which its creator treats as off. So backdating them cannot switch a reform on.
    • With no reform, the only reform triggered at every start from 2015 through 2035 (plus 2025-07-01 and 2026-10-01) is the Michigan surtax. Its creator installs it unconditionally and checks in_effect inside the formula.
    • I switched on all 163 gov.contrib in_effect flags that build under both orderings, as 2026-01-01.2100-12-31 and as year:2026:1. Triggered reforms, variable formulas and parameter values were identical.
    • As 2019-01-01.2100-12-31, main raised on dc_kccatc.active. So I also pinned five detection parameters (dc_kccatc.active, cbo.payroll.secondary_earnings_threshold, the two biden.budget_2025 rates, wyden_smith.actc_lookback) to their baseline values from 2019. Then the triggered reforms were identical. The only parameter differences were the 24 in_effect flags first dated 2019–2025, which main set to true for 2015–2018 by copying the reform's value back.
    • Five more flags crash under both orderings; see "Related" below.
  • Reform classes: there are 23 Reform objects under policyengine_us/reforms/ that call modify_parameters. Passed as reform=, 22 give identical parameter trees and variables under both orderings; ut_hb210_s2 crashes under both (see "Related" below).
  • modified flags: still reset after backdating and the first reform application, and set again by the second. A test checks that reformed parameters carry the flag and untouched backdated ones do not.
  • Mass sweep (at e8bf23a958): I perturbed all 48,127 non-list late-dated parameters in five reform shapes, one build per shape, and read each at Jan 1 and Jul 1 of 2015–2035:
Reform shape Parameters main This PR
The year starting at the first dated value 48,127 48,127 leak back to 2015 all correct
The year before the first dated value 47,419 46,874 leak; 19,834 have a None gap all correct
Two years before the first dated value 46,874 46,874 have a None gap all correct
From the first dated value onward (bare date) 48,127 48,127 leak all correct
<year before>-01-01.2100-12-31 (app format) 47,419 46,874 leak all correct

Who is affected

Every path that builds the system with the reform in it. Simulation(reform=...) and Microsimulation(reform=...) go through spm.py to CountryTaxBenefitSystem(reform=...). That covers:

  • policyengine.py household and population calculations;
  • policyengine-api's household calculations, whose Simulation signature check falls back to reform= for the US;
  • the api-v2 simulation executor, which runs society-wide impacts for the app, including each year of a budget window;
  • microcosm's reform validation.

These are not affected, because they apply the reform to an already backdated system:

  • policyengine-household-api;
  • the YAML test runner, which is why no YAML test could catch this;
  • any caller that passes tax_benefit_system=.

policyengine-api and the api-v2 executor both pin policyengine[models]==6.2.1, which pins policyengine-us==2.2.1 (checked 2026-10-06). That release has the same ordering. policyengine.org gets this fix once they move to a release that contains it.

Results that change (household calculations, main vs this PR)

Measured in round 1 (2026-09-25), on e8bf23a958 (then main) and that plus the first version of this change; not re-run on ea79c39fb7.

  • This round re-ran the Georgia reform as a microsimulation (see "Impact").
  • It also re-ran the Utah 2024–2025 figure with only in_effect switched on: +$800 at 743b6dfd3d.
  • The Georgia +$1,196 also follows from the current fee formula, whose weekly fee is floor(annual income × rate / 52): floor($30,000 × 7% / 52) × 52 − floor($30,000 × 3% / 52) × 52 = $2,080 − $884.
Reform (app-style payload) Parameter first dated Effect on main This PR
Georgia CAPS family fee rate for income above 100% FPL: 7% → 3% from 2026-01-01 2026-03-02 Single parent, $30k, child aged 3, $12k childcare: household net income +$1,196 in 2024 and 2025, before the reform starts $0 before 2026; +$1,196 from 2026
Utah CTC contrib reform (in_effect, amount and refundable amount from 2026-01-01) 2026-01-01 (amounts; in_effect has a 0000-01-01 sentinel) Married couple, $110k, children aged 2 and 3: +$2,800 in 2024 and 2025 +$800 in 2024 and 2025 (a separate problem, see below); +$2,800 in 2026
Washington millionaires tax switched on from 2026 with a 5% rate for 2026-01-01.2026-12-31 (app yearly mode) 2028-01-01 (rate; in_effect is dated from 2000) Single, $3M wages: wa_income_tax and household_net_income raise ParameterNotFoundError in 2027 2027 tax $198,000 at the baseline 9.9% rate
Federal tip deduction cap $25k → $50k from 2025-01-01 2025-01-01 $45k of tips: tip_income_deduction is $45,000 in 2015–2024, but no tax effect, because the deduction only enters the deduction lists from 2025 $25,000 before 2025

In all four cases, the years each reform value covers match under both orderings. In the Washington case, 2027 is inside the in_effect period but after the one-year rate, and main raises there.

Formulas that read the poverty guidelines at the prior October 1 (SNAP and others) are the one path I found where the leak also changes the reform's own years: the Guam, Puerto Rico and US Virgin Islands amounts are first dated 2024-01-01. As a contrived demonstration, a Guam guideline reform from 2024-01-01 pays $667 a month of SNAP in January–September 2024 on main, against $0 on this branch (round 1; SNAP has changed on main since, and this was not re-run).

Tests

policyengine_us/tests/policy/baseline/parameters/test_reform_backdating.py has 20 tests and does 4 system builds during them:

  • the three exhaustive checks above, for invariants 1 and 3;
  • targeted cases, each comparing whole 2015–2030 series:
    • a reform on the first dated value is not copied back;
    • a bounded reform before the first dated value leaves no gap, for a scale bracket and a scalar;
    • every scale bracket survives;
    • app-style range reforms starting on and before the first dated value, including a filing-status breakdown;
  • a parameter a reform adds is backdated;
  • only reformed parameters carry modified;
  • Simulation(reform=...) keeps the pre-start year at baseline, and a formula reading the year after a bounded reform computes instead of raising;
  • a structural reform switched on through Simulation(..., start_instant="2019-01-01") is detected;
  • six parametrized guards that check each targeted parameter's YAML is still first dated where the tests assume, with a message saying what to change if not;
  • the window helper shifted moves a Feb 29 date to Feb 28 in a non-leap year (new in this round).

Before and after, at a50e1e3a7c. These are local runs, one pytest process at a time, on Python 3.13.9 with policyengine-core 3.32.15, on a shared machine running other sessions' work.

Tree Result pytest time
This branch (a50e1e3a7c) 20 passed: 17 in one run, the 3 exhaustive tests in a second 693 s and 203 s
main (ea79c39fb7) with this test file 11 failed, 9 passed 660 s
  • The two later commits change only comments and docstrings in the test file (743b6dfd3d) and the changelog fragment's wording (81e427cf85).
  • The 9 that pass on main are:
    • the 6 guards;
    • the modified test, which guards the new order and passes on the old too;
    • the added-parameter test, which checks parity with main;
    • the shifted test, which builds no system.
  • The shifted test, before and after: the helper at a7fd4ad171 raises ValueError: day 29 must be in range 1..28 for month 2 in year 2025 for shifted("2024-02-29", years=1); now it returns 2025-02-28.

Cost. This round adds no system build:

  • the new whole-tree test reuses the module fixture and took 0.40 s locally;
  • the shifted test builds nothing.

The file runs in the policy group of Full Suite - Rest (Python + variables).

CI at 81e427cf85 (run 37434830792, which tests the merge of 81e427cf85 into main at b812bd46a0): all 35 checks pass. In Full Suite - Rest (Python + variables), all 20 tests in this file passed in about 69 s. The slowest steps were the start-instant structural-reform test (29.0 s), the exhaustive tests' fixture (14.6 s), the Simulation test (10.5 s) and the first fixture build (9.9 s).

Earlier rounds, before the rebase and the second pass (e8bf23a958 plus the first version of this change):

  • pytest outside microsimulation: 890 passed, 1 skipped;
  • YAML: 28,817 baseline, 1,360 contrib, 60 reform and 69 variables tests, all passing;
  • microsimulation pytest: 83 passed and 4 heavy tests skipped. 5 failed identically on main, because they download microdata and the run was offline.

The YAML runner applies reforms to the already built baseline system, never through CountryTaxBenefitSystem(reform=...), which is the only place the second pass runs. CI at 81e427cf85 also re-ran every Baseline and Contrib YAML shard and the variables YAML tests, all passing.

ruff format --check . and ruff check . (ruff 0.15.5, the uv.lock pin) are clean at 743b6dfd3d.

Review round 2

Round 2 reviewed a7fd4ad171. It found the code and tests ready and asked for corrections to this description only. This round:

  1. The main failure count (finding 1). It said 45,937; round 2 measured 45,941 on the older base 9e5852a337. Re-measured on today's main, it is 41,635 (see "Invariants"). The message of merge commit a40fa9aa7a carries the correction for a7fd4ad171's commit message.

  2. The invariants claimed too much (finding 2). "Invariants" now:

    • limits the scope to reforms that set existing parameters (other than the derived per-capita series) and switch on no gov.contrib structural reform, and says 1 and 3 don't hold for what a Reform class adds or replaces;
    • excludes the derived per-capita series from invariant 3;
    • says how the check samples dates.

    test_reform_at_construction_matches_afterwards_for_every_parameter (bb010100ff) checks invariant 3 over the whole tree. It fails on main for 41,635 parameters and passes here.

  3. "Added or replaced" (finding 3). The second-pass comment in system.py, the test module docstring, the changelog fragment (81e427cf85) and this description now say the second pass reaches parameters a reform added or replaced.

  4. Test robustness (finding 4):

    • the selection-size guard is now in the shared fixture, so none of the three exhaustive tests can pass on an empty selection;
    • shifted handles Feb 29, with a test;
    • the comments say which parameters the selection takes. That is 176 that only look backdated, up from 1 when round 2 reviewed it, because main has since added 2015 values for them.
  5. Shadowed fixture (finding 5). Locals renamed so they no longer shadow the reformed fixture.

  6. Tracking issues (finding 6). The "Related" items now have issues: Utah CTC contrib reform changes years in which its in_effect switch is off #9842, AL, KY, MS, ND and OH dashboard EITC contrib reforms raise 'mixes computation modes' when switched on #9843, ut_hb210_s2 cannot be passed as reform= (VariableNameConflictError) #9844, Derived _per_capita series are not rebuilt by reforms applied to a built system, and hold their value through a None total #9845 and _normalise_period_key drops the stop date of start.stop reform keys policyengine-sim-api#724. I re-ran the first four on this branch before filing; the fifth is from reading the code. The stale scratchpad/pr_body.md draft was in the temporary scratchpad of the session that worked in the old quizzical-bouman-016455 worktree. Neither exists any more, and this description was rebuilt from the live one.

  7. Merged main twice:

Related, not fixed here

I re-ran the first four on this branch (743b6dfd3d) on 2026-10-06, except the YAML-runner case under the per-capita item, which comes from reading policyengine-core's tools/test_runner.py. The fifth is from reading the code. Each has an issue.

axiom: n/a: infrastructure (order of backdating and reform application in CountryTaxBenefitSystem), not a policy rule.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits September 25, 2026 04:00
CountryTaxBenefitSystem.__init__ backdated every parameter to 2015 (copying
its earliest value back) after applying the reform set. For a parameter
first dated after 2015, a reform starting on or before that first date was
copied back to 2015, and a reform ending before it left the years in
between undefined, which dropped brackets from scales and could raise
ParameterNotFoundError. Backdate the baseline tree right after the
parameter pipeline instead, before the reform is applied. Reforms still
apply after uprating and interpolation (#9075), and structural-reform
detection still reads reformed values, now on a backdated tree, so a
start_instant before 2024 no longer raises ParameterNotFoundError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Independent review found that a reform adding a parameter (guarded,
because __init__ applies a reform twice) was backdated on main but not
after moving backdating ahead of the reform, so a formula reading it
before its first date raised ParameterNotFoundError. Backdate again after
the first reform application: every baseline parameter already has a
value dated by 2015 and Parameter.update never removes earlier values,
so this pass reaches only parameters the reform added.

Tests now check two invariants over every backdated numeric and boolean
parameter outside gov.contrib (46,615): a reform changes a parameter only
inside its own period, and building the system with a reform matches
applying the same reform to the finished system. Both fail on main for
45,937 parameters. The targeted tests compare whole series, cover an
app-style reform starting before a breakdown parameter's first date, a
formula reading the year after a bounded reform, the Simulation path
with start_instant, and parametrized guards on each test parameter's
first dated value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 4 commits October 5, 2026 23:18
Resolves the conflict in policyengine_us/system.py with main's
FIRST_MODELED_YEAR (#9626): the PR's two backdating passes now use
f"{FIRST_MODELED_YEAR}-01-01" instead of "2015-01-01", and main's call after
structural-reform detection is the one this PR moves before the reform. The
second-pass comment now says it reaches parameters the reform added or
replaced (review round 2, finding 3).

Correction to a7fd4ad's message: on the pre-merge base 9e5852a, both
exhaustive tests fail on main's ordering for 45,941 parameters, not 45,937.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2 (findings 2, 4 and 5):
- test_reform_at_construction_matches_afterwards_for_every_parameter
  compares every parameter, not only the ones the reform sets, between
  building with the reform and applying it afterwards. The derived
  <national total>_per_capita series are skipped: construction rebuilds them
  from the reformed totals and Reform.from_dict does not.
- The selection-size guard moves into the shared fixture, so neither
  exhaustive test can pass on an empty selection.
- shifted() moves Feb 29 to Feb 28 in a non-leap year instead of raising;
  test_shifted_moves_february_29_to_february_28 covers it.
- Locals no longer shadow the module's reformed fixture.
- The first backdated year comes from FIRST_MODELED_YEAR (#9626).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
backdated_first_date picks a parameter whose 2015-01-01 value repeats its next
value. That is every backdated parameter, and also any parameter whose YAML
dates 2015-01-01 itself with the same value as its next date: 178 of the
42,489 selected at a50e1e3, mostly state sales tax table cells. The
invariants hold for those too, so the selection stays; the comments now say
so. The module docstring says the second pass reaches parameters a reform
added or replaced (review round 2, finding 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The changelog fragment now says parameters a reform adds or replaces are
backdated afterwards, as the second-pass comment in system.py does (review
round 2, finding 3).

Correction to 743b6df's message: 176 of the 42,489 selected parameters
only look backdated (139 state sales tax table cells, 36 Arkansas low-income
tax table entries and the SSA PIA threshold), not 178.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 6, 2026 13:05
@MaxGhenis
MaxGhenis merged commit 6440b3e into main Oct 6, 2026
35 checks passed
@MaxGhenis
MaxGhenis deleted the fix-backdate-before-reforms branch October 6, 2026 14:24
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#9628 at 81e427cf852fc417ece06e30e37a84529b330f1c (squash)

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