Skip to content

Uprate additional State Pension and protected payments by September CPI - #2336

Draft
MaxGhenis wants to merge 6 commits into
mainfrom
asp-cpi-uprating-1941
Draft

MaxGhenis wants to merge 6 commits into
mainfrom
asp-cpi-uprating-1941

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1941

Law

Additional pensions and protected payments follow prices separately from the full State Pension rates. SI 2026/148 article 4(3) directs the covered additional pensions to be “increased by 3.8 per cent.”, with a final relevant year before 2025–26. Article 6(3)(b) covers amounts “at the transitional rate which exceed the full rate” and applies the same 3.8%. The full rates rise about 4.8%: £176.45→£184.90 and £230.25→£241.30 (articles 4(2) and 6(1)).

The equivalent 2025 article 4(3) and article 6(3)(b) direct the add-ons to be “increased by 1.7 per cent.”, against about 4.1% for the full rates. The made HTML and XML were read for both Orders; the current 2025 /data.xml omits these provisions, so the operative text was checked at /made/data.xml.

SSAA 1992 sections 150–151 provide the prices review and percentage increase for additional pensions; section 151A covers the transitional excess. These duties prescribe an increase when prices rise, rather than a cut when they fall. The 2016 Order explanatory memorandum, paragraph 4.2 and footnote 2, confirms no increase under section 150 when September 2015 CPI was −0.1%. Section 150A sets an earnings minimum for the full rates; it does not itself legislate all three triple-lock elements.

Change

additional_state_pension uprates the reported excess above the period type's legislated data-year full rate by I(period) / I(data_year), where I is the simulation's own gov.economic_assumptions.indices.september_cpi_uprating. CPI or macro scenarios can therefore move the add-on. The baseline data-year split from #2122 remains; basic and new flat-rate components retain their full-rate ratios.

Reforms to the full rates or the triple lock no longer move the add-on because in law it follows prices: SI 2026/148 articles 4(3) and 6(3)(b). This fixes the model to the statute; it is not a methodology choice and needs no separate decision. This intentionally changes the #2122 full-rate-reform expectations, including a rate of zero. gov.contrib.abolish_state_pension still removes the entire pension.

The existing September CPI builder was read, including its docstring: published September CPI is rounded to 0.1 percentage points and floored at zero. The test reference hard-codes the statutory rises for 2011–2026 and independently rounds CPI inputs only for forecast years; it never calls the production index. Those historical percentages were checked against the Orders, including zero in April 2016. The model rounds each compounded index to five decimal places, so ratio/reference checks allow 1e-5 rounding tolerance.

Files: policyengine_uk/variables/gov/dwp/additional_state_pension.py:50 (baseline split), :79 (simulation CPI), :82 (index ratio); policyengine_uk/tests/test_state_pension_components.py:101 (Order reference), :275 and :306 (dataset regressions); docs/book/programs/gov/dwp/state-pension.md (Components, Uprating and fixes); changelog.d/1941.fixed.md.

Tests and fail-on-main evidence

The two statutory regressions build 2024–25 data: BASIC, £200/week, has 2026–27 additional pension (200 − 169.50) × 52 × 1.017 × 1.038 = £1,674.254556; NEW, £240/week, has protected payment (240 − 221.20) × 52 × 1.017 × 1.038 = £1,031.9995296. Separate growth cases check each year's CPI rise. Situation simulations use the period as data year, so dataset-built cases are necessary.

At the previous continuation head 2fd0c7a8be714487be8895ffa248b600d48ba8e0, CI run 38200686793 passed: 3,058 YAML tests; 5,125 Python tests, 67 skipped and 2 expected failures; zero failures, across three shards with four workers each. The shard gate verified all 5,194 collected Python tests ran exactly once. Lint, documentation build and smoke installs on Python 3.11–3.14 passed. The latest continuation commit changes documentation only. The older 15bb98d44 CI had only the known dataframe round-trip failure, fixed by the merged #2349.

The recovered local YAML log records 298 cases passed in 737.73 seconds across 24 State Pension/downstream files at 4b9264f19; the exact old invocation was not preserved. Completed full CI at the formula head 2fd0c7a8 covers the other State Pension readers and the DWP folders, subject to its reported dataset-access skips. Focused uv run --active --no-sync pytest policyengine_uk/tests/test_state_pension_components.py -n 4 -vv --tb=short at that head passed 31/31 in 1,140.63 seconds, with no skips or failures. All four Hypothesis tests ran ten deterministic examples each. The final documentation-only head has identical formula/test source.

