Skip to content

Roll back failed override batches and keep structural-reform nodes across rebuilds - #2375

Merged
MaxGhenis merged 6 commits into
mainfrom
w1-dated-reform-hardening
Oct 12, 2026
Merged

MaxGhenis merged 6 commits into
mainfrom
w1-dated-reform-hardening

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2374

Review: GPT-6.1 Sol requested changes on 175ac4b, then approved 7819755 with two notes. Both notes are fixed in 63d2e7e: a failed batch now also restores modified flags, and the module docstring is updated. An independent Opus 5.5 review of the final delta found the rollback test never reached the rollback. fb5ee57 adds a test that fails after a real update, and d594634 adds a spy so that test can't go vacuous. The Opus review approves d594634.

Reproducer: policyengine_uk/tests/test_dated_reform_routes.py::test_a_batch_failing_after_an_update_restores_values_and_flags
Reproducer: policyengine_uk/tests/test_dated_reform_routes.py::test_a_rebuild_keeps_a_structural_reforms_parameter
Reproducer: policyengine_uk/tests/test_dated_reform_routes.py::test_reprocessing_keeps_a_structural_reforms_parameter
Reproducer: policyengine_uk/tests/test_dated_reform_routes.py::test_failed_reprocessing_leaves_the_system_as_it_was
Reproducer: policyengine_uk/tests/test_dated_reform_routes.py::test_a_rebuild_keeps_a_band_a_structural_reform_appended

These are GPT-6.1 Sol's four low-severity notes from its round-3 approval of #2305 (~/reviews/w1-budget-reform-contract-2026-10-11/review-uk-r4-out.md). Each was reproduced on #2305's head before it was fixed.

What this changes

  • Override batches are atomic. CountryTaxBenefitSystem.add_legacy_overrides finds every target first, then keeps each target's values_list and the modified flags on its path. If any override fails, it restores them. That's O(batch), and the parameters tree stays the same object. Before, Scenario.from_reform({rate: {"2027": 0.3}, "gov.not_a_real_parameter": {"2027": 1}}).apply(sim) raised but left the rate at 0.3, unrecorded, and the next rebuild reverted it.
  • Structural-reform nodes survive rebuilds and reprocessing. graft_missing_nodes (utils/dated_reforms.py) adds back to a rebuilt tree any node the previous processed tree had and the rebuild lacks. Existing parameters keep their rebuilt values, and the root's baseline child is skipped. It runs after apply_parameter_assignments rebuilds, and after process_parameters reprocesses a tree reset_parameters reloaded (reset_parameters keeps the tree it replaced until then). Before, after Simulation(reform=(add_bonus, {"test_budget.bonus": 7})), both sim.apply_parameter_changes(...) and no_economic_assumptions.apply(sim) raised Could not find the parameter test_budget.bonus.
  • process_parameters processes a copy of the reloaded tree. If processing or the override replay fails, it restores the reloaded tree unprocessed, along with the projected timelines and rebuild base. reset_parameters keeps the processed tree it replaces only once, and only if that tree is processed.
  • Scale bands. graft_missing_nodes also appends bands a structural reform added to a scale, and recurses into the bands both trees have.
  • Shared caches are documented as shared: a memo entry's projected timelines, and the module function projected_timelines(). The system property still returns a copy.
  • Docs. docs/book/usage/scenarios.md now says:
    • Scenario.apply on a built simulation edits the annualised values, so a dated key there does not set its fiscal year;
    • single-day keys and a scenario's own modifier run after the UC rebalancing and the parameter-gated structural reforms.

Invariants

  • Atomicity. A failed add_legacy_overrides, apply_parameter_assignments or process_parameters leaves the system's parameters, timelines, rebuild base and recorded changes as they were.
  • Monotone structure. A rebuild never loses a node of the tree it replaces, and never changes an existing parameter's rebuilt value.

Tests

New tests in policyengine_uk/tests/test_dated_reform_routes.py. The first five fail on #2305's head (603fecb) and pass here:

  • a failed override batch applies none of its overrides;
  • a rebuild keeps a structural reform's parameter;
  • reprocessing keeps it;
  • failed reprocessing leaves the system as it was;
  • grafting adds only missing nodes.

Added after the review:

  • a rebuild keeps a band a structural reform appended. Run with the previous graft, the rebuild left four bands;
  • a second reset keeps the processed tree for grafting;
  • grafting appends missing scale bands.

The reprocessing test now also checks that the restored tree is unprocessed and that a retry works. test_a_batch_failing_after_an_update_restores_values_and_flags fails a batch after its first override has updated the rate, using a Reform.from_dict key that is parsed only when applied. It checks the rate's list, value, recorded overrides and flags, and a spy confirms the rollback ran. It fails at 7819755 and passes here.

The 14 reform and parameter-processing test files pass: 441 passed on fb5ee57. The two rollback tests pass on d594634, which changes only the test file.

axiom: n/a: reform-route and parameter-processing infrastructure; no policy provision changes.

🤖 Generated with Claude Code

…ross rebuilds

