Skip to content

Lock policyengine-uk 2.151.0 and write the UK inputs it reads (#1095, #1278) - #1383

Open
juaristi22 wants to merge 47 commits into
mainfrom
uk-pe-uk-bump-2572
Open

juaristi22 wants to merge 47 commits into
mainfrom
uk-pe-uk-bump-2572

Conversation

@juaristi22

Copy link
Copy Markdown
Collaborator

Summary

The next policyengine-uk bump (plan of 2026-10-22, microcosm#1095 and microcosm#1278). It locks policyengine-uk 2.151.0, the first release past pe-uk#2573 and pe-uk#2580, and writes the inputs those releases read. One PR of focused commits in three phases, then the rows arm 2 re-measures.

Closes #1340, closes #1320, closes #1222, closes #1349. Part of #1319: Scotland opens in the Carer's Allowance draw, and the back-test and the follow-ups listed there stay open.

Phase A, on the 2.147.2 lock (13 commits; receipts in experiments/1095-uk-next-bump-phase-a-receipts.md)

Phase B, the lock (1 commit): policyengine-uk 2.147.2 to 2.151.0, policyengine-core unchanged at 3.33.3. The uprating pins' stamp, the concept coverage (314 to 335 inputs), the coverage manifest, the H2 fixture and APPROVED_UV_LOCK_SHA256 re-derive. The ESA support-group test's legacy tenant no longer claims UC, since 2.149.0 ends income-related ESA on a UC claim (pe-uk#2563). The gambling duty inputs of pe-uk#2590 stay at their defaults (#1359).

Phase C, on 2.151.0 (12 commits; receipts in experiments/1095-uk-next-bump-phase-bc-receipts.md)

The rows arm 2 re-measures (3 commits, with the Phase B and C receipts)

Measurement

All runs are local and licensed; their outputs stay with the spines. Details are in the two receipts.

  • Control: b2572-c0main, main at 6bdb800 on policyengine-uk 2.147.2, one spine and one calibration (review date 2026-10-23).
  • Arm 2: one spine and one calibration at the bump head. All seven terminal gates pass; 1,265 rows bound against 1,259. Over the 1,258 shared rows with a nonzero target, the share within 10 percent goes from 99.21 to 99.05 percent, the mean absolute error from 0.56 to 0.55 percent, the effective sample size from about 7,400 to 7,500, and the share of design weight stretched 9.5 times or more from 0.67 to 0.59 percent. Two shared rows cross the 10 percent line outward: a row of the SPI channel and a UC band whose figures are withheld.
  • Arm 2b: calibrate-only on arm 2's spine with the four UC bands bound (the head's calibration; review date 2026-10-24). All seven gates pass; over the 1,264 shared rows, the share within 10 percent goes from 99.05 to 99.13 percent, and no shared row leaves the band.
  • Arm 2c: arm 2b with the ORR row bound on table 7271. The row reaches its target, but uk_weight_ratio fails and the effective sample size falls by about a tenth, so the row stays held on table 7270.
  • Phase A: arm 1 and arm 1b on Lock policyengine-uk 2.145.0, follow its bus fares and bind thin target rows #1333's 2.145.0 spine, in the Phase A receipts.

Disclosure

No figure in this PR, its commits or its receipts rests on fewer than ten source households or names a record. No row's model figure is given where the row is of the SPI channel or rests in large part or in part on the SPI channel's households: SN 9422's Special Conditions send analyses of the tape to HMRC ten working days before they are distributed, and that ruling is open (#1278). The surface-wide fit and weight statistics are given as in #1308. Publisher figures appear as published. The private checks behind C1, C2 and C10 stay with the licensed data, outside the repository.

Signatures for María to confirm

  • The new uprating hold on mhclg.council_tax_stock.single_adult_discount (in-year snapshot, signed 2026-10-23 to 2026-12-11), drafted as the band rows' hold is.
  • The Housing Benefit and JSA renewals, signed 2026-10-24 to 2027-01-24, and the eight UC bands' reasons restated on arm 2.
  • The two ONS energy rows' reasons and the ORR row's reason and adjudication, each with an arm 2 or arm 2c measurement added.
  • The restated signed rationale of ons.household_energy_expenditure (the frame now carries liquid and solid fuel).
  • The Phase A drafts listed in the Phase A receipts.

Not in this PR

Tests

Counted with --junitxml, on this machine.

  • tools/ci_test_plan.py verify: ok. tools/spec_engine_coverage.py --check: 42,184 of 42,184 configuration fields, 41 of 41 inventory checks.
  • The register, signed-exclusion and membership surfaces the late changes touch: 95, 619 and 715 tests, all passing.
  • The full engine-free suite at the head, in chunks: running at posting. 14 of 21 chunks have run: 15,109 tests, 165 skipped, 5 failing, each only on this machine: three Orrery parser tests (the npm package is not installed here), the US scorer test that needs Python 3.14.7, and the CGT test that expects no UK extra.
  • The UK engine tier and integration-uk passed at the Phase C head, before the arm 2 commits, which change the register, reasons and receipts only. They run again at this head.
  • Main has moved since the base (the US county work); the merge is clean.

🤖 Generated with Claude Code

juaristi22 and others added 29 commits October 24, 2026 00:35
The fifteen dwp.benefit_cap rows (the total and the fourteen amount bands
of DWP's Benefit cap tables to November 2025, Table 4) bound
benefit_cap_reduction. That variable adds the Housing Benefit cap and keeps
the gross regulation 81 amount, while DWP counts Universal Credit
households by the amount the award loses. Every row now binds
uc_benefit_cap_reduction_after_floor at benefit-unit grain, and the
measurement names the UC reduction. The total was the incumbent's 0/1
household indicator; it now counts the benefit units the bands partition,
and the complementary withholding covers the closer difference.

Regenerated on abf0921 (the national references from the feed cut to the
national and local-authority rows): the target references move on the
total's entity alone, household to benefit unit; the membership report and
the national compile-parity receipt are unchanged. The diagnostic tool
reads the bound variable, and the reviewed population variables add it.
The OBR Universal Credit rows split by cap status keep
benefit_cap_reduction.

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

María's ruling of 2026-10-22 extends the zero-support floor to the
national families. Its share ceiling sums published values, so the
register (schema 4) now holds the floor's rows in scopes of one unit, each
with its own ceilings: the UC payment-distribution and benefit-cap bands
(benefit units, as before), the HMRC SPI income-band counts, the HMRC SPI
income-band amounts, and the band-H council-tax rows by region. A test
checks that every scope holds one unit.

Measured on #1333's 2.145.0 spine, every HMRC £1m-and-over cell and every
band-H region cell has carriers, so their thirteen entries retire and the
rows bind, with their figures withheld where fewer than ten source
households carry them. Plan 5's liable stock fits at design weights and
drifts only while unbound, so its entry retires too. The OBR split of UC
spending at the welfare cap and the England postgraduate repayments leave
the targets for registry_parity.excluded: no household concept marks the
welfare-cap boundary, and the stage assigns no postgraduate plan.
obr.universal_credit still binds the sum of the two cap cells.

Regenerated on abf0921: the national references and membership lose the
three retired rows (1,322 to 1,319 active), and the compile-parity receipt
signs them as fixture-only with their reasons. The other held rows keep
their entries until arm 1 re-measures them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling D3 of 2026-10-08 held obr.vat out until a locked
policyengine-uk split the constant survey-coverage factor in vat.
policyengine-uk#2187, released in 2.135.7 and inside the 2.145.0 lock,
does that. On #1333's 2.145.0 arm the row, measured while held out,
already sits inside the 25 percent bound at both the design and the
calibrated weights, so its entry retires and the row binds. The notes
declare the residual the row keeps: OBR's receipts include VAT on
private school fees from 1 January 2025, which policyengine-uk's
current-law vat leaves out until policyengine-uk#2319.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s 95% intervals

The #731 scorecard measured period 2024 against HBAI FYE 2024, but period
2024 is FRS FY2024/25, which HBAI calls FYE 2025 (uk-data#487). The new
microcosm.build.uk_runtime.hbai_validation reads the four statistics the
pinned feed carries for FYE 2025, each with DWP's 95% confidence interval
and the release it came from: the median equivalised household income and
the share of individuals in relative low income, before and after housing
costs. It reports each measured statistic inside or outside its interval
and refuses a missing statistic or fact; it never binds a row or fails a
build (María's rulings of 2026-10-16 and 2026-10-20).

scorecard_731.py takes --hbai-facts and writes an hbai_validation block at
engine year 2024; the FYE 2024 reference figures it compared the other
HBAI-shaped rows with leave the script, and those rows stay as diagnostics.
The classifier is tested on fixture facts.

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

microcosm writes no self_employment_gross_receipts: no source of the
self-employed's turnover is read yet. Since policyengine-uk 2.108.0 the
trading allowance replaces expenses and does not stack on them, so without
receipts trading_allowance_deduction relieves a profit of £1,000 or less in
full and gives a larger profit no allowance, which puts taxable profit at
most £1,000 a person from where receipts would put it (#1095).

UK_ENGINE_DEFAULT_INPUTS, beside the take-up register in the concept
mapping module, signs the approximation with that bound, as the plan for
the next policyengine-uk bump set by default. An engine test checks each
entry is an engine input the mapping never binds, and an engine-free test
that no spine column in the H2 fixture's schema writes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling of 2026-10-17 left property_purchases, regional_home_values
and uc_legacy_closure_take_up ungated for a follow-up. Each now has a
release-blocking stage-health gate that blocks on an absent receipt:

- uk_stage_property_purchases_fit: each purchase kind is drawn once, draws
  HMRC's Stamp Tax transactions within 5 percent, and fits HMRC's value
  less a recorded shortfall of at most one fifth, the floor the stage
  itself refuses below; the individuals' shares are proper shares.
- uk_stage_regional_home_values_ranking: rows are re-ranked in every
  English region and Wales on the 2024 knots, at the weighted midpoint
  within each FRS region, with no source at the value cap.
- uk_stage_uc_legacy_closure_take_up_contract: the Move to UC receipt
  withholds what a thin combination realizes, so the gate checks what it
  prints: the design-weight Great Britain basis, a probability and a
  declared source for each rate, a printed support band, and a
  well-formed unreachable flag.

All three pass on #1333's 2.145.0 spine receipts. The gates join the spine
scope, the data contract's battery map and certification scope, and the
mirrored battery digests move. The H2 parity fixture does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tape's Special Conditions add two obligations to the End User
Licence: no attempt to attribute a record to a named individual, and
analyses of the tape go to HMRC at least ten working days before they are
distributed or published. Nothing in a build's outputs said which of them
rest on the tape.

microcosm.build.uk_runtime.spi_dependency reads which of a run's stages
declare the tape among their artifacts (the spec's ukds_study_number,
written bare or as "SN 9422"). The spine's build record carries the marker,
spine_provenance_from_sidecar carries it into the calibrated candidate and
its signed diagnostics, and both release assemblers restate it in
release_manifest.json: on the microdata's artifact entry
(spi_public_use_tape_dependent) and, with the conditions, under
licensed_data. The assemblers rederive it from the stages the signed
provenance names, through the packaged spec, and refuse a recorded marker
that disagrees or a stage the spec does not declare; a provenance from a
spine built before the marker records none, and the rederived one stands.
On the packaged roster three stages read the tape.

How SPI-dependent outputs are released stays María's ruling; the marker
records the dependency only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Arm 1 (calibrate-only on #1333's 2.145.0 spine at the Phase A head) bound
the ten HMRC £1m-and-over cells that A2 released. Bound, they moved
neighbouring rows of the SPI channel that had fitted closely out of the
10 percent band, one cell stayed outside the release fit bound, and the
arm's uk_target_fit gate failed; the other six gates passed. Under
María's rule of 2026-10-21 a measured distortion keeps rows out, so the
ten are held again as a family, re-signed to 2027-01-23 with that reason,
pending a split that binds the cells which do not distort. The register
names no figure of theirs or their neighbours': they rest on the SPI
Public Use Tape.

The band-H region cells, Plan 5's liable stock and obr.vat reached their
targets on the arm without stretching their carriers' weights, and stay
bound. The floor's adjudication and the changelog say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The twelve UC payment bands and Plan 5's repayments lapsed on 2026-11-26,
the two Plan 2 stocks too, and the any-tenure housing-element count on
2026-11-14. Arm 1 (calibrate-only on #1333's 2.145.0 spine) measured each
unbound at its calibrated weights, close to #1333's own arm:

- every one of the twelve bands stays outside 10 percent, eleven short
  and one over; they stay on their own renewals until the award-band
  take-up of #1344 binds them (María's ruling of 2026-10-21);
- the Plan 2 coverage gap stands;
- Plan 5's repayments: fewer than ten source households carry them, so
  the reason gives no figure and the row's figures are withheld;
- the housing element sits at the other/unknown-tenure gap itself.

The rows rest in large part on the households of the SPI support
channel, so the reasons give no new figure of theirs (SN 9422). Each
entry is re-signed on 2026-10-23 to 2027-01-23, and the adjudications
keep their history. The tests follow the new dates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling of 2026-10-23 extends the ten-household floor for
published figures, with its complementary check, to constituency and
local-authority rows; the area-support rule still decides which local
rows bind. uk_withheld_row_sets and uk_thin_rows now take every row, in
blocks: the national, regional and BRMA rows in one span, as before, and
the local rows one grain and area at a time, since the local surface is
too large for one dense span and binds many families per area. The
positive-weight slice of the problem is built once for all blocks.

The report guard matched a bare area code for every withheld key, which
suits a BRMA's one family but would stop a dense build on any published
row in a constituency with one thin row. It now takes the bare codes
explicitly, and uk_withheld_gate_areas gives them for withheld BRMA rows
only; a withheld local row is matched by its family/area/metric key. The
dense terminal phase passes both.

Tests: a thin row is withheld at every level; in one constituency a
family total less its published band gives a thin band back, so the next
band is withheld beside it, while a thin row in another area stands
alone; and the guard names a published row of another family in the same
constituency but refuses the withheld key and a withheld BRMA's code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
experiments/1095-uk-next-bump-phase-a-receipts.md records how Phase A was
measured without a new spine: #1333's 2.145.0 arm as the control, arm 1
(calibrate-only on its spine with every candidate row bound) and arm 1b
(the plan's contingency run, with the HMRC £1m-and-over cells held again).
Arm 1 failed uk_target_fit on one of those cells and moved neighbouring
SPI-channel rows out of the 10 percent band; arm 1b passes all seven
gates, with the shared rows' fit, effective sample size and stretched
share as on the control, and the band-H region rows, Plan 5's liable stock
and obr.vat at their targets. It also records why the rows still held are
renewed.

The receipt names no SPI-channel row and gives no count of source
households. It gives no row's model figure that rests on fewer than ten
source households or in large part on the SPI channel's households.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The integration-uk smoke build blocked at phase 'transferred' on two of
the gates a6280dba1 added. Both check the frame against published facts,
which a synthetic fixture is not: it holds a few regions, so the regional
home-values receipt rewrites three of the ten regions, and it sizes its
own purchase targets to its few households, so the non-residential draw
misses its count by more than 5% and the receipt carries none of HMRC's
individuals' shares.

Both entries now declare population_fact_check, as #1069 did for the
Pension Credit take-up gate: a smoke build records their failures in the
report and continues, and every other posture blocks on them as before.
The legacy closure gate holds only the receipt's own contract, which the
fixture meets, so it stays blocking in every posture. A test pins all
three. The mirrored battery digests move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling of 2026-10-23: weekly amounts annualise at 52 wherever a
weekly or monthly rule reads them. policyengine-uk reads an annual amount
back to a weekly or monthly one at policyengine-core's 52 weeks before
such a rule tests it, so at 365.25 / 7 every FRS weekly amount read 0.34%
high at Housing Benefit, Council Tax Reduction, Pension Credit, the
non-dependant deductions and the hours tests. #1329 made the State
Pension the first amount at 52 (her ruling of 2026-10-20); it is now the
rule, not an exception.

- frs_spine.WEEKS_IN_YEAR is 52, and STATE_PENSION_WEEKS_IN_YEAR leaves
  the spine; the readers that import the constant follow it (council tax,
  disability, education grants, legacy proxies);
- the HMRC replay leaves mirror it, and their declared formulas say * 52;
- the private-rent localisation keeps its own copy, now 52, so it imports
  nothing of the build;
- the spec's annualize_periodic_amounts operation declares 52 for every
  amount and drops the State Pension exception; its prose and the HMRC
  leaf formulas say 52; source_stages.json, the release input coverage
  manifest and the H2 parity fixture are regenerated.

LCFS spending keeps 365.25 / 7 and NTS trips 52.14, since no weekly rule
reads them back. A new test holds every reader to the one constant; the
State Pension and manifest tests follow. The 0.34% shift is measured on
arm 2, the bump's one new spine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UK lock moves from 2.147.2 to 2.151.0 (policyengine-core stays at
3.33.3), the first release past pe-uk#2573 and #2580 and the newest on
PyPI. The uprating pins' stamp, the concept coverage (314 to 335
inputs), the coverage manifest, the H2 fixture, the cited versions and
APPROVED_UV_LOCK_SHA256 re-derive; no uprating value moves.

One engine test follows the new engine: since 2.149.0 a claim for
Universal Credit ends an income-related ESA award (pe-uk#2563) and the
engine's default unit claims it, so the ESA support-group test's tenant,
on legacy ESA, now claims no Universal Credit. The gambling duty inputs
of pe-uk#2590 stay at their defaults (microcosm#1359).

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

policyengine-uk 2.148.1 (PolicyEngine/policyengine-uk#2571) projects
stored domestic energy spending from the price period a dataset declares
in its root energy_price_basis attribute, and keeps undeclared spending
at its stored prices in every later year. A Microcosm H5 declared none,
so every later year kept the 2024 prices lcfs_consumption sets
(PolicyEngine/policyengine-uk#2526, its remaining item 2).

The one physical UK writer now declares {"period_type":
"calendar_year", "period_value": 2024} wherever the household table
stores any of the three columns the engine projects from a declaration,
with the keys sorted as the engine writes its own. The period is the one
the stage-time gate uk_stage_lcfs_consumption_basis holds every LCFS
spend level to (microcosm#1113), and a test keeps the two equal. An H5
without energy declares nothing; a reload and rewrite declares it again.
An engine-tier test reads it back through policyengine-uk and checks the
columns are the engine's own.

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

policyengine-uk 2.146.0 (pe-uk#2549) closes a legacy award through its
owners: the claimant and partner, or a member's own award, which closes
on its own. The uc_legacy_closure_take_up draw still read every member's
reports, so an award a dependant claims in their own right could put the
family in the draw, or keep it out as a Universal Credit unit
(legacy_closure_take_up.py, flagged in #1095's bump item).

The reports that set a unit's combination, and its Universal Credit
status, now come from members marked is_claimant_or_partner, the FRS
adult roles the spine already writes. The graph node declares the read,
the operation names its reporters, and the spec, its schema entry and
source_stages.json follow. A new test runs a dependant's own ESA (the
family stays out of the draw) and a non-claimant's Universal Credit (the
family stays in it); the stage refuses a frame without the column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From policyengine-uk 2.149.0 a claim for Universal Credit ends a
claimant's or partner's income-related ESA. The UC take-up redraw kept
each unit that holds a legacy benefit at its earlier single-rate flag,
which sets many units reporting ESA, and no UC, to claim it, so the
engine would end their ESA at the calibration year. A unit whose
claimant or partner reports income-related ESA, and that neither reports
Universal Credit nor is moved off its legacy benefits, now claims none:
it was on the award at the interview, and a reported receipt is a fact.

The rule is declared on the operation (kept_legacy_award) and the
closure draw, which reads the same flag, sees it too; the receipt bands
the units it keeps off UC. Housing Benefit, which the engine also ends
on a claim, keeps the stored flag pending the HB and JSA PR (#867, #869).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling of 2026-10-23: Scotland opens in the Carer's Allowance
draw. The stage kept it closed because Carer Support Payment reads the
same flag and policyengine-uk did not apply its earnings limit; since
2.146.1 (PolicyEngine/policyengine-uk#2473) it does, so a drawn Scottish
carer is paid only under Carer Support Payment's own limit.

The eligible set loses its Scotland mask, and the receipt reports
Scotland apart: its eligible support band, weighted persons (withheld
under ten sources) and drawn support band, in place of the closed-region
check. The texts, the spec and source_stages.json follow. The engine
tier now checks that a drawn Scottish carer under the limit is paid
Carer Support Payment and one over it is not, and the stage-through-
engine test draws the Scottish carer over the 2024-25 limit and finds
them paid at 2025.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FRS benefit code 6 is Bereavement Support Payment with its predecessors,
and the spine summed it all into bsp_reported. policyengine-uk 2.149.0
(pe-uk#2572) gives Widowed Parent's Allowance its own input, because it is
taxed and means-tested where the payment is not. María's ruling of
2026-10-22: split code 6 by rate, after a private check of the amounts.

A weekly amount within 1% of a Bereavement Support Payment rate's weekly
equivalent (GBP 350 or GBP 100 a month, frozen since 6 April 2017; at
12/52 for a month, quarter or year and at 1/4 for four weeks) stays
bsp_reported; any other goes to widowed_parents_allowance_reported. The
new column goes wherever War Widow's Pension goes: the spine's outputs,
the SPI support and spine lists, the SPI benefit-coherence restore and
its gate, the graph, the spec and the export allow-list with its
gates.json mirror. The mirrored digests move, and source_stages.json and
the coverage manifest are regenerated.

A parametrised test runs each rate's two weekly equivalents, the edges
of the 1% band either side, and amounts off every rate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-uk 2.149.0 (pe-uk#2572) adds spousal_maintenance_income,
which Universal Credit (UC Regulations 2013 reg. 66(1)(d)), Pension
Credit and Housing Benefit count, unlike the child maintenance in
maintenance_income. The spine read no alimony at all, and kept APAMT,
the allowances from a husband or wife who lives away, in
private_transfer_income.

María's rulings: ALIAMT, or ALUAMT where ALIUS 2 says the last amount
was not the usual one (2026-10-22), plus APAMT, which the FRS asks only
of married adults whose spouse is away or separated (2026-10-23, after a
private check of the recipients). APDAMT, the household bills such a
partner pays directly to third parties, stays a private transfer.

The new column goes to the spine's outputs, its non-negative outputs and
the export allow-list with its gates.json mirror; an adult tab without
the three alimony columns refuses, as one without SPPADJ does. Tests read
each ALIUS answer, refuse each missing column, and keep every FRS field in
one income column. The H2 fixture's adults carry the three columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-uk 2.149.0 adds pension_contributions_via_bonus_sacrifice,
which the GBP 2,000 NICs exemption cap from April 2029 and the annual
allowance tests read summed with salary sacrifice (pe-uk#2572 section
1). Microcosm wrote salary sacrifice only, so a costing of the cap left out
the bonus route the OBR costs (#1340). María's ruling of 2026-10-22:
impute it; the OBR's base starts in 2027-28, after the calibration year,
so it anchors the stage instead of binding the calibration.

- frs_spine writes frs_bonus_amount, each adult's gross occasional
  bonuses over the last 12 months from the JOB tab (BONAMT1-6, a bonus
  given after tax with its imputed weekly tax over the year); a tab
  without the fields refuses, and the carrier leaves at the release
  boundary.
- salary_sacrifice gains a third operation after the salary conversion.
  Candidates are adults with a bonus whose pay without it reaches the
  upper earnings limit, the 2% band the OBR costs bonus sacrifice at.
  Each converts the whole bonus, within half the pay and the room left
  above the other routes, out of pay, in the order of identity-keyed
  draws, until the engine's projection of the converted bonuses to
  2027-28 (each year's growth compounded, the threshold on the two
  routes summed) is nearest the OBR's base. The receipt bands its
  support on source households and withholds the base under ten.
- The OBR's Table 1.1 tax base is vendored from the pinned feed
  (obr_salary_sacrifice_costing_facts.json, a streamed copy of
  tools/vendor_uk_ledger_facts.py's output), and the stage verifies it
  first. tools/pin_uk_bonus_sacrifice_anchor.py writes
  uk/bonus_sacrifice_anchor.json from it and the locked engine's
  earnings and population growth and upper earnings limit; an
  engine-tier test holds the anchor to the installed engine and the
  bonus route's excess to the engine's own cap arithmetic.
- The spec, its schema and operation registry, the declared seeds, the
  graph's output cells, the export allow-list and its mirror (digests
  re-pinned) and the H2 fixture's jobs follow. The HMRC salary-sacrifice
  rows, which ASHE builds, keep binding the salary route alone, and a
  test says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-uk 2.149.0 (pe-uk#2572) adds liquid_fuel_consumption and
solid_fuel_consumption, parts of housing that add nothing to
consumption. The LCFS stage left the diary's heating oil, bottled gas
and solid fuels inside the housing remainder with no column. María's
ruling of 2026-10-22: LPG with liquid, heat in the remainder, and the
ONS 04.5 row measured on electricity, gas, liquid and solid fuel.

- The donor split writes each fuel from its labelled parts (b017, c45312,
  b018, c45222; c45411, c45412); both are annualised, uprated to
  calendar-2024 prices on the CPI fiscal bridge as every diary spend
  column is (the consumption-basis gate reads them), and drawn last in the
  chain so every earlier draw is the chain's own.
- The housing recomposition nets their draws and adds them back, so the
  total always holds them; a household whose drawn parts outgrow its
  drawn total keeps at least its fuels.
- The recomposed-totals receipt's uncarried block becomes published_fuels:
  ONS 04.5.3 and 04.5.4 beside each fuel's donor total at diary prices and
  the frame's draw at stage weights. The stage-time gate recomputes the
  published rows; the spec, its schema, the graph's cells and the stage's
  outputs follow.
- ons.household_energy_expenditure, which the contract keeps for
  registry parity and the reference surface signs out as superseded by
  its fuel rows, reads the four fuels through a value expression; its
  signed rationale no longer says the frame lacks liquid and solid fuel.
- The consumption drift block reads the two fuels where the frame
  carries them, so an older spine keeps its block, and
  uk_consumption_drift_flags gives warn-only flags at #1278's R1
  thresholds (2 percent for a total, half a point for a share), outside
  the diagnostics payload so its v8 schema stays as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each COICOP division whose frame total per household stands above its
donor's by more than the twelve divisions together do comes down to that
shared ratio before the hold to the donor maximum; no other division moves.
A recomposed total moves by its remainder alone, so the levelled parts keep
their published levels. Declared as level_division_excess on the recompose
operation; the receipt records each division's ratio and factor, and the
stage-time gate checks them.

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

The WAS property draw of María's ruling of 2026-10-23. Three draws are
appended after consumer debt, each from the holders of the column it rests
on, so the first eight segments draw as before:

- the directly held fraction of the share-like holdings outside ISAs, from
  which directly_held_shares (WAS UK shares plus employee shares and
  options) and unit_and_investment_trusts (WAS collective investments) are
  written on the final corporate_wealth less the ISA, so the three sum to
  corporate_wealth and the engine's share_like_capital does not move
  (pe-uk#1974);
- the debt secured on other residential property and on non-residential
  property, zero where the asset is zero and scaled down together where
  they would pass the mortgages on other property.

The recorded UC and Pension Credit capitals and the reporter redraw's
screen net each class's debt after the costs of sale, floored at zero per
class (UC Regs 2013 reg 49(1)(b); SPC Regs 2002 reg 19(b)), as the engine
values UC property capital. The donor cleaning refuses debts that pass
the other mortgages; the wealth coherence gate holds the split and the
debts as identities; the four columns are reviewed export extras.

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

policyengine-uk applies the single adult household 25% discount from
2.148.0 (pe-uk#1864, council_tax_discount_rate). The new contract row
mhclg.council_tax_stock.single_adult_discount counts England households
at that rate against MHCLG Council Taxbase line 8, all bands, the England
row of the latest October return not after the build base period. The
billing-authority cells bind in the next dense build; no incumbent row
has this definition, so it is an unmapped declaration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-uk projects the 2024 dataset to the 2025 calibration year
and relabels every holder by a course started at 18; the stage dates a
course from the age its borrower left full-time education (#868, #1299).
The engine test pins where the two disagree: a course dated after 2012
by its leaving age but before by the age rule becomes Plan 1, and a
1993 course is written off, so the calibration measures the engine's
plans at 2025. An engine that keeps the stage's dating fails it, and the
SLC rows are then re-measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Arm 2 of the next bump (one spine and one calibration on
policyengine-uk 2.151.0) re-measured the rows still held, unbound at its
calibrated weights. Since #1344 draws UC take-up by award band, four of
the twelve UC payment-distribution bands come within 10 percent, so their
entries retire and the rows bind (the plan's rule: bind a held row that
fits); the eight that stay keep their 2026-10-23 renewals, their reasons
restated on arm 2.

The four Housing Benefit rows and DWP's JSA claimants stay outside the 25
percent bound at design weights. Their 2026-12-08 entries are renewed,
signed on 2026-10-24 to 2027-01-24, pending the Housing Benefit and JSA
PR (#867, #869).

The two ONS energy rows, held out since #1113, keep their entries; their
reasons add arm 2's consumption drift block, on which electricity and
gas spend and the gas-connected share stay within the warn-only flags
between the design and the final weights.

The UC bands rest in large part, and the Housing Benefit and energy rows
in part, on households of the SPI support channel, so their reasons give
no new figure (SN 9422). The JSA row's claimants at arm 2 are all in FRS
households, and its reason gives its figures. The register tests follow
the counts and the dates.

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

The plan of 2026-10-22 set a default on the open 7270-versus-7271 ruling
(microcosm#890 G): bind orr.rail_government_support on ORR table 7271's
net operational government support from all sources, unless an arm
measures a distortion. Arm 2c, a calibrate-only run on arm 2's spine
with the row bound on table 7271 beside the four UC bands, did: the row
reached its target, but the weights concentrated past the release bound,
uk_weight_ratio failed, and the effective sample size fell by about a
tenth.

So the row stays held out on table 7270's basis, its entry keeps the
2026-12-11 window, and its reason and adjudication record the
measurement. The basis ruling stays open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
experiments/1095-uk-next-bump-phase-bc-receipts.md records how the
2.151.0 lock and Phase C were measured: the control on main, arm 2 (one
spine and one calibration at the bump head), arm 2b (calibrate-only on
arm 2's spine with the four UC payment bands bound) and arm 2c (with the
ORR row bound on table 7271 as well). Arm 2 and arm 2b pass all seven
gates; arm 2c fails uk_weight_ratio, so the ORR row stays held. It also
records each Phase C item's check and why the rows still held are
renewed.

The receipt names no SPI-channel row and gives no count of source
households. It gives no row's model figure that rests on fewer than ten
source households or in large part or in part on the SPI channel's
households.

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

Copy link
Copy Markdown
Contributor

@juaristi22 thanks, this is my review at 74a2a378e.

Verdict. The bump is careful work and CI is green at the head. The lock, the new inputs and the energy_price_basis declaration do what the body says, and I could reproduce the declaration's effect through policyengine-uk. One must: the local withholding cannot see a difference across grains, so a region row less its published local rows can give a withheld local row back. The main shoulds are these. The body does not say what the engine alone moves at the bump (income-related ESA roughly halves on the current release). Two method choices in the bonus sacrifice stage need a ruling. And obr.vat binds a target the engine's VAT factor was itself fitted to.

What I ran

  • CI at 74a2a378e: all ten jobs pass. tools/ci_test_plan.py verify: ok. Main has moved past the base (the US county work and Carry industry and worked-last-year on every US person record (ASEC restore + ACS crosswalk) #1039), and the head still merges with main cleanly (git merge-tree).
  • Engine-free and engine tests for the touched areas, at the head: 293 for the national frame, diagnostics, SPI marker, both release assemblers, LCFS, WAS and the energy declaration; about 580 for the support floor, HBAI, stage health, measurement and terminal gates; plus the FRS spine, take-up, council tax, student loan, ESA, Carer's Allowance and bonus sacrifice files. All pass. Most new tests test new symbols, so they fail on the base by construction; I did not run them there.
  • An engine-only A/B on the current release, a133f37a (sha256 64f83900… checked against its manifest): the same file under policyengine-uk 2.147.2 and 2.151.0, and under 2.151.0 with the declaration added to a copy. Aggregates only.
  • Primary sources: OBR's costing of NICs on salary-sacrificed pension contributions (February 2026; Table 1.1 matches all eight vendored rows, sha256 matches); MHCLG Council Taxbase 2025; the BSP rates in SI 2017/410; Carer Support Payment's earnings limit in SSI 2023/302.
  • Generated parts, spot-checked only: the H2 fixture's person.csv changes are the three new columns, with no existing amount changed; the coverage golden's 21 new inputs; the manifest's new outputs.
  • Not run: no spine, calibration or release build, and no FRS, LCFS or WAS data. Not read line by line: source_stages.json, gates.json, the membership and signed-difference JSON, the fixture .tab and .csv files, the three new stage-health gate bodies, diagnose_uk_benefit_cap.py, the UC take-up residual solve and pin_uk_bonus_sacrifice_anchor.py in full. The FRS and LCFS codebook wording (ALIAMT, code 6) is licensed, so I could not check it.

Findings

Must

  1. Withholding misses differences across grains (uk_runtime/diagnostics.py:1081-1106, _row_blocks). National, regional and BRMA rows form one block, and each local grain and area forms its own. The docstring says a difference "across areas or against a national row is not" checked. But local families have regional controls: the band-H region rows bind beside the local band-H cells, and local_small_cell_rule.json counts deferred cells "in its region's or nation's control". A published region row less the published local rows in that region gives back the one local row withheld, and band H is where thin cells sit. Before any local release's diagnostics leave the licensed environment, please either check each local family together with its parent-region rows of the same concept, or show that no published regional or national row is the sum of a local family's rows.

Should

  1. The body does not say what the engine alone moves. On a133f37a, swapping 2.147.2 for 2.151.0 changes the following:

    • income-related ESA falls from £3.85bn to £1.88bn in 2025, under 2.148.4's closure on a UC claim (your receipt notes that no SPI-channel household carries this row). Commit 5cf7e38 is the fix, and the receipt shows the DWP claimant row fits. Please say in Phase B that this is the engine effect that commit offsets;
    • LTT and LBTT rise by the 1/0.045 factor of 2.150.0, into gov_tax, household_tax and net income;
    • council tax falls under 2.148.5's projection;
    • the targeted two-year-old entitlement falls by about a quarter (2.148.1 reads UC earnings before the work allowance);
    • from the changelog, outside my two years: September CPI uprating (2.148.2) and the Pension Credit floor (2.151.1) move benefits from April 2027;
    • statutory paternity pay is now taxed;
    • household net income falls by about 0.15%, and the poverty rates move by at most about 0.2 points. I give no rate here, under your SN 9422 rule.

    Please add a short "what the lock moves" list. Give the figures where SN 9422 allows (ESA rests on FRS households), and say which bound rows absorb the rest. Costings and dashboards built on a133f37a need it before they move.

  2. energy_price_basis: declared correctly. Say what it changes, and test it end to end (national_frame.py:209-225, called at :502). It declares {"period_type": "calendar_year", "period_value": 2024} for exactly the three columns in pe-uk's DATASET_UPRATING_EXCLUSIONS, and the engine reads it. On a copy of a133f37a with the attribute added, domestic energy spending rises 3.4% in 2025 and 5.8% in 2026. That is every household alike, the CPI ratio, where undeclared files stay flat. Nothing else in my list moved. This closes pe-uk#2526's item 2 for builds from this head, though earlier releases stay undeclared. Please:

    • put the effect in the changelog line;
    • add one engine test that loads a written H5 through Microsimulation and checks a projected year. The current test only reads the attribute back.

    2.152.2 (pe-uk#2591) later narrows the projection and refuses one before the index's history. It is not in 2.151.0; harmless here, as far as I can see. 2.151.0 reads only period_value, so calendar versus fiscal year makes no difference yet.

  3. One new input is left at its default without saying so. 2.151.0 adds 21 inputs. Five are written (WPA, spousal maintenance, bonus sacrifice, liquid fuel and solid fuel), and energy_price_basis is written as an attribute. The 15 gambling inputs stay at default, as stated (UK: impute alcohol, tobacco and gambling consumption so policyengine-uk's excise duties stop scoring zero #1359, Write policyengine-uk's alcohol, tobacco and gambling duty inputs from the LCFS stage #1381). housing_benefit_residential_capital_exception (benefit unit, pe-uk 2.148.4) also stays at False, and neither the body nor any register says so. pe-uk documents that this "may understate Housing Benefit for qualifying residents". Please state it beside the gambling inputs.

  4. Bonus sacrifice takes the bonus out of reported pay (salary_sacrifice.py:597). BONAMT1-6 are bonuses the respondent received. A real bonus sacrificer did not receive the bonus, so their FRS pay already excludes it. Moving about £11bn of 2024 pay from bonus receivers into sacrifice lowers the income tax and NICs base, and calibration then re-inflates it through the weights. The salary route is different: there gross pay does include the converted contribution. Please rule between this and adding the contribution without lowering pay, and state the assumption. The "UGRSPAY adds occasional bonuses" comment needs a source.

  5. SPI copies keep their FRS twin's bonus (spi_spine.py:214). frs_bonus_amount is not in UK_SPI_INCOME_SPINE_REWRITE_COLUMNS, and the candidates (pay above the UEL) sit largely in the SPI channel. So the candidate test mixes tape pay with a donor's bonus: the uk-data#629 pattern. Please zero or redraw it on SPI rows, or restrict candidates to FRS rows.

  6. The bonus anchor is met at stage weights only. The prefix solve meets OBR's £13.0bn (2027-28, above £2,000) at stage-time weights times population growth. After calibration nothing checks it. The vendored salary line (£12.9bn) also goes unused, though the bonus excess is computed jointly with it. A warn-only post-calibration check of both lines would do, recorded privately if needed. OBR grows the base with employment and wages, the anchor with population; say so.

  7. LPG is in liquid fuels, but ONS puts it under gas (lcfs_consumption.py:205-217, b018 and c45222). COICOP 04.5.2 "Gas" includes liquefied hydrocarbons, and c45222 is itself a 04.5.2 code. ONS 04.5.3 (£1.195bn) leaves LPG out. So the receipt's "about 2.1 times" is partly a concept gap. Please caveat the ratio or report bottled gas apart. pe-uk's liquid_fuel_consumption docstring has the same mislabel, so that needs a small pe-uk fix. Separately, both fuels sit at about twice ONS, unlevelled where electricity and gas are levelled. That is fine while no formula reads them, but the column notes should warn that a costing on them would roughly double.

  8. The division levelling counts levelled-up parts as over-draw (lcfs_consumption.py:2098, division_excess_factors). Transport's ratio includes petrol, diesel and bus, which are already levelled up to published totals, so the cut lands on its remainder (vehicle purchases and other transport). Comparing each remainder with the donor's remainder would isolate the over-draw. It is also a single pass. The receipt gives no change in total consumption or in the VAT base, and obr.vat rebinds in the same PR.

  9. obr.vat is circular. pe-uk's gov.simulation.vat.survey_consumption_coverage (0.67) was "calibrated to the certified Microcosm UK 2024-25 national release only" (5c6b3f68) so that its VAT meets OBR's £170.994bn. Binding obr.vat mostly pulls consumption weights back toward that release, after this PR changes the consumption base (items 8 and 9). It also binds over private school VAT, which is in OBR's receipts but not in current-law vat. Please keep it as a diagnostic until pe-uk re-derives the factor on a file that does not bind it, or state the circularity in its notes and the receipt.

  10. uk_consumption_drift_flags is never called (diagnostics.py:780). It is new in this PR, but only test_uk_diagnostics.py calls it. The receipt's "the solid fuels move by more than the 2 percent the consumption drift block flags" must come from a hand run. Please wire it into the diagnostics, or call it a manual tool.

  11. WPA takes every code-6 amount that matches no BSP rate (frs_spine.py:1048-1120, bsp_rate_match). The BSP rates (£350 and £100 a month, frozen by SI 2017/410) and the 1% band are right. But a BSP lump sum, a part month or a misreport becomes WPA, which is taxable and means-tested. Please add a positive WPA rule (the published 2024-25 rate, with a reduced award below it), or give the private count of off-rate amounts. Since this PR splits BSP out, pe-uk can now stop CPI-uprating bsp_reported (its uprating_indices.yaml says so); worth an issue there.

  12. Spousal maintenance may include former cohabitants (frs_spine.py:862-890). pe-uk's spousal_maintenance_income excludes maintenance from a partner who was never a spouse or civil partner, since UC and Pension Credit don't count it. ALIAMT reads as alimony or an allowance from a previous partner. Please confirm what the FRS 2024-25 variable covers, or state the approximation. The rest is right: APAMT leaves private_transfer_income, child maintenance stays in maintenance_income, and nothing is taxed.

  13. Netting secured debt raises UC and Pension Credit awards, unmeasured (uc_capital_coherence.py). The netting is right in law (UC Regs 2013 reg 49(1)(b); SPC Regs 2002 reg 19(b)), and it matches pe-uk's max(0, 0.9 v - debt). But C10 gives identities only. Please give the change in UC and Pension Credit spend, or the units crossing £6,000 and £16,000, privately if needed.

  14. The 52-week rule also lowers annual incomes by 0.34% before calibration (b69248f, frs_spine.py:82). The ruling is defensible, and the constant is held equal across readers. But pay, self-employment and the HMRC replay leaves all fall before the HMRC and SPI rows rebind, and arm 2 bundles that with everything else. One line in the receipt on which bound rows absorb it would do. frs_annual_income.py:9 still says "365.25 / 7".

  15. The student-loan test pins a defect with no pe-uk issue (tests/engine/uk/test_uk_student_loans.py). At 2025 the engine relabels every holder by a course started at 18, so the SLC rows calibrate on the engine's plans. Please link a pe-uk issue and say when the SLC rows are re-measured.

  16. The benefit-cap total is exactly the sum of its bands (682772f). The fourteen monthly bands partition the units the total counts, so the total adds nothing and gives a withheld band back by difference. The commit says complementary withholding covers that, which I did not check on a run. Housing Benefit-capped households now have no row, which is right for DWP's UC table but is not stated.

Nit

  1. The single adult discount row's 8,496,178 matches MHCLG. That count is as at 10 September 2025, not October, and the release was revised on 21 January 2026; please confirm which you vendored.
  2. Schema 3 exclusion registers are now refused, not read (measure_simulation.py:1715). No current build changes, but an old run directory can no longer be replayed; say so in the changelog.
  3. No test pins the fuel parts (b017 + c45312 + b018 + c45222, and c45411 + c45412, with c45511 left in the remainder).
  4. The restated ons.household_energy_expenditure rationale keeps approved_on: 2026-09-16; it should move when you sign it.
  5. Scotland's carers take the GB newly entitled take-up rate; one sentence in the reason would do.
  6. The bonus room rule is pay - max(salary, employee), while the docstring describes a sum. And "a person's draw moves only with their own inputs" overstates it, since the prefix cutoff depends on everyone's contributions.
  7. The support floor's per-scope unit label is never checked against its rows (band H says "dwellings" and is measured on households).

What looks right

  • The lock is consistent: uv.lock, the three pyproject.toml floors, the coverage golden (314 to 335), the manifest and APPROVED_UV_LOCK_SHA256 move together. The ESA support-group test change is the right response to 2.149.0.
  • The declaration is written only where an energy column exists, keys sorted as pe-uk writes them. Both national and local writers go through _write_uk_single_year_tables, and the HDF write exclusion is registered.
  • The OBR bonus and salary rows match Table 1.1. The anchor is not also a calibration target, and the carrier is dropped at the release boundary.
  • The draws stay keyed on identity: WAS's SeedSequence.spawn is prefix-stable with earlier draws byte-equal, and the UC take-up and legacy closure draws are identity-keyed.
  • HBAI is report-only: no HBAI row binds, engine year 2024 maps to FYE 2025, and bad facts refuse.
  • The synthetic-smoke relaxation cannot reach a release: gate_battery.py:907 refuses synthetic smoke with release_candidate.
  • The SPI Public Use Tape marker in both release manifests is a real improvement, and the receipts keep to your SN 9422 rule.
  • The legacy closure draw reads claimant and partner reports only (pe-uk#2549). Opening Scotland's Carer's Allowance draw matches 2.151.0's Carer Support Payment earnings limit.

Merge order

🤖 Generated with Claude Code

@juaristi22
juaristi22 marked this pull request as ready for review October 24, 2026 10:44
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Fixed the cross-geography subtraction gap in 3ce1fca6c7b28f7cdb5c723ce6384ebd2d984e94.

The publication layer now withholds all local-authority, constituency and BRMA/statistical-scope model figures until joint publication safety across geographies is established. This also applies to separate BRMA-only files and stale caller-provided suppression sets. Administrative target facts stay available. Public gate details cannot disclose hidden local values through numerical errors or named failure messages.

Private calibration values and fit-gate decisions are preserved: a failed local fit still blocks release. This is a conservative restriction on published diagnostics; it does not implement a general joint solver or retroactively repair old artifacts.

Independent review found no new defects in this repair. The exact snapshot passed 364 focused tests, including synthetic parent-minus-child reconstruction cases and private failing-fit enforcement. Ruff and test-layout verification also passed. No licensed records were used.

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

@juaristi22 thanks, this is my second review, at 3ce1fca6c.

Verdict. The must is fixed for disclosure. Every constituency, local-authority and BRMA model figure is now withheld from published diagnostics, so a region row less its published local rows has nothing left to give back. But the redaction also reaches the full gate report that the local sign-off tool reads, and that tool now refuses every local build, a passing one included. It also drops the signed local exclusions from the release manifest without saying so. Nothing leaks either way, but the local release line cannot be signed off at this head. CI is also red: one engine-free test still patches a name the commit removed. The 16 shoulds and 7 nits from round 1 are unchanged: the delta touches only the withholding code, and the PR body has not been edited since.

What I ran

  • CI at 3ce1fca6c: lint, wheels, select-countries, integration-uk and both engine-uk jobs pass. engine-free (3.14) fails with a setup error (item 29), which I reproduced locally. engine-free (3.13) and engine-us were still running when I posted. tools/ci_test_plan.py verify: ok. The head still merges cleanly with main (git merge-tree).
  • The three touched test files (test_uk_diagnostics.py, test_uk_full_gates.py, test_uk_graph_terminal.py, engine and engine-free) with --extra uk: all pass. test_uk_dense_signoff_draft.py: passes as committed.
  • A probe: I copied test_uk_dense_signoff_draft.py and ran each run directory's gate report through uk_published_fit_gate_report before writing it, which is what UKFullGateKernel now does. 29 of its tests then fail (item 25). This was the test harness's synthetic run directory, not a real build artifact.
  • Not run: no build of any kind, and no data. This round has no £ figures, so your SN 9422 rule is not engaged.

Status of round 1

  1. Must, withholding across grains: resolved for disclosure. uk_withheld_row_sets adds every constituency, local-authority and BRMA row to the complementary set (diagnostics.py:1278-1282), and the caller-supplied path in uk_calibration_diagnostics_payload adds them too (:2499), so a stale set cannot reopen them. Totals and pass rates count those rows as withheld, the local fit gate details are redacted, and the new test test_a_parent_cannot_give_a_thin_local_cell_back_by_difference covers the region-less-children case. Fixing it this way brings the issues in items 25 to 29.

2 to 17 (shoulds) and 18 to 24 (nits): open, no change at 3ce1fca6c. The one new commit touches diagnostics.py, full_gates.py, graph_terminal.py, their tests, the README and a changelog fragment, none of which those items cover. The body still has no "what the lock moves" list (item 2), no energy_price_basis effect or end-to-end test (3), and no note on housing_benefit_residential_capital_exception (4). The bonus sacrifice rulings (5 to 7), fuels and levelling (8, 9), the obr.vat circularity (10), the uncalled drift flags (11), WPA (12), spousal maintenance (13), secured debt (14), the 52-week rule (15), the student-loan pin (16) and the benefit-cap total (17) are all as they were.

New findings in the delta

Should

  1. The local sign-off tool refuses every local build at this head (graph_terminal.py:1028-1059, full_gates.py:343-373, tools/draft_uk_dense_signoff.py:755-790). UKFullGateKernel writes the redacted report into the gate_report artifact. That artifact is uk.full.gates.calibrated.gate_report.json, the full report load_build reads. _check_fit_details needs targets_checked, withheld_targets, failing_targets and reviewed_exclusions in uk_local_target_fit's details. Redacted, targets_checked is missing, so the tool stops with "uk_local_target_fit checked a different number of rows than targets.csv holds". In my probe that held for 29 of the file's run-directory tests, including test_a_build_that_passed_needs_nothing. Your 364 tests pass because write_run in the sign-off test writes the report in its old, unredacted form, not what the kernel writes now. There is a second break behind it: even unredacted, the local fit gate counts fit_withheld, while targets.csv now withholds every local row, so the withheld-count check would fail too. The tool's own docstring says the full report "does not ship". So I think the fix is to keep that artifact private and unredacted, and to redact only what ships (*.local_gates.json, the diagnostics, the targets CSV). Then make the sign-off test build its run directory through the kernel's path, so a change like this fails CI. I have tagged this should because it fails closed. But it blocks the next local release candidate, so please treat it as release-blocking for the local line.

  2. The signed local exclusions drop out of the release manifest silently (tools/assemble_uk_dense_release_dir.py:304-310, contract.py:8354-8390). _signed_exclusion_names reads details.reviewed_exclusions. Redacted, it skips the gate, so no reviewed_exclusion:uk_local_target_fit:* limitation is written. _check_uk_dense_signed_exclusions then compares an empty list with an empty set and passes. Its job ("the manifest names every reviewed exclusion the gates stood on") is lost without a failure. The committed register holds 31 local cells. Cell names (family/area_code/metric) carry no figures and the register is public, so the names can stay in the published details while the errors are redacted. The fix in 25 would also cover this.

  3. The size evaluation's red ledger goes empty (size_evaluation.py:735-756, _red_target_names). It reads failing_targets and reviewed_exclusions from *.local_gates.json, so after this commit it finds no red local targets and scores every local candidate as clean. It should read the private report, or refuse a redacted one.

  4. BRMA rows are now withheld everywhere, which goes further than the must (_UNCERTIFIED_LOCAL_PUBLICATION_LEVELS, diagnostics.py:1036). BRMA rows already sat in the national and regional block, where the joint difference check runs. With constituency and local-authority figures withheld, a BRMA row has no published local neighbours to be differenced against. So withholding every BRMA figure, in the national payload too, is a choice rather than something the must needs, and it blanks the Scottish BRMA rent figures the national payload published before. If that is your ruling, please say so in the body and the changelog. If not, keep BRMA rows under the block check.

  5. engine-free is red (tests/engine_free/uk/test_uk_full_calibration_graph.py:68). The fixture still does monkeypatch.setattr(full_gates, "uk_withheld_rows", none_withheld), but full_gates now imports uk_fit_gate_withheld_rows and uk_uncertified_local_rows instead, so the autouse fixture _every_toy_row_published raises AttributeError. Locally all 15 tests in the file error at setup; CI's shard reports one of them. Patch the new names. The test means "nothing withheld", so please check whether it needs uk_uncertified_local_rows patched as well.

Nit

  1. target_pass_rates_by_geography_level now publishes pass_rate: 0.0 for constituency and local authority (the new test asserts it). That fits the schema, but it reads as "no local row within 10%". None, or a flag saying the outcomes are withheld, would be truer.
  2. The guard in uk_refuse_named_withheld_rows now matches every local row name and gate key, and every BRMA code, as substrings. I found no local gate other than the two redacted fit gates that names a row or cell key, and BRMA codes are distinct S33 codes, but I did not check this on a build.

What looks right

  • Withholding is monotone: withholding more rows can never make a difference attack easier. So putting every local row in the complementary set cannot break the national block's existing checks.
  • Private fit decisions are kept. fit_withheld still decides the local fit gates, enforcement reads the in-memory report, and test_local_publication_suppression_preserves_a_failing_private_fit_gate shows a failing local fit still blocks.
  • A stale caller-supplied set is widened, not trusted, and every withheld local row is labelled publication_safety: cross_geography_not_certified.
  • Administrative target values stay visible, which is right, since they are published statistics.
  • The README and changelog describe the restriction plainly, and say it is conservative rather than a joint solver.

Merge order

Unchanged from round 1: this PR, then #1381, then my #1373. If you take the private/public split in item 25, #1381's shared files (the diagnostics and the gate kernel) will need a rebase.

🤖 Generated with Claude Code

@juaristi22

Copy link
Copy Markdown
Collaborator Author

Pushed d48652be to repair the synthetic graph fixture and align Orrery assertions with mandatory local withholding. The fixture bypasses only carrier-span selection; local publication suppression and private fit-gate decisions remain active.

Validation: 395 targeted tests pass, and independent verification passes 143 tests, both on Python 3.13. Ruff formatting and lint pass. Hosted Python 3.14 validation is pending.

This test-only follow-up preserves the reviewed PR history and leaves its main base unchanged. At the pre-push check, main had 79 commits absent from this branch; integrating them requires a separate review.

juaristi22 and others added 16 commits October 24, 2026 14:06
…nors

Vahid's review of #1383, items 5 and 6, and María's rulings of
2026-10-24.

A bonus sacrificed is never received, so the FRS records it in neither
pay nor its bonus amounts (the FRS 2024-25 derived-variable notes, SN
9563, add received bonuses to UGRSPAY). The stage took candidates'
received bonuses out of pay, which lowered taxable pay before the HMRC
and SPI rows rebound it. Now a candidate is an adult with a bonus whose
pay reaches the upper earnings limit, and its sacrifice, the bonus up to
half its pay, is added beside pay, which stays as it is; the room rule
against the other routes and the pre-conversion pay for bonus-only rows
go. The received bonus marks a bonus-paying job and its size.

An SPI-channel adult carries its FRS twin's bonus beside the tape's pay.
It now draws a bonus share of pay from a base-FRS adult with pay in the
same pay cell (weighted pay quantiles, a cell with fewer than 20 donor
source households joining the one below), by person weight, keyed on
person_id; FRS rows keep their own bonus. The receipt bands the drawn
adults on source households.

The spec, its schema and source_stages.json declare the new parameters;
the coverage manifest and the H2 fixture follow. Tests: the candidate
rule, the half-pay bound with no room rule, the stage leaving pay whole,
and the SPI draw (share of own pay, the donor floor, stability under a
row shuffle, the refusal without donors).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or's same spend

Vahid's review of #1383, item 9, and María's ruling of 2026-10-24.
Transport's ratio counted petrol, diesel and bus, which the stage levels
up to published totals, so its cut landed on its remainder for an
over-draw that was not the chain's. A division's ratio is now the spend
the chain drew for it per frame household over the donor's mean of the
same spend: for housing and transport, the remainder against the donor's
total less the same parts (lcfs_donor_drawn_means); for any other
division, its whole draw. A division above the divisions' shared ratio,
measured once before any levelling, comes down to it.

One pass, by her ruling: measured again after each cut, the shared ratio
falls until every division sits at the lowest one's, which is close to
levelling to the donor totals she ruled out on 2026-10-23.

The declaration (rule excess_over_shared_ratio, basis
drawn_spend_over_donor_spend_per_household), its schema, the stage's
notes, source_stages.json, the coverage manifest and the H2 fixture
follow; the stage-time gate's checks hold as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of #1383, item 8 and nit 20. COICOP puts liquefied
hydrocarbons under 04.5.2 (gas), and c45222 is a 04.5.2 code, so ONS's
04.5.3 leaves out the bottled gas the liquid column holds by María's
ruling of 2026-10-22. The published-fuels receipt now gives the donor's
liquid fuels without it too, with the concept note, so the ratio to
ONS reads the diary's capture and not the concept gap. The column's
comment, the stage's notes and the changelog say that both fuels stay
at the diary's level, about twice ONS's, unlevelled, so a costing on
them would come out near twice ONS's spend.

A test pins each fuel's diary parts, bottled gas inside the liquid
fuels, and c45511 (hot water, steam and ice) left in the remainder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of #1383, item 12. Every code-6 amount at no Bereavement
Support Payment rate went to Widowed Parent's Allowance, so a lump sum, a
part month or a misreport became a taxable, means-tested allowance. An
amount at the allowance's 2024-25 rate (GBP 148.40 a week, DWP's Benefit
and pension rates 2024 to 2025; within 1%), or below it as a reduced
award, is now the allowance; any other amount, above the allowance's
rate and at no rate, stays bsp_reported. An allowance with an inherited
additional pension above its rate reads as the payment, and a lump sum
spread over a year below the rate reads as a reduced allowance; the
rule cannot tell those apart.

The spine stage's notes, source_stages.json, the coverage manifest, the
H2 fixture and the changelog follow; the test runs both sides of the
allowance's rate and a lump sum above it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d to it

Vahid's review of #1383, item 10, and María's ruling of 2026-10-24.
policyengine-uk's gov.simulation.vat.survey_consumption_coverage, which
policyengine-uk#2187 introduced, was derived on the certified Microcosm
UK 2024-25 national release so that its VAT meets OBR's 2024-25
receipts. Binding obr.vat therefore mostly pulls the consumption weights
back toward that release, after this PR changes the consumption base
(the heating fuels and the division levelling), and it binds over the
private-school VAT that current-law vat leaves out until
policyengine-uk#2319.

The row is back on the measured-diagnostics register, signed on
2026-10-24 to 2027-01-24 (a draft for María), and rebinds when
policyengine-uk re-derives the factor on a file that does not bind it.
The changelog fragment states the final position, and the load test
follows the entry.

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

Vahid's review of #1383, item 4. policyengine-uk 2.148.4 adds
housing_benefit_residential_capital_exception, which asks for the
working-age residential capital tariff exception pre-assessed (Housing
Benefit Regulations 2006 reg. 52(4)-(5), (8)-(9); NI reg. 49), and the
build leaves it at False without saying so. It joins
UK_ENGINE_DEFAULT_INPUTS with the error it can make: the FRS samples
private households and records no assessment of the accommodation or
saved circumstances the exception covers, so Housing Benefit is
understated only for a qualifying working-age resident, who takes tariff
income on capital above GBP 6,000 instead of GBP 10,000. The register's
tests (an engine input the mapping never binds, written by no spine
column) cover the entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of #1383, item 3. The engine test only read the
energy_price_basis attribute back. It now also loads a written file
through policyengine-uk's Microsimulation and checks that each energy
column in 2025 and 2026 is the stored spending times the engine's energy
price index over its 1 January 2024 value, every household alike, and
that a copy without the declaration keeps its stored spending. The
changelog says what the declaration changes and that releases built
before it stay undeclared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of #1383, item 11: uk_consumption_drift_flags was called
only by its test, so the receipt's drift flag came from a hand run. The
UK calibration diagnostics now turn each flagged total or share into a
diagnostic_warnings entry (code uk_consumption_drift, severity warning),
naming it and the threshold it passed and none of its figures; the
payload's v8 schema stays as it is and nothing is gated on them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scope's share ceiling sums published values, so its unit label must be
the unit its targets are counted in. uk_support_floor_target_unit reads it
from the contract measurement (a count's entity, or pounds for an amount),
and the run's floor input refuses a scope labelled otherwise, beside the
approval-window check. The band-H scope was labelled "dwellings"; its rows
count households (occupied chargeable dwellings), so it now says so.

Vahid's nit 24 on microcosm#1383.

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

frs_annual_income's docstring still said current pay annualises at
365.25 / 7; since uk-data#531 it is 52 weeks (frs_spine.WEEKS_IN_YEAR).
The restated ons.household_energy_expenditure rationale is signed on
2026-10-24, the day it was restated. The held-rows fragment now says a
schema 3 register is refused rather than read, so an old run directory's
register no longer replays, and that each floor scope's unit is checked.

Vahid's nits 15, 19 and 21 on microcosm#1383.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The take-up contract still applied carers_allowance_newly_entitled to
persons outside Scotland, from before Scotland opened in the draw. Its
citation and population now say Scotland's carers take the same rate:
Carer Support Payment reads the same claim flag, and the contract holds
no rate of its own for it. The stage docstring says the same.

Vahid's nit 22 on microcosm#1383.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reverts 3ce1fca and d48652b, at María's ruling of 2026-10-24. They
withheld every constituency, local-authority and BRMA model figure from
what ships, which blanked the local fit evaluation and, as Vahid's
second review of microcosm#1383 found, stopped the local sign-off, the
release manifest's signed exclusions and the size evaluation (items 25
to 27). Her rule stands: a row that ten or more distinct source
households carry publishes its figures; a thin row, and any published
row that would give it back by difference, is withheld.

The next commit restores the joint check across grains that the must
(item 1) asks for. BRMA rows publish from ten households under the
national block's check (her ruling of 2026-10-17, re-affirmed on
2026-10-24).

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

Vahid's review of #1383, item 1 (must): the local withholding checked the
local rows one grain and area at a time, so a published region row less
its published local cells could give a withheld cell back, and so could a
region's constituencies less its local authorities, or a national row less
a family's published cells.

uk_withheld_row_sets now spans, beside the national block:
- each region's rows of every grain, the rows whose positive-weight
  carriers all live in it (local authorities and constituencies nest in
  regions);
- each local family with its higher-grain parents: the targets the
  cross-grain declarations (bridges, fan-out, sum and band bridges,
  partitions) or a nested measurement tie it to (uk_local_family_parents).

The blocks overlap, so a row one block withholds beside a thin one is
protected in every other block that holds it (any combination holding it
counts as too small to give back), and the blocks run again until none
withholds another row. Local rows need the frame's household region and
refuse without it; the dense gates and the Orrery summary pass each row's
contract target. On a synthetic region of 2,218 rows the check takes
under two seconds.

Tests: a region row less its cells, constituencies less authorities, a
national parent less its family's cells, a row withheld by one block kept
hidden in another, and the refusal; each mechanism mutant-checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of microcosm#1383. Nit 18: the feed vendors MHCLG's
Council Taxbase 2025 in its revision of 21 January 2026, whose counts
are as at September 2025; the row's selector note and changelog said
"the October count", after the feed's 2025-10 period label. Item 13:
the FRS 2024-25 asks for alimony or an allowance "from a previous
partner" (AliMny), so ALIAMT also holds a former cohabitant's payments,
which policyengine-uk's spousal_maintenance_income leaves out; the
survey does not say whether the partner was a spouse or civil partner,
so the stage's comment and changelog state the approximation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of microcosm#1383, item 7. The anchor grows each
sacrificed amount with the OBR's average earnings and the weights with
the ONS population to 2027-28; the OBR grows its base with employment
and earnings. The base is met at the stage's weights, and calibration
does not bind it. The stage's comment and the changelog say so; the
check of both OBR lines at the final weights is a private receipt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vahid's review of microcosm#1383. The Phase B and C receipts now give the
single adult discount count's vintage (as at September 2025, revised on
21 January 2026); the remainders-basis levelling, under which six
divisions come down on arm 3's spine and communication keeps its draw;
the donor's liquid fuels without bottled gas beside ONS's 04.5.3; the
bonus sacrifice rulings and the anchor's growth basis; the bound rows
that absorb the 52-week rule (item 15); and the benefit cap's
complementary withholding and its Universal Credit scope (item 17).
Figures that rest on the SPI channel's households stay with the
licensed data; arm 3's calibration receipt follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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

2 participants