Skip to content

UK pensions: resident State Pension, Pension Credit, contributions and salary-sacrifice relief (#1069) - #1084

Merged
juaristi22 merged 22 commits into
mainfrom
uk-pensions-1069
Oct 2, 2026
Merged

juaristi22 merged 22 commits into
mainfrom
uk-pensions-1069

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #1069 (UK pensions). It implements the approved plan's c1–c12 and the first review round; c0 waits for PolicyEngine/policyengine-uk#1899.

What this does

  • Facts. It re-pins the Chronicle feed to 825406f, which carries the UK pension facts from UK pension facts for the 2025 calibration: DWP State Pension and Pension Credit caseloads from Stat-Xplore (UK residents), Northern Ireland pensions, workplace pension contributions, and the unported pension rows of files Chronicle already archives chronicle#302 via META: publication + validation portfolio to a well-validated populace launch #305, and adds the consumer operations those facts need.
  • Resident State Pension.
    • It binds the resident level from Stat-Xplore (GB) and DfC (NI), with the age-by-sex-by-type, area and weekly-amount rows.
    • The OBR line and DWP's forecast lines become diagnostics.
    • The empty £40–60 band is signed out.
  • State Pension age boundary.
    • SPI donor ages follow ONS single-year shares (c5).
    • The donor forest is queried at mid-year, which removes a zero-State-Pension artefact at 66 (c5 fix).
    • The SPI channel's prior mass is split by region and by whether a household has a member at State Pension age (c6), with a pension-age share σ_p = 0.2 (ruled 2026-10-02).
  • SPI State Pension amounts by band. They now move to 2025 by their count rows' recipient growth times the State Pension rate, not the rate alone. The two rows of each band had described different numbers of recipients; this was the £50–70k band's residual (D6).
  • Pension Credit (c7). Fourteen caseload targets, and a take-up stage that applies DWP's FYE2024 rates by component.
  • Contributions (c8–c9).
    • Personal contributions are no longer clipped.
    • Employer contributions are drawn from ASHE rate bands instead of three times the employee contribution.
    • Converted salary-sacrifice records lose the sacrificed pay.
    • DWP's employer total (GB) is bound.
  • Pension-age benefits (c10). Attendance Allowance in England and in Wales, and pension-age Housing Benefit, plus Winter Fuel recipients as a diagnostic.
  • Salary-sacrifice relief (c11). The measure resolver runs a counterfactual simulation (the sacrifice returned to pay), and the income-tax and NICs relief rows from HMRC Table 6.2 bind through it.
  • Clean-up.
    • Retires the stale D6 target-fit deferral.
    • The SPI allocation's reason is read from its declared shares everywhere.
    • Re-pins the gate digests.
    • Adds the receipts note experiments/1069-uk-pensions-receipts.md; Part J covers the review round.

Measurements (local, measurement-only builds; details in the receipts note)

  • Build at 677336b (fix20; the head adds only the receipts note). Every gate passes. Loss 0.00742 on 1,170 targets, against 0.00836 on 1,090 for the 30 September main build. 98.0% of rows are within 10%, ESS is 5,007, and no row is above 25%.
  • SPI State Pension bands. Every band is within 4%, except £20–30k at +13.1%, a headcount residual (8% too many recipients) that the uprating does not cause. Before the fix, £50–70k sat at +24.5%, or +25.6% at σ_p = 0.2.
  • Engine, FY2025-26, on that build's H5.
    • State Pension £142.5bn and 12.41m recipients, against the £142.3bn resident level.
    • Pension Credit 1.46m benefit units and £6.16bn, against targets of 1.45m and £6.11bn. Realised take-up is 71.6% (Guarantee Credit) and 40.9% (Savings Credit only), against DWP's 69% and 37%.
  • HBAI.
    • Pensioner quintile shares before housing costs are 21.5/27.9/21.1/16.7/12.7, against HBAI's 22/25/21/18/14.
    • Pensioner relative poverty is 15.2% BHC and 15.3% AHC, against HBAI FYE 2025's 16% and 14%.
  • CPI-only State Pension probe. +£3.11bn by 2030 (bootstrap SE £0.07bn), within 0.5% of the σ_p = 0.5 build in every year.
  • σ_p, 0.2 against 0.5 at 677336b. Both pass every gate. 0.5 fits single ages 62–63 and the pensioner quintiles better. 0.2 fits Pension Credit take-up after calibration, the pre-calibration benefit caseloads and the £20–30k State Pension band better. Details are in Part J.

For review (rulings owed)

  • Table 3.8 and the DWP employee row. Both stay out of the fit: Table 3.8 splits contributions by relief mechanism, the engine's columns by provider. Vahid recommends binding the combined Table 3.8 amounts against employee plus personal contributions, with the DWP employee row as a diagnostic.
  • The salary-sacrifice amount. The relief rows fit, but £34.3bn is sacrificed against the £24.6bn implied, because records paid under £9,100 hold about half of it. UK spine follow-ups after #1045: SPI band donors as a mass-conserving support channel, WAS tenure coherence, the residential draw's bound and Table 7 composition, then a national build #1063's earners-only fix resolves it. Vahid recommends keeping c9's amount row bound until then.
  • The employer NICs relief is restated from HMRC's 13.8% to the statutory 15% (£3.70bn) by the rate only. On the fix20 H5 the engine-measured factor for earners is exactly 15/13.8; the threshold effect sits only on the records paid under £9,100.
  • Single ages. 65 is −17%, 64 −6%, and 62 and 63 are +15% and +12%. Vahid recommends the R3 split (65, 66, 67–69) now.
  • The income-tax relief bands, once the band test is fixed: bind the bands and make the total a diagnostic, or the reverse.

Not in this PR

Verification

  • CI passed every job at 004b5c9.
  • At 677336b, the affected suites pass locally: the HMRC uprating, contract, target-reference, State Pension, Pension Credit, pension-age benefit, contribution, SPI and coverage tests, the shared spec and contract-pin tests (417), the CI test-plan check, lint and the integration-uk smoke build.
  • Every derived surface was regenerated at its own commit: national references and membership, both national parity receipts, the coverage manifest and the charter-H2 fixture. No gate digest moved with the uprating fix.

🤖 Generated with Claude Code

juaristi22 and others added 20 commits October 1, 2026 13:35
…on age (#1069 c5)

The SPI income stage drew donor ages uniformly within each AGERANGE band, and
its 65-74 band stopped at 74, so State Pension (SRP) donors landed on
65-year-olds and the stage-2 receipt bridge carried State Pension below State
Pension age.

- Donor ages are drawn within the band by the ONS mid-2023 single-year
  populations of the donor's sex (uk/ons_single_year_age_populations.json,
  vendored from the pinned Chronicle feed and declared as the stage's
  donor_age_populations artifact). SRP recipients draw at or above the
  engine's State Pension age; the band's below-SPA share is drawn from its
  non-recipients, and any shortfall is receipted.
- AGERANGE 6 is [65, 75) and 7 starts at 75.
- Band carriers draw from their own SPI age band when that pool holds 20
  records (composites never age-match), then narrow by region as before.
- A deterministic guard zeroes the SPI State Pension leaf before the bridge,
  base-channel State Pension reports before stage 2 trains, and the SPI
  channel's State Pension, Pension Credit and Winter Fuel reports after stage
  2, each with a receipt of rows, weighted people and pounds removed.

The coverage manifest and the charter-H2 parity fixture are regenerated; the
fixture's synthetic CGT asset-type facts move because they are sized to the
frame the upstream stages leave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#305, UK pension facts) (#1069 c1)

The artifact is chronicle main at the #305 merge: 344,402 rows (505e0e7's
287,150 plus #305's 57,252), consumer_fact.v4 with an unchanged schema. It also
carries chronicle #246, #278, #292 and #304.

- No existing value moved. The national and local reference files are
  byte-identical, and the 18 vendored resources carry the same rows under the
  new feed identity.
- The artifact is not a strict row superset of 505e0e7. #305 rebuilt the DWP
  Spring 2026 package's record sets: 44 incapacity-benefit forecast rows come
  back under new keys with the same line and value, and 192 rows change only
  their layout. No UK target binds them.
- The five hmrc.salary_sacrifice relief targets now also match the 2023-24
  Table 6.2 rows and still resolve to 2024-25. For the same reason, the 2023
  production compile-parity receipt gains five ledger-only rows. The two 2025
  receipts are unchanged.
- The charter-H2 fixture's stage_contract_sha256 moves for the eight stages
  that declare vendored resources; the runbook now says a re-pin regenerates
  it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- monthly_window_count_x_mean: averages a published count times its published
  mean over declared months. The count and mean operands name their concepts,
  because DWP names its measures `recipients` and `mean_weekly_amount`, not
  `*_count` and `*_mean`. The mean operand's period_factor annualises a
  weekly mean. Each month pairs the count and mean of one published cell and
  one publication, although Stat-Xplore files them in sibling record sets.
  Like the single-series windows it takes the source_window period policy,
  so its declared months are never re-selected or held for uprating. The
  existing count_x_mean and single-series windows are unchanged.
- The engine-index uprating applier restates each window month's mean at the
  calibration year's rate: April onward at that year's rate, January to March
  at the previous year's. Under ruling R1, only a February point moves.
- A monthly window summing published cells no longer refuses cells whose
  numeric band edges differ (a single year of age carries its own
  `age >= 66` and `age < 67`): the edges belong to the cell, while each
  cell's own months are still compared on every constraint.
- The band parser reads DWP's half-open amount labels ("£20.00 to under
  £40.00", "Under £20.00"), each keeping its own published upper edge.
- dwp_state_pension and dwp_pension_credit join the source-month families,
  and a count x mean window resolves two members per month.
- The generator pins dfc_ni.* targets to Northern Ireland and
  dwp.winter_fuel_payment.* to England and Wales. K04000001 is now a
  labelled geography.

On the pinned 825406f feed, the calendar-2025 State Pension level compiles to
£138.62bn for Great Britain and £3.71bn for Northern Ireland at 2025 engine
rates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contract gains 26 targets in the new dwp_state_pension family, which
compile to 60 references. They come from the DWP Stat-Xplore and DfC NI facts
of PolicyEngine/chronicle#302 (via #305), on the calendar-2025 window of four
quarterly points (ruling R1).

- Level: Great Britain residents' recipients (12.05m) and amount (£138.6bn,
  recipients times the mean weekly amount times 52, restated per point at
  the engine's 2025 rate), plus Northern Ireland (0.33m and £3.7bn).
- Age: recipients by age band x sex x type, summed over the published single
  years. Only the cells DWP populates are bound, because the type follows the
  State Pension age date.
- Area: recipients by type in the nine English regions, as a region-tier
  fan-out pinned to England. Scotland and Wales are their own rows, because
  DWP has no Northern Ireland cell for a twelve-area tier.
- Weekly amount: recipients by weekly amount band x type, on the May to
  November points DWP pays at the 2025-26 rate. The detail measure pin leaves
  out the publisher's 'all' margin row.

Also in this commit:
- The dfc_ni provider and the two State Pension categories, so the local
  references restate the hierarchy.
- A dwp_state_pension fan-out outcome in the generator.
- Active national references move from 1152 to 1212. The 2025 incumbent
  parity receipt gains 60 ledger-only rows; the 2023 surface gains 58 (the
  two amounts cannot move from 2025 back to 2023).
- A feed-gated test that the age and area shapes each partition the Great
  Britain caseload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cs (#1069 c4)

Rulings R4 and R2: obr.state_pension leaves the fit. The OBR FY2025-26
forecast (£146.19bn) counts Great Britain plus pensioners paid abroad and
leaves out Northern Ireland, so it measures a different quantity from the
resident level that c3 binds.

- The exact OBR figure, plus DWP Spring 2026's Great Britain total (£146.07bn)
  and paid-abroad line (£5.63bn), ride on dwp.state_pension.amount as
  metadata only. DWP's caseload lines (13.204m, of which 1.088m abroad) ride
  on dwp.state_pension.recipients.
- The diagnostic attachment, written for the CGT cash forecast alone, is now
  driven by the contract's diagnostic_references. Each declaration names its
  metadata prefix and value unit.
- UK_REQUIRED_TARGET_DIAGNOSTICS names the declarations each target requires,
  so a missing or misdirected declaration still refuses the target. The CGT
  metadata keys are unchanged.
- The incumbent obr/state_pension row moves to registry_parity.excluded.
  Active national references go from 1212 to 1211.
- The compile-parity receipts give the fixture-only OBR row and the 60 State
  Pension rows their ruled rationales.
- The UC support probes protect dwp.state_pension.amount in place of the
  OBR line.
- The spec-only entrypoint guard also admits a fact identity under
  ledger_fact_key. One of the new keys' digests starts with a letter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DWP's new State Pension "£40.00 to under £60.00" weekly-amount cell
(2,903 recipients over May to November 2025, 0.06% of new State Pension
recipients) has no record on the frame: the nearest reporters sit at
£58.59 a week and restate to £60.99 at the 2025-26 rate, so the c4
measurement arm fitted the row at -100% and the target-fit battery
blocked the H5 on it. The row is signed out of the weekly-amount
fan-out in target_reference_signed_exclusions.json (María, in session,
2026-10-01); the other nine new State Pension bands stay bound.

Active references 1,211 -> 1,210 (signed out 16); the dwp_state_pension
family binds 59 of its 60 references. The generator now lists
dwp_state_pension row sign-outs beside the hmrc_cgt ones in the
membership report's signed_exclusion_rationales.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#1069)

The c4 measurement arm left 31% of the frame's 66-year-olds without State
Pension against DWP's ~3%. In the c4 spine, 330 of the 402 such records
are SPI-channel synthetic people (88% of their weight): the SPI channel
shows no State Pension for 100% at 65, 51% at 66 and 6% at 67, while FRS
respondents show 7% at 66.

Since c5 the donors carry a drawn fractional age (a whole year plus a
uniform fraction) and no donor below State Pension age has State
Pension. Frame people carry a whole-year age, so a 66-year-old queried
at exactly 66.0 sits on the forest's split at that birthday and lands
with the 65-year-old donors whenever the split falls just above it; the
zero State Pension leaf then sets the stage-2 receipt bridge. A
synthetic forest reproduces it: queried at the whole year no 66-year-old
draws State Pension, at the middle of the year all of them do.

- Stage 1 (and the base-channel dividend redraw, which reuses the forest)
  now queries floor(age) + 0.5, the middle of the year of age; stage 2
  compares frame ages with frame ages and is unchanged. The rule is in
  the stage evidence as donor_age_draw.stage1_query_age.
- Band carriers draw only donors whose drawn age is on their side of
  State Pension age (the published 65-74 band straddles it), before the
  age-band and regional narrowing; the receipt counts the side-matched
  carriers. 32 of the 330 came from this path.

The H2 fixture's synthetic CGT asset-type facts move with the SPI draws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… status (#1069 c6, provisional share)

PROVISIONAL: the pension-age share is 0.5 (stratification only) until the
measurement arms choose it; this commit is amended with the measured share.

Every adult in an SPI support household carries a taxpayer's SPI income,
and the channel's 10,000 households are drawn unweighted and each given
its region's average weight. FRS pensioner households carry lower design
weights than the average, so the region-only allocation gave the channel
more pension-age mass than the FRS design gives pension-age households:
at prior weights on the c4 spine the channel held 5.67m pension-age
households against 4.44m in the FRS channel, and 7.81m of the 13.88m
people aged 66 and over (design weights give about 12.1m; ONS 12.4m).
Its pension-age taxpayer share is 83% against the FRS channel's 68%
(HMRC: 67% of State Pension recipients pay income tax).

- build_uk_spi_support_channel takes pension_age_share and
  state_pension_age: within each region it splits households with a
  member at or over State Pension age (a stratum derived for the
  allocation and never written to the frame) and gives the channel that
  share of their mass, 50% of the rest. The receipt carries both shares,
  the State Pension age and the pension-age masses.
- The stage reads State Pension age from the engine at the build
  instant (the c5 guard's source); the manifest declares
  pension_age_share and pension_age_stratum beside the reviewed constant,
  and the parser refuses drift.
- uk_stage_spi_support_channel_mass checks the declared pension-age
  share against the receipt.

The schema, the coverage manifest and the charter-H2 fixture move with
the stage contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the SPI chain (#1069 c7)

Rows (family dwp_pension_credit, 14 targets, CY2025 mean of the four
quarterly Stat-Xplore points; PolicyEngine/chronicle#302 via #305):
- Great Britain benefit units in payment (1,390,586).
- Six type-by-partner cells: Guarantee Credit only, Savings Credit only
  and both, with and without a partner. The engine reads the type from
  the paid components (guarantee_credit and savings_credit each positive
  or zero where pension_credit > 0) and the partner cell from
  relation_type.
- Six age bands (65-69 to 90 and over) on the eldest adult's age: both
  partners must have reached State Pension age to claim. DWP's partnered
  cells by age run from 1k to 30k, so the bands bind across type and
  partner rather than the full 36-cell grid.
- Northern Ireland claimants (DfC).
obr.pension_credit stays bound. Active national references 1,210 ->
1,224; the 2023 and 2025 parity receipts gain the ledger-only rows and
the local receipt does not move.

Take-up (R9). frs_take_up drew would_claim_pc early, on FRS incomes,
at a flat 70% that misread the FYE 2020 publication, over every benefit
unit. A new stage, pension_credit_take_up, after uc_capital_coherence
and before CGT cloning:
- materializes the engine's pre-take-up guarantee_credit,
  savings_credit and is_pension_credit_eligible (consumed only);
- within each DWP take-up component (Guarantee Credit, with or without
  Savings Credit; Savings Credit only) a reporter claims and the other
  entitled units claim at r = (t*E - R) / (E - R), t being DWP's FYE
  2024 caseload take-up (69% and 37%, checked against the gov.uk
  publication), E the component's entitled units and R its reporters,
  weighted at the stage's household weights;
- uses frs_take_up's would_claim_pc identity stream, and rewrites the
  column (frs_take_up keeps its early draw for the intermediate engine
  simulations).
A stage gate, uk_stage_pension_credit_take_up, checks each component's
realised take-up (within 0.05, or at least the rate where reporters
alone exceed it). The terminal take-up signal gate no longer measures
would_claim_pc, whose rate is a share of entitled units.

New stage surfaces: manifests, the assign_component_take_up_residual
op kind and schema branch, graph cells and consumes (open surface,
reader-isolation boundary), kernels, the fixture tool, the roster 35 ->
36, the take-up contract's two FYE 2024 rates, the microcosm-data gate
maps, the coverage manifest and the charter-H2 fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… reported personal contributions (#1069 c8)

Rows (family hmrc_spi, PolicyEngine/chronicle#302 via #305): SPI 2023-24
collated Table 3.8 net-pay-arrangement and relief-at-source pension
contributions, amount and count, each fanned over the table's total-income
bands. Amounts are uprated by the engine's average-earnings index (R5),
counts by HMRC's projected taxpayer growth for the band. The net-pay rows
measure employee_pension_contributions and the relief-at-source rows
personal_pension_contributions. Active national references 1,224 -> 1,276;
the 2023 and 2025 parity receipts gain the ledger-only rows; the local
surface, the coverage manifest and the charter-H2 fixture do not move.

FRS spine: personal and stakeholder contributions are no longer clipped
at the 95th percentile of every PENPROV amount. The clip removed 28% of
the reported amount (1.8% of rows); the engine caps the relief at the
annual allowance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SPI state-pension amount band for total income GBP 50,000 to 70,000
fits at +23.0 % on the c5-fix arm (+28.6 % on c4) and at +23.6 % and
+24.0 % on the two c6 arms: the SPI channel's 66-year-olds receive the
State Pension again (c5 fix), so the band is back inside the 25 % release
bound and the terminal uk_target_fit gate fails its reviewed deferral
(ruling D6 of 2026-09-23, expiring 2026-10-21) as stale. The entry is
retired, as the SE deferrals were in 9c17173 and 8e77a90; the
register is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kplace pension contributions (#1069 c9)

The FRS HMRC spine leaves set employer contributions to three times the
employee contribution. Each member of an employer scheme (PENPROV.STEMPPEN
1-4, occupational first) now draws a rate inside an ASHE 2024 rate band for
their scheme type and job sector (JOB.JOBSECT), applied to their pay; the
bands are a vendored Chronicle resource (81 rows; all-sector bands are the
public and private bands weighted by Table P2 jobs). The leaves stage now
reads pay (graph consumes employment_income) and the penprov and job tabs;
the HMRC/SPI chain contract expects the new operation. Converted
salary-sacrifice records lose the sacrificed amount from employment_income.

Contract (R6, R10; PolicyEngine/chronicle#302 via #305):
- DWP employer contributions into workplace pensions, calendar 2025, all
  savers, Great Britain (GBP 104.2bn), against the engine's employer
  contributions plus salary sacrifice;
- gross employee contributions (GBP 45.5bn net of tax plus GBP 20.7bn
  relief = GBP 66.2bn), a linear combination whose operands, not the row
  selector, pick the saving components;
- the 2024-25 salary-sacrifice amount, HMRC Table 6.2's employer NICs
  relief over 13.8% (GBP 24.6bn), at the default latest-not-after policy;
- the salary-sacrifice users total (7.7m) leaves the fit.
Active national references 1,276 -> 1,278. The new hierarchy category
dwp.workplace_pensions moves the local references; the coverage manifest
moves with the stage; the charter-H2 fixture writes JOBSECT and an
occupational scheme on even households.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rywhere (#1069 c6 follow-up)

c6 made the SPI support allocation separate households with a member at or
over State Pension age, so its MassChangeRecord carries the stratified
sentence. Two readers still used the 50% constant:
- the hmrc_spi_income_spine graph kernel rebuilt the preceding record with
  it, so the stage evidence named the old allocation;
- the release-input coverage manifest required it on the final dataset,
  which the release-cut uk_release_input_coverage gate would fail.
Both now call spi_spine.uk_spi_support_mass_change_reason, read from the
declared allocation. The coverage manifest is regenerated; a test ties the
helper, the support stage's record and the committed manifest together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-age Housing Benefit, Winter Fuel diagnostic (#1069 c10)

Rows (PolicyEngine/chronicle#302 via #305, #259/#260):
- Attendance Allowance cases with entitlement in England (1.60m) and in
  Wales (124k), calendar-2025 mean of the four quarterly Stat-Xplore
  points, family dwp_attendance_allowance (declares source months).
  Scotland is not bound: its caseload moves to Pension Age Disability
  Payment through 2025 (171k in February, 25k in November), which the
  engine does not model.
- Pension-age Housing Benefit benefit units, Great Britain, all tenures
  (1.11m) and the social (891k) and private (217k) rented sectors, on the
  twelve 2025 months of the existing Housing Benefit rows; the tenure
  cells are Stat-Xplore detail rows (measure "claimants"); the engine reads
  the eldest adult's age against 66.
- DWP Winter Fuel Payment recipients, winter 2025/26, England and Wales
  (K04000001, 11.0m), a required diagnostic beside
  obr.winter_fuel_allowance, pinned to its fact key: DWP pays everyone and
  HMRC recovers the payment above GBP 35,000, where the engine withholds it
  (PolicyEngine/policyengine-uk#1760).
Active national references 1,278 -> 1,283; the new hierarchy category
dwp.attendance_allowance moves the local references; the 2023 and 2025
parity receipts gain the ledger-only rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e salary-sacrifice relief rows (#1069 c11)

Resolver route (R7): resolve_target_measures no longer refuses
input_substitution_counterfactual skips. A provider exposing
counterfactual_delta(binding, period) resolves the binding's delta into a
slash-free measure-input column (counterfactual_delta__<metric>; the HDF
writer refuses slashes), which UKFrameTargetAdapter reads before the
legacy precomputed column. UKMeasureResolver builds one extra
Microsimulation per (zeroed input, folded-into) pair from the same source
at the calibration year: the sacrifice is zeroed and added to
employment_income (uk-data's recipe), and the output's delta against the
baseline comes back with a receipt. Banded measures and other periods are
refused. Both resolution entry points (prepare_uk_target_frame and
full_measure) go through the loop; injected columns are dropped before
export.

Rows (HMRC Table 6.2, 2024-25; PolicyEngine/chronicle#302 via #305):
- the employee NICs relief row binds (GBP 1.0bn);
- the employer NICs relief row binds, restated from the 13.8% HMRC's
  GBP 3.4bn was charged at to the 15% the engine charges in 2025 (pe-uk
  dates the rise at 1 January 2025): GBP 3.70bn through a new engine
  index parameter, the employer Class 1 rate (pins re-vendored from
  policyengine-uk 2.100.0), so the row anchors the sacrificed amount at
  the 2024-25 level as R10's conversion row did;
- a new all-rates income-tax relief total binds (GBP 8.8bn); income_tax
  carries the High Income Child Benefit charge and the annual-allowance
  charge, so the delta includes their response;
- c9's interim amount row retires; the three rate bands stay excluded,
  re-reasoned (the band test compares adjusted net income with
  after-allowance thresholds and ignores the Scottish bands).

Tests: the engine-free loop and resolver routes, and a real-engine persona
whose returned GBP 3,000 is taxed at 20% income tax, 8% employee and 15%
employer NICs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fit (#1069, c8/c9 follow-up)

The c9 measurement arm blocked on twelve Table 3.8 rows and showed both
families definitionally mismatched with the engine's columns:
- Table 3.8 splits contributions by relief mechanism (net pay vs relief
  at source); employee_pension_contributions holds every workplace
  deduction and personal_pension_contributions only the FRS personal and
  stakeholder pensions, while workplace group personal pensions and many
  master trusts take relief at source. At prior weights net pay sat +67%
  (amount) and relief-at-source contributors -82% (about 2.0m against
  10.9m), out of reach of reweighting; binding them cut ESS 5,004 -> 4,060
  and roughly doubled the loss.
- DWP's gross employee contributions (GBP 45.5bn plus GBP 20.7bn relief)
  sat -43% at prior against the engine's pay deductions and pulled against
  SPI's net pay (SPI net pay plus relief at source is GBP 44.1bn, near
  DWP's employees' own GBP 45.5bn).
The four Table 3.8 targets and the DWP employee row are removed from the
contract (scripts/c11b_unbind.py) pending a ruling on their
re-specification. The DWP employer row (+12% at prior, fits), the employer
draw, the conversion fix and the clip removal stay. Active national
references 1,283 -> 1,230; the local surface, the coverage manifest and the
charter-H2 fixture do not move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The measurement record for the c5 fix through c11 and the two follow-ups:
the SPI channel's 66-year-olds, the pension-age prior share (σ_p 0.5
committed, 0.2 measured at c6 and at the final state), the D6
retirement, Pension Credit take-up and caseload, the contribution rows
that left the fit and why, the pension-age benefit caseloads, the
salary-sacrifice relief counterfactual and its decomposition, the final
arm (battery passed, loss 0.00730 on 1,170 targets, ESS 4,900), HBAI
pensioner positions, the CPI-only State Pension probe with bootstrap
SEs, and the remaining gaps and rulings owed. Every H5 is measurement
only; the scripts live under data/ukds/acceptance/pensions-1069/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
)

The full engine-free job at d9fca7b failed eight tests that the
targeted runs of each commit did not cover:
- the microcosm-data contract's mirrors of the UK gate battery (policy,
  gates manifest, spec fingerprint) and of the certification parts' scoped
  digests: the pension stages and their gates (c6, c7, c9) moved them;
  six digests are re-pinned in contract.py and test_contract.py (the gate
  battery pins and the release assembler's three tests);
- the UC capital coherence stage is now followed by the Pension Credit
  take-up redraw (c7) before the deduction attributes;
- the SPI spine path-resolution test passes State Pension age to the
  support stage, which reads it from the engine for the pension-age
  allocation stratum (c6) unless given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hout blocking (#1069)

integration-uk failed on #1084: the hermetic smoke build runs the spine on
the charter-H2 fixture, where the new uk_stage_pension_credit_take_up gate
realised 0.55 Guarantee Credit take-up and found no Savings Credit only
units. The realised rates are population facts, so the gate now declares
population_fact_check like the NTS and LCFS fact gates: a synthetic smoke
build records the failure and continues; every other posture still
blocks. No gate digest moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2026-10-02)

σ_p (c6) was committed at a provisional 0.5. Measured at the final state
(the finals20 arm), 0.2 keeps more low-income pensioners in the prior:
Pension Credit caseload -24% before calibration (0.5: -36%), pension-age
Housing Benefit -28% (-41%), Attendance Allowance (England) -32% (-35%);
ESS 4,994 (4,900), top-1% weight share 0.267 (0.272), non-taxpayer State
Pension GBP 37.5bn (35.5). Its one blocked row, the SPI State Pension
50-70k band at +25.6%, is fixed at its root in the next commit.

The constant, both manifests and the stage gate parameter move to 0.2;
the release-input coverage manifest's required allocation reason and the
charter-H2 fixture regenerate; five gate digests are re-pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Automated review pass (Claude Code, high effort) — round 1 at 004b5c9c

Verdict: nothing blocking in the code. The stages are deterministic and well receipted, the new target rows read the right facts, and the targeted suites pass locally. Three should-fixes: the DWP employer row is GB but binds a UK frame; the employer NICs restatement corrects the rate but not the secondary threshold; and Pension Credit take-up is set at prior weights with no post-calibration reading. Recommendations on the five rulings are below. Separately, and outside this PR: main's slc.borrowers.plan_2_* exclusions expire tomorrow (2026-10-03), and an expired entry raises in every build.

What the 32k lines are.

  • About 26.6k are data: ons_single_year_age_populations.json (8.4k), uk_population_targets.json (6.6k), ashe_employer_pension_contribution_rates.json (4.1k), the target references and membership (5.6k), the parity signed differences (1.2k), and the fixtures, including job.tab.
  • About 2.8k are tests and 2.7k code. The code is mostly spi_income.py, pension_credit_take_up.py, employer_pension_contributions.py, ledger_targets.py, spi_spine.py, target_materialization.py and measure_simulation.py.
  • About 0.4k are docs (the receipts note).

Verified locally at this head.

  • uv sync --all-packages, then 112 passed and 4 skipped across the nine new or changed pension test files: take-up, employer contributions, State Pension targets, Pension Credit targets, pension-age benefits, SPI pension age, the window consumers, SPI support, and test_ledger_monthly_count_x_mean.
  • test_uk_battery_bindings, test_country_spec, test_uk_salary_sacrifice and test_uk_measure_simulation are green.
  • tools/ci_test_plan.py verify is ok.
  • CI: lint, wheels, engine-uk (both Pythons) and integration-uk pass; engine-free and engine-us were still running.

1. Should-fix: the DWP employer contributions row is GB, but its measurement has no country filter

dwp.workplace_pension.employer_contributions is labelled "Great Britain", and its notes say the source is ASHE, Great Britain (uk_population_targets.json:25678,25706). Its measurement.filters is null, so the engine sums employer contributions plus salary sacrifice over the UK frame, NI included.

Every other GB row in this PR filters on uk.geography.country: the State Pension amount, the Pension Credit caseload, and Attendance Allowance for England and Wales. NI is about 2.5–3% of UK employees, so the row asks GB's £104.2bn of UK records and pulls the GB employer draw down by roughly that much. Add uk.geography.country in [england, scotland, wales].

2. Should-fix: the employer NICs relief restatement corrects the rate, not the threshold

HMRC Table 6.2's 2024-25 £3.4bn is relief at 13.8% above a £9,100 secondary threshold. The counterfactual runs at the calibration year, at 15% above £5,000. The row's uprating_index (gov.hmrc.national_insurance.class_1.rates.employer) scales the fact by 15/13.8 to £3.70bn, which accounts for the rate only.

For a sacrificer whose pay is well above both thresholds, the returned pay is above both, so 15/13.8 is exact. But Part G of the receipts puts 52% of the final sacrificed amount (£18.8bn) on records with no pay. Their returned pay starts at £0. Under 2025 parameters the first £5,000 is relief-free, against £9,100 under 2024-25's. That is up to £4,100 a head more taxable at 15%, on about 1.24m people. So the engine's 2025 relief per pound is higher than the restated fact assumes, on exactly the records the solver leans on.

Two options:

  • (a) Restate by an engine-measured factor: the employer NICs delta under 2024-25 parameters over the delta under 2025 parameters, on the same frame. It costs one extra counterfactual.
  • (b) Keep HMRC's £3.4bn unrestated and record the parameter gap as a known residual, like the unuprated 2024-25 amounts (R1/R10).

Either is more honest than a rate-only factor. The point weakens a lot once #1063's earners-only fix removes the no-pay sacrificers (ruling 3).

3. Should-fix: Pension Credit take-up is set at prior weights and never read after calibration

assign_component_take_up (pension_credit_take_up.py:220-285) computes the residual rate on the stage's household weights, so realised take-up hits 69%/37% at prior. That is 69.4% and 39.1% on the c7 spine. Calibration then reweights to the 14 caseload rows while entitlement at prior is 36% short (Part D). Closing that gap by reweighting changes the entitled-to-claimant mix, so the take-up the final H5 implies can drift away from DWP's FYE2024 rates without anything noticing. The uk_stage_pension_credit_take_up gate reads the stage, not the calibrated population.

Report realised Guarantee Credit and Savings-Credit-only take-up on the final H5 (weighted claimants over weighted entitled) in Part H. If it lands well off 69/37, R9's fallback (solving take-up against the caseload) is worth reopening.

4. Nit: a licensed unit-record value sits in a committed rationale

The signed exclusion for the new State Pension £40–60 band (target_reference_signed_exclusions.json:162) quotes the two nearest FRS respondents' weekly amount, "£58.59 a week". That is an individual value from the licensed FRS. The rationale holds without it: "the two nearest restate above £60 at the 2025 rate".

5. Nit / question: the three income-tax band exclusions expire on 2026-11-25

hmrc.salary_sacrifice.it_relief_{basic,higher,additional}_rate stay excluded until 2026-11-25 (calibration_measure_exclusions.json). On expiry they fail closed: counterfactual_delta refuses banded bindings (measure_simulation.py), and an expired entry raises. When the band-test fix lands, the bands and it_relief_total both bind the same £8.8bn. Please say now which leaves the fit then (I'd keep the total and make the bands diagnostics, or the reverse), so the fix doesn't double-bind.

6. Note: D6's retirement is legitimate, but the band sits 0.5 points from the fence

Retiring the stale deferral is right: it expired on content and the row fits inside the bound. But the 50–70k State Pension band is at +24.5% against a 25% release-blocking fence. Any spine change can block the H5 on it, as σ_p = 0.2 already does (+25.6%). Worth a line in the body so the next blocked build isn't a surprise.


Rulings owed, with recommendations

  1. Table 3.8 and the DWP employee row. Take option (1): bind the combined Table 3.8 amounts (net pay plus relief at source) by income band against employee plus personal contributions, and drop the counts.
    • The engine can't reproduce the relief-mechanism split, and option (2) leaves NEST-type trusts ambiguous anyway.
    • Keep the DWP employee row as a diagnostic. Its "employees' own £45.5bn" measures nearly the same total as the combined Table 3.8 (£44.1bn), so binding both pulls on one quantity twice.
    • The gross £66.2bn includes relief the engine doesn't hold in those columns, so it shouldn't bind.
  2. σ_p: keep 0.5.
    • At 0.2 the H5 blocks (50–70k band +25.6%), single ages worsen (62 +15%, 63 +11%), and HBAI Q2 moves 2.9 points off.
    • Its gains are on prior gaps that calibration closes anyway.
    • The CPI-only probe's budget effect agrees within 0.5%, so the choice doesn't move the headline analysis.
    • Revisit after c0.
  3. Salary-sacrifice amount: keep c9's amount row bound beside the relief rows until UK spine follow-ups after #1045: SPI band donors as a mass-conserving support channel, WAS tenure coherence, the residential draw's bound and Table 7 composition, then a national build #1063's earners-only fix lands. Two reasons beyond Part G's:
    • The DWP employer row binds employer contributions plus salary sacrifice, so the inflated £36.3bn (about £11.7bn over the £24.6bn implied) displaces genuine employer contributions inside that row's fit.
    • Relief on no-pay records' returned pay is mis-specified (see 2).
    • Bound, the amount anchors both until the forest stops predicting sacrifice for non-earners.
  4. Employer NICs restated to 15%: not as a rate-only factor. See finding 2. Use (a) the engine-measured factor, or (b) HMRC's £3.4bn as published with the gap recorded.
  5. Single ages (65 −19%, 64 +9%): take the R3 fallback now (65, 66 and 67–69).
    • Age 65 sits right at the State Pension age boundary that the SPI age draws, the pension-age stratum and Pension Credit eligibility all key on.
    • A −19% error there feeds those mechanisms directly.
    • The split costs three rows, and c0 doesn't remove the need for it.

Checked and correct

  • State Pension resident level.
    • GB (Stat-Xplore, filtered to England, Scotland and Wales) and NI (DfC) are separate rows; each is count × mean weekly × 52, averaged over Feb/May/Aug/Nov 2025.
    • Each point is restated by the engine's new State Pension rate (221.20 → 230.25), so the February point isn't left at 2024-25 rates.
    • ledger_targets.py:2101-2132 refuses negative operands and records every member month.
  • Pension Credit take-up draw.
    • It uses keyed stable_identity_uniforms on benunit_id with its own salt.
    • The residual rate is clipped to [0, 1], and reporters always claim.
    • The bands follow DWP's components: Guarantee Credit with or without Savings Credit, then Savings Credit only.
  • Salary-sacrifice relief binds on hmrc.ss_* only. Every relief row's ledger_selector names hmrc.ss_income_tax_relief_* or hmrc.ss_nics_relief_*, not the generic relief concepts chronicle META: publication + validation portfolio to a well-validated populace launch #305 also emits. So the double emission I flagged there doesn't double-bind here.
  • The counterfactual resolver.
    • One cached simulation per (zeroed, folded) pair.
    • The delta is per row, with the direction declared.
    • Banded bindings are refused.
    • The receipt records the route and non-zero rows.
  • Converted salary-sacrifice records lose pay floored at £0 (salary_sacrifice.py:243), so pay can't go negative.
  • The employer draw happens on the raw FRS spine (frs_hmrc_spine_leaves). SPI-channel rows take pension columns from the FRS-only fill, not from the donor household.
  • HBAI FYE 2025 pensioner relative poverty is 16% BHC (CI 14–18) and 14% AHC (CI 12–16), as the body says. The final arm's 14.2% BHC sits at the bottom of that interval and 14.7% AHC inside it.
  • The body's measurement figures match the receipts note: loss 0.00730 on 1,170; State Pension £142.5bn and 12.42m; Pension Credit 1.46m and £6.15bn; the quintiles.
  • Privacy. Apart from 4, the committed evidence is aggregates and sample counts, with no unit records.

Outside this PR, time-critical

slc.borrowers.plan_2_liable and slc.borrowers.plan_2_above_threshold expire on 2026-10-03 on main too (calibration_measure_exclusions.json). From tomorrow every UK build and the certifier will raise on them. They need extending or retiring today, on main.

…nt growth times the rate (#1069)

The SPI Table 3.7 State Pension amount rows by total-income band moved
to 2025 by the new State Pension rate alone, while their count rows
moved by HMRC's projected taxpayer growth in the band. The two rows of
a band then described different numbers of recipients: in the 50-70k
band the count grew by 1.217 and the amount by 1.130, which is the
+24.5% residual (and the +25.6% block at a 0.2 pension-age share) the
final arm showed. A new applier moves each amount row by its count
row's factor times the engine rate ratio, receipting both.

Also, from Vahid's first review: the DWP employer contributions
measurement declares Great Britain (the binding already did), and the
signed-out new State Pension 40-60 band's rationale no longer quotes a
unit amount.

National references and membership and the 2025 parity receipt are
regenerated; the 2023 receipt, the coverage manifest, the charter-H2
fixture and every gate digest are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… root fix (#1069)

Part J records the round: the CI fix, σ_p = 0.2, and the SPI State
Pension amount uprating fix with the fix20 and fix50 arms (fresh
spines at 677336b, data-identical to the finals20 and final spines).
It also records the post-fix σ_p comparison and the measurements
Vahid's review asked for: the employer NICs threshold factors, and
Pension Credit take-up realised after calibration. Parts B, C, H and I
point to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Thanks, Vahid. Round 1 is addressed at 677336b; details are in Part J of the receipts note. Point by point:

1. Employer row GB filter: done (677336b). The contract's measurement block now declares uk.geography.country in [england, scotland, wales]. The engine binding already restricted the row to GB households: its household_conditions list the nine English regions, Wales and Scotland. So the fit was GB-only before; the declared measurement now matches it.

2. Employer NICs threshold: measured. I ran the counterfactual under both parameter sets on the calibrated frame (the H5 built at 677336b, σ_p 0.2):

3. Pension Credit take-up after calibration: measured; now in Part H and Part J. Weighted claimants over weighted entitled, on the calibrated H5s (DWP FYE2024: 69% and 37%):

  • Final build, σ_p 0.5: Guarantee Credit 74.7%, Savings Credit only 46.1%.
  • σ_p 0.2: 71.6% and 40.7%.
  • At 677336b, σ_p 0.2: 71.6% and 40.9%.
  • Calibration fixes the claimants, so the drift is in the entitled pool behind them, and it is smallest at 0.2.

4. Unit value in the rationale: removed (677336b). The rationale now says the nearest records restate above £60 at the 2025 rate. The earlier text is still in the branch history (a2bfee0, below the #1063 stack's base). Rewriting that history is María's call.

5. Relief band exclusions after the band test. My proposal: bind the three bands and make the total a diagnostic, since the bands carry the distribution and the total is their sum. María to confirm; I'll record her choice next to the exclusions so the fix can't double-bind.

6. The £50–70k band and the fence: fixed at the root (677336b). The SPI State Pension amount rows moved to 2025 by the rate alone, while their count rows moved by HMRC's taxpayer growth in the band. So each band's amount kept the 2023-24 number of recipients while its count row grew them; in £50–70k the count grows by 1.217. The amount rows now move by their count row's factor times the rate ratio. After the fix the band is +3.8% at σ_p 0.2 and +3.0% at 0.5. Every SPI State Pension band: within 4% at 0.2, except £20–30k at +13.1%. Its count factor is about 1, so that is a headcount residual (8% too many recipients), not an uprating one.

On your rulings.

  • σ_p: María ruled 0.2 (3a3fc10). The root fix removes the block at both shares, and I re-ran both at 677336b with fresh spines. Both pass every gate.
    • 0.5 is better on single ages 62–63 (+8% and +2%, against +15% and +12%) and on the pensioner quintiles (a 4.8-point total gap against 6.1). Its lower reported loss (0.00716 against 0.00742) is one row, ESA claimants.
    • 0.2 is better on Pension Credit take-up after calibration (71.6% and 40.9%, against 74.7% and 45.7%), the pre-calibration benefit caseloads, the £20–30k State Pension band (+13.1% against +18.7%), BHC poverty and ESS (5,007 against 4,937).
    • The probe's budget effect agrees within 0.5%.
    • So the prior gaps don't close for free: your point 3 is where they show up after calibration. Details are in Part J.
  • 1, 3 and 5 are with María.
  • 4 is point 2 above.

Outside this PR. slc.borrowers.plan_2_* still expire on main on 2026-10-03; I've flagged it to María.

@juaristi22
juaristi22 marked this pull request as ready for review October 2, 2026 12:51
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Automated review pass (Claude Code, high effort) — round 2 at c78dff42

Verdict: nothing blocking; I'd approve once the four CI jobs still running (engine-free and engine-us, both Pythons) come back green. The round-1 items are closed or have a measured answer I agree with. Two of them carry a condition, stated below. The remaining open points are María's rulings.

R1 item Status Evidence
1 GB employer row has no country filter Closed The measurement block now declares uk.geography.country in [england, scotland, wales] (uk_population_targets.json:25648). The engine binding's household_conditions already listed the nine English regions, Wales and Scotland, so the fit was GB-only before and the declaration now matches it.
2 Employer NICs relief restated by rate only Accepted, with a condition Part J measures the counterfactual under both parameter sets: 1.087 for sacrificers paid at least £9,100 (exactly 15/13.8), 1.430 under £9,100, and 1.171 overall. For an earner above both thresholds, the relief per pound sacrificed is the rate alone, so the rate-only restatement is the right real-world quantity. The excess sits on records paid under £9,100, which #1089's c7 removes. Condition: after #1089 lands, re-measure the frame-wide factor and record that it converges towards 1.087. Until then the frame computes about 1.171 against a target restated at 1.087.
3 Pension Credit take-up not reported after calibration Closed Part H/J now reports it on the calibrated H5s. At σ_p 0.2 (fix20), Guarantee Credit is 71.6% and Savings Credit only 40.9%, against DWP's 69% and 37%. That's about 2.6 and 3.9 points high, with the entitled pool (1.81m and 0.41m) carrying the drift. It's acceptable as a recorded residual, since take-up isn't a bound row.
4 Single-record weekly amount in a rationale Closed in the tree The current text says "the nearest of them restate above £60" (target_reference_signed_exclusions.json:162), and the figure appears nowhere else in the tree. It's still in a2bfee0a, but that commit is on this PR's branch only, so a squash merge keeps it out of main's history. The PR's own commit list stays visible on GitHub. I'd treat that as de minimis rather than rewrite the branch; it's María's call.
5 Relief band and total rows double-binding after 25 November Agree with the proposal Bind the three bands and make the total a diagnostic. Please record the choice next to the exclusions in this PR, so the band-test fix can't re-bind both.
6 £50–70k band against the 25% fence Closed at the root align_hmrc_amount_by_recipient_growth_and_rate (hmrc_uprating.py) multiplies the count row's own taxpayer-growth factor by the engine's rate ratio, and writes both component receipts. That's the right decomposition: amount = recipients × amount per recipient. £50–70k now moves by 1.217 × 1.1295 = 1.375, and the band reads +3.8% at σ_p 0.2 (+3.0% at 0.5). The £20–30k band's +13.1% is a headcount residual (count factor 1.0025, 8.0% too many recipients), not an uprating one, as Part J says.

σ_p 0.2: noted, no objection. Part J lays out the trade-off fairly: 0.5 does better on single ages 62–63 and the pensioner quintiles, while 0.2 does better on take-up after calibration, the benefit caseloads, the £20–30k band and ESS.

Verification at this head: test_uk_hmrc_uprating, test_uk_spi_spine, test_uk_release_input_coverage_manifest and the shared test_contract all pass locally against this worktree's source (about 300 tests, no failures). In CI, lint, wheels, select-countries, both engine-uk jobs and integration-uk are green; the engine-free and engine-us jobs were still running.

Outside this PR, still urgent: slc.borrowers.plan_2_liable and plan_2_above_threshold still expire on main on 2026-10-03 (calibration_measure_exclusions.json:362 on main). The renewal exists only on #1089, which is stacked on this PR, and I found no PR to main carrying it. A one-line PR to main today avoids every UK build on main failing tomorrow.

@juaristi22
juaristi22 merged commit fe4e38d into main Oct 2, 2026
10 checks passed
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.

2 participants