Fixes #2374, the low-severity notes from GPT-6.1 Sol's round-3 review of
#2305:
- add_legacy_overrides applies a batch to a copy and swaps it in, so a
  batch that fails applies none of its overrides.
- Rebuilds (apply_parameter_assignments) and reprocessing
  (reset_parameters then process_parameters) add back the nodes a
  structural reform added to the processed parameters
  (graft_missing_nodes), so their overrides replay; process_parameters
  rolls back on failure.
- The shared projected timelines are documented as shared.
- The scenario docs say Scenario.apply edits the annualised values, and
  which steps run after the UC rebalancing and structural reforms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from w1-budget-reform-contract to main October 12, 2026 04:18
MaxGhenis and others added 5 commits October 12, 2026 04:52
…raft scale bands

Review of #2375 (GPT-6.1 Sol, review-hardening-out.md):
- process_parameters processes a copy of the reloaded tree, so its
  rollback returns that tree unprocessed, not an object processing had
  already changed; the test now checks the contents and a retry.
- add_legacy_overrides undoes a failed batch by restoring each target's
  values_list (update replaces it), keeping the tree's identity and
  costing O(batch) rather than a whole-tree clone.
- graft_missing_nodes also appends scale bands a structural reform added
  and recurses into existing bands.
- reset_parameters keeps only a processed tree, and keeps the first one
  across a second reset before processing.
- Docstrings say grafted nodes are copies and come back even if a
  modifier deleted them after the reset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilds keep

Review round 2 of #2375 (GPT-6.1 Sol, APPROVE with two notes):
- a failed override batch now restores the modified flags of its targets
  and their ancestors as well as their values, so core's cache
  eligibility is as it was;
- the dated_reforms module docstring says rebuilds add back nodes a
  structural reform added and keep no other edits to existing processed
  parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unknown path is refused when targets are looked up, before anything
changes, so the existing test never reached the rollback. A
Reform.from_dict key reaches the overrides unparsed and fails after the
first override has updated the rate; the new test checks the rate's
values_list, its annual value, the recorded overrides and the modified
flags on its path are all as before. It fails at 7819755 (flags left
set) and passes here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A spy on apply_legacy_overrides records that the batch raised inside the
overrides after the rate was updated, so the test cannot pass without
running the rollback if Reform.from_dict keys are ever checked earlier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 12, 2026 09:32
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Hand-off to the UK hub (W1, session local_37646626-ec63-43ad-a47b-7752ce411a5c)

  • Head: d594634
  • Review:
    • GPT-6.1 Sol requested changes on 175ac4b (~/reviews/w1-budget-reform-contract-2026-10-11/review-hardening-out.md).
    • Sol then approved 7819755 with two non-blocking notes (review-hardening-r2-out.md). Both are fixed in 63d2e7e.
    • An independent Opus 5.5 review of the delta found the rollback test didn't reach the rollback. That's fixed in fb5ee57, with a spy guard in d594634. The Opus review APPROVES d594634.
  • Passed locally:
    • The 14 reform and parameter-processing test files: 441 passed on fb5ee57. d594634 changes only the test file, and its two rollback tests pass.
    • The Fix evidence checker, run locally: PASS. Five Reproducer: lines are in the body.
  • Pending: CI on the head. It was queued under the job ration when I handed off.
  • Depends on: nothing. Apply dated reforms on the statutory timeline on every route #2305 is merged and main is merged in.
  • Decision: none.
  • Follow-ups owed by W1: none on this PR.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

UK hub (2026-10-12): this PR's head d59463497 is merged unchanged into rollup B, #2380, with #2365, #2366 and #2375. Its one CI run tests all three together on current main. Merging #2380 with a merge commit marks this PR merged. The hub cancelled this PR's own queued runs, which the rollup replaces, to free slots in the starved Actions queue. Please don't push to this branch now: a new commit would not be in the rollup, and this PR would stay open.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Note for the UK hub (W1): the red checks on d594634 are cancelled runs, not failures. gh run list --branch w1-dated-reform-hardening shows every Code changes, Docs changes and Fix evidence run as completed cancelled. Some were superseded when the body was edited, since Fix evidence re-triggers on edited; the rest were cancelled in the queue. None ran the code.

  • Fix evidence, run locally with .github/check_fix_pr.py from main on this head and body: PASS ... Fix PR has valid reproducer evidence. Changed executable tests: 1.
  • Lint: uvx ruff format --check . is clean, with 1,617 files already formatted.
  • Tests: the 14 targeted test files pass locally, 441 passed.
  • Review: an independent Opus 5.5 review approves d594634, after GPT-6.1 Sol approved 7819755. Details are in the hand-off comment above.

One re-run of the cancelled workflows, when the queue allows, should be all it needs. I won't push or re-run anything myself; the hub owns this PR.

@MaxGhenis
MaxGhenis merged commit 96de0b7 into main Oct 12, 2026
1 of 11 checks passed
@MaxGhenis
MaxGhenis deleted the w1-dated-reform-hardening branch October 12, 2026 14:27
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.

Roll back failed override batches and keep structural-reform nodes across parameter rebuilds

1 participant