Repository navigation
Apply dated reforms on the statutory timeline on every route - #2305
Conversation
Reform dicts and Reform.from_dict classes now write their changes onto the parameters as loaded from YAML, which are then processed as current law is (uprating, threshold indexation, fiscal-year annualisation). Before, they edited the already-annualised tree, so a change from 6 April missed that fiscal year and derived parameters stayed on the baseline. A bounded change with no stated value after it resumes prior law's projected path rather than restoring the last stated value, which froze the next year of an uprated parameter. Scenario parameter changes now come right after the data load, before the UC rebalancing inputs and parameter-gated structural reforms read the parameters, and before the scenario's modifier. The reprocessed tree keeps current law's baseline child (#2188). Fixes #2304, #2188, #2252, #2167. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the route contract A change starting after a parameter's last stated value lost the uprating or indexation between that value and the change, because the generators extend a timeline only from its last stated value. Prior law's projected path is now written in first wherever a change reaches the projected part of the timeline. Single-day reform keys are logged as well as warned, since some variable modules switch warnings off when they load. Adds test_dated_reform_routes.py: the Budget cases on four routes, documented readings, an independent annualisation oracle checked by Hypothesis sampling, route differentials, prior law under the reform's other changes, and baseline isolation (sim.baseline, parameters.baseline, query order, module caches, a fresh process). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a Dated changes section to the scenarios guide covering the statutory-timeline processing, each route's period-key reading, prior law around a change, and when changes are applied relative to data load. Fixes the UC rebalancing example whose single-date key changed nothing (#2252), and a stale personal allowance path in the phasing example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rameter Review fixes (GPT-6.1 Sol round 1), each reproduced before fixing: - A modifier that reloads and reprocesses the parameters (no_economic_assumptions, reindex_benefit_cap) discarded the reform, and Scenario.apply dropped changes a simulation already had. The system now keeps its dated changes and legacy overrides, and every rebuild writes them again. process_parameters builds the baseline copy from the parameters as they were before the changes. - Two changes that both reach the projected part of their timelines did not see each other: a one-year primary threshold change inside an allowance freeze resumed the unfrozen path. Prior law is now projected per parameter, with the others' changes, to a fixed point. - A change starting before a parameter's first stated value lost that value's backdating (found by a new boolean property). The backdated value is written as prior law first. - The projection cache is a copy, no longer sharing lists with the module-level system's tree. - Rebuilt parameters are memoised by their changes, so repeated simulations of one reform skip the rebuild. - Mixed single-day and dated keys on one parameter warn that single-day keys apply last. Tests: each of the above, a differential against editing the annualised parameters for calendar-year changes, and oracle properties for a boolean and a null-valued preserve_calendar_dates parameter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o, and roll back failed changes Review round 2 (GPT-6.1 Sol, review-uk-r3-out.md): - Scenario.apply keeps main's semantics: it edits the annualised parameters and never rebuilds, so a structural reform's parameters and a modifier's edits survive it. Later rebuilds start from the raw tree as the simulation left it. - The rebuild memo keys only plain scalar values, and returns copies. - Construction and Scenario.apply give parameter_changes and dated reforms the same precedence. - A change that fails leaves the system as it was. - Prior law that does not settle within the pass limit is an error, not a cached partial projection. Also tests W5's shape: one scenario with a modifier that sets a parameter and parameter changes, in either order and either timing, matches the reform dict with both (on 2.125.1 the modifier's 8.75% dividend rate reverted to 10.75%). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hand-off to the UK hub (W1, session local_37646626-ec63-43ad-a47b-7752ce411a5c)
|
|
UK hub landing audit for #2305 (W1, Budget critical path).
|
Fixes #2304
Fixes #2188
Fixes #2252
Fixes #2167
Review: GPT-6.1 Sol rounds 1 and 2 requested changes. Every finding is fixed (db9dcb0, 603fecb), and round 3 approved 603fecb. Its four low-severity notes are going into a follow-up PR.
What this changes
Every reform route now writes its dated changes onto the statutory parameter timeline (the parameters as loaded from YAML), then processes them as current law is processed: uprating, threshold indexation, then fiscal-year annualisation by each parameter's rule.
Simulation(reform=dict),Scenario.from_reform(dict)) andReform.from_dictclasses used to edit the already-annualised tree. So a change from 6 April 2027 first showed in fiscal year 2028, and derived parameters stayed on the baseline: aligned NICs thresholds, and anything uprated from a changed CPI forecast.SINGLE_OLD= £500 for 2027 gave £424.90 in 2028).Scenario.applyon a built simulation keeps main's semantics: it edits the annualised parameters and never rebuilds them, so a dated key there does not set its fiscal year, and it logs a warning saying so. Constructing the simulation with the scenario (orreform=) is the statutory route.parameters.baselinechild.Each route keeps its documented reading of period keys. Single-day reform-dict keys keep their legacy one-day meaning on the annualised tree, with a
ReformPeriodWarningwhen the key can change nothing. The table is inpolicyengine_uk/utils/dated_reforms.py.Verified so far (by running the code)
SINGLE_OLD£500 for2027-04-06.2028-04-05, dict: FY27 / FY28parameter_changesbare year 2027: FY28 / FY29The 20 existing reform, fiscal-year, threshold-indexation, UC rebalancing, behavioural and code-health test files pass locally (2,452 passed).
Prior law around a change
Outside
[start, stop]a change leaves prior law in force:stopresumes the next day.Whether indexation should instead resume from a changed statutory amount (ITA 2007 s57 indexes "the previous tax year's" allowance) is a methodology call under cos d1344. With the default, an extended PA freeze returns to the baseline path in 2034. The contract runner records the statutory expectation as pending.
Review
GPT-6.1 Sol round 1 (on 48c1417) requested changes. I ran each predicted reproduction, confirmed it, then fixed it.
no_economic_assumptions,reindex_benefit_cap), combined with a dict reformScenario.applyon a simulation that already has a reform_projected_timelines_cacheshared lists with the module-level system's treeReformPeriodWarning, also loggedHow the fixes work:
process_parameters()buildsparameters.baselinefrom the tree as it was before the changes.GPT-6.1 Sol round 2 (on db9dcb0) requested changes. Each reproduction was run before and after the fix:
reform=(add_bonus, {"test_budget.bonus": 7}), thenScenario.apply)ValueError: Could not find the parameter test_budget.bonusno_economic_assumptions, then an unrelated change: PA 2035Scenario(parameter_changes=…).apply, then a dated changeparameter_changesplus a dated reform for the same path: construction vsScenario.applyValueErrorrepras the memo keyint/float/bool/strvalues are memoisedBuild time for a reformed household simulation (host load average over 300, so absolute times are inflated):
Invariants (tested in
test_dated_reform_routes.py)fiscal_year_blendparameters. Checked by Hypothesis sampling over rate, uprated, indexed and blended parameters, including two-change schedules in either order.start.stopkeys give identical parameters and income tax through the reform dict,parameter_changes,Reform.from_dictand both Scenario timings. Sampled.Reform.from_dict: onwards), single-day dict keys, scalars, unknown paths, and parameters added by a structural reform in the same tuple.sim.baseline,parameters.baseline(Scenario parameter changes leak into the baseline parameter tree, so baseline_vat shows no change #2188,vat − baseline_vaton five routes), later simulations, query order, the processed and statutory caches, and a fresh process.Tests
policyengine_uk/tests/test_dated_reform_routes.py, 68 tests, passing locally. Since the reviews they include a differential against editing the annualised tree for calendar-year changes, boolean and null-valuedpreserve_calendar_datesproperties, a test for each round-1 and round-2 finding, and the modifier-plus-parameter-changes scenario.Merge order with #2242
#2242 adds
add_uc_standard_allowance_upratingtoprocess_parameters. Either PR can land first; the second keeps that step afteradd_threshold_indexationinprocess_parameter_tree, as agreed with its session.axiom: n/a: reform-route handling and parameter processing; no policy provision changes.
🤖 Generated with Claude Code