At 4a6b736a09076e303f3e646c85f5709805c93de4, documentation build, lint, Python 3.11–3.14 smoke installs, full YAML and Python shards 1/3 and 3/3 pass. Python shard 2/3 reported results for all 1,791 collected cases (1,765 passed, 25 skipped, one expected failure, zero failed), including explicit passes for all 14 State Pension cases in that shard, then received a runner shutdown before its final summary/artifact upload. The log says “The runner has received a shutdown signal” and “The operation was canceled.” Its job status is failed and the overall gate is incomplete; this is not a model assertion failure. The underlying shutdown cause is unverified. No source change or CI retry was made for the interruption.

Fail-on-main: an isolated clone at the merged main 8019c0ccea83f414d6b44a90c2c64fe5fc9b5ee8, with only the updated component test file copied in, produced 23 expected assertion failures and 8 passing controls in 1,843.53 seconds, exit 1. Both statutory pension cases, both annual-growth cases, all four properties and the changed add-on reform expectations fail. There were no import/fixture failures. The eight passing controls exercise unaffected behavior. Command: PYTHONPATH=<main clone> uv run --active --no-sync pytest policyengine_uk/tests/test_state_pension_components.py -n 0 -vv --tb=short -o usefixtures=cloned_uk_tax_benefit_system. This existing repository fixture loads model code once and clones independent state; no conftest/model source was edited. A shared clone replaced the linked worktree because linked-worktree metadata is outside the sandbox's writable roots. These old-main property violations are intended evidence of the statutory defect, not new failures on the fix.

Formatting/lint: uv run --active --no-sync ruff format and ruff check on additional_state_pension.py and test_state_pension_components.py: two files unchanged, all checks passed. The touched-file equivalent was used because workspace-only scratch clones make repository-wide make format traverse duplicate test trees.

Invariants and mutation evidence

Four Hypothesis tests, ten deterministic examples each, cover:

  • Deflating each component by its own uprating recovers the reported pension for eligible records, including arbitrary later ages and BASIC/NEW type transitions; components have no gap or overlap.
  • The add-on is nonnegative and monotone in reported pension; annual growth equals September CPI when the type is unchanged.
  • Differential against the old main formula: flat parts unchanged, data-year add-on unchanged, later differences attributable only to CPI versus the flat-rate ratio.
  • Differential for the same records with and without arbitrary full-rate reforms (including zero, undated and future-dated): add-on unchanged.

All four properties were also reduced to one initial BASIC record (male, age 90, three months since birthday, £200/week in 2024): 4 expected failures on main in 7.33 seconds; 4 passes on head in 6.98 seconds. The monotonicity case adds the required paired record reporting £1/week more; the reform case sets both full rates to zero, undated. Main's deflated accounting result is £10,437.43275 against a £10,400 report, and its zero-rate reform removes £1,586 of data-year add-on; the fix preserves both identities. These are synthetic counterexamples, not national figures.

The saved mutation run had 12/12 unmutated checks pass and 5/5 mutants killed: restore full-rate uprating; read CPI from baseline instead of the scenario; remove the floor; use the prior-year index; split at the reformed data-year full rate. This evidence predates the main merges; its formula source is identical to the merged formula head. A fresh scratch-tree confirmation passed three unmutated statutory/scenario controls, then killed both restored full-rate uprating (one intended assertion failure) and baseline-CPI reading (one intended assertion failure). The source was restored and the scratch clone removed. Each command used uv run --active --no-sync pytest <selected nodeids> -n 0 -vv --tb=short -o usefixtures=cloned_uk_tax_benefit_system; no model file was mutated in the actual head.

Impact on the certified pair

Not measured on the current base: sandbox access blocks the required machine-wide heavy queue. Base is the exact merged main 8019c0ccea83f414d6b44a90c2c64fe5fc9b5ee8; final head is 4a6b736a09076e303f3e646c85f5709805c93de4. The interrupted launch preparation originally targeted 2fd0c7a8be714487be8895ffa248b600d48ba8e0; the final commit changes documentation only, and the prepared retry script is pinned to the final SHA. Dataset: enhanced_frs_2024_25, uk-data 1.58.0, hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@1.58.0, 128,369,110 bytes, verified SHA-256 5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6.

The unchanged required heavy runner first failed its sandbox-denied os.nice(0) query. A read-only os.getpriority adapter confirmed actual niceness zero while preserving the shared queue, locks, thread caps and 20 GB ceiling. The runner then failed creating its queue ticket with operation not permitted under ~/reviews/test-runner-memory-2026-10-02/heavy.queue/; it was stopped, with no simulation child started. Runtime and all 2024–2030 totals, changed-person counts, BHC/AHC poverty rates and decile impacts remain unverified. No stale result or extrapolated figure is presented as current evidence.

When the full-rate rise exceeds prices, this correction lowers projected State Pension and widens the shortfall against the OBR/DWP table in the docs. The current national size is unverified. The unsupported stale quantified paragraph was removed from the docs; their forecast table was not rerun. Impact scripts are prepared to copy/hash the certified dataset privately per tree and produce weighted aggregates, all requested poverty rates and deciles with primary/complementary small-cell suppression. A synthetic disclosure check passed.

Continuation and overlaps

Already done: formula, tests, docs/changelog, five mutation checks, both main merges, and Axiom queue issue. Added here: recovery/audit of the pushed continuation, statutory/skill/overlap verification, current test evidence, precise statutory documentation and removal of stale impact figures, hardened disclosure reporting, and this full PR write-up. No uv.lock is in the PR diff.

Axiom parity

rulespec-uk#457 is open, labelled pe-parity, and dispatch-ready: module paths uk/regulations/uksi/2026/148/article/{4,6}.yaml, corpus citations, verbatim law, required outputs, pasteable review_finding, and companion cases derived from the same Orders. Live modules/corpus still include only the flat-rate provisions. Re-ingestion/release/rebind of the missing corpus paragraphs is the recorded blocker; no RuleSpec was handwritten or billed encoder dispatched.

axiom: TheAxiomFoundation/rulespec-uk#457 queued

Not verified

Current national impact for 2024–2030 and numerical movement against OBR/DWP forecasts; completed latest CI gate/artifacts after runner shutdown; the underlying shutdown cause; local broad-suite rerun after the final merge (full CI passed at the formula head, with its reported dataset-access skips). The documentation build passed in CI after the final clarification. SKILLS_BLOCKED: model-development runtime loader/catalog unavailable; its complete source and relevant references were read, and the canonical repository guidance was used as the brief's explicit fallback. The historical local YAML and five-mutant shell invocations were not preserved; complete results/source were recovered, and current focused/mutation commands were recorded.

This PR remains a draft.

🤖 Generated with Claude Code

Reproducer: policyengine_uk/tests/test_state_pension_components.py::test_protected_payment_above_new_state_pension_follows_period_type, policyengine_uk/tests/test_state_pension_components.py::test_unchanged_type_splits_at_its_own_flat_rate, policyengine_uk/tests/test_state_pension_components.py::test_new_in_data_year_and_basic_in_period_is_paid_in_full, policyengine_uk/tests/test_state_pension_components.py::test_additional_pension_rises_with_september_cpi_not_the_basic_rate, policyengine_uk/tests/test_state_pension_components.py::test_protected_payment_rises_with_september_cpi_not_the_new_rate, policyengine_uk/tests/test_state_pension_components.py::test_add_on_grows_each_year_by_that_years_september_cpi_rise

The part of a reported State Pension above the type's full rate (additional
pension for the basic State Pension, protected payments for the new) was
uprated by the full rate's own triple-lock rise. Each Up-rating Order raises
it by September CPI instead (SSAA 1992 ss.150(1)(c), 151 and 151A; SI
2026/148 arts 4(3) and 6(3)(b): 3.8% against 4.8% for the full rates; SI
2025/295: 1.7% against 4.1%). Uprate it by
gov.economic_assumptions.indices.september_cpi_uprating from the data year,
read from the simulation's own parameters, so a reform to the full rates or
the triple lock no longer moves it and a price scenario does.

Fixes #1941.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rms as a reform

CI found two test faults (run 38161065391): the differential against main's
formula compared the model's float32 arrays with a float64 reference at
rtol 1e-12, which fails for any reported amount float32 cannot hold exactly
(e.g. £169.00 a week, aged 98); and a Scenario cannot take the reform
dictionary's "2026-01-01.2100-12-31" period, so that case passes it as a
reform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 11, 2026 17:14
…in the docs

At 5% September CPI the triple lock also rose 5%, so the scenario test passed
on main. At 1%, below the triple lock's 2.5% floor, the add-on rises 1% and
the full rates at least 2.5%. The docs record the change in the model's State
Pension against DWP's forecast, from full runs on the certified dataset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the September CPI uprating in additional_state_pension: main's
np.divide guard (00c07d6) was on the full-rate ratio this branch replaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 12, 2026
…branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: GPT-6 <noreply@openai.com>

This branch has not been deployed

No deployments
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.

Uprate additional State Pension and protected payments by CPI, not the flat rate

1 participant