Skip to content

One consistent UK target surface: enforced cross-grain reconciliation, declared uprating holds, nation household controls and the 942a1fa re-pin (#1123 PR A) - #1198

Draft
juaristi22 wants to merge 31 commits into
mainfrom
uk-target-surface-1123
Draft

juaristi22 wants to merge 31 commits into
mainfrom
uk-target-surface-1123

Conversation

@juaristi22

Copy link
Copy Markdown
Collaborator

Part of #1123 (PR A: items 1, 2, 6 and 7; private rent is PR B). Addresses #104.

Why

The K=25 audit found that most local misses surviving the dense solve were on the target side. Local rows were never uprated, 509 national rows were held at an older period with no declared index, nothing refused an undeclared hold, and reconciliation across grains was opt-in. This PR makes the UK target surface one consistent, enforced surface.

What changes

  • Cross-grain reconciliation is enforced. Every family bound at more than one grain declares how its grains agree (uk/cross_grain_declarations.json: bridges, partitions, relations) or the compile refuses. The lattice gains a nation grain, rows that span several legs, and same-geography partitions. The national register is reconciled across UK, nation and region in both loaders, and the joint local pass refuses a national factor away from one.
  • Uprating doctrine. A target bound from a fact older than the calibration year carries a declared index or a declared hold (uk/uprating_holds.json, 15 entries, approved_by null). The compile refuses an undeclared, expired or stale hold, nationally and locally.
  • Chronicle re-pin to 942a1fa (UK local-surface facts for the 2025 calibration: household counts by nation and area, communal-establishment residents by age, mid-2025 population estimates, UC households by area for calendar 2025, and BRMA geography and rent inputs (PolicyEngine/microcosm#1123, PolicyEngine/microcosm#1090) chronicle#313 via Add UK 2025 calibration publisher inputs chronicle#314). National population, local-authority ages, land, SLC repayments, ISC, DfI and public sector employment move to their 2025 editions. Spine inputs move only by publisher revisions (revised mid-2024 population, DfI rounding).
  • Households: nations win. Each nation's official estimate controls its census cells: LFS 2025 for England's nine regions, NRS for Scotland, Welsh Government rolled by population for Wales, Census 2021 rolled by LPS dwelling stock for Northern Ireland. The UK composition takes the nations' sum (29,082,020; the LFS UK total is a diagnostic, 0.27% lower).
  • HMRC. Income targets are on a taxpayer basis at both grains; area cells take HMRC's all-ranges growth and reconcile to the national SPI bands within 2%. Higher and additional rate taxpayers bind for each English region and nation (Income Tax liabilities Table 2.2, calendar 2025, on the engine's tax_band), and higher rate taxpayers bind by constituency and local authority in England, Wales and Northern Ireland (919 cells).
  • UC by area binds the calendar-2025 mean of DWP's monthly series, and the child bands follow each constituency's caseload.
  • Tenure. England's census shares drift by ONS SPREE's 2022 to 2024 share change; tenure cells follow their authority's households.
  • Small cells. Council-tax band cells under 50 dwellings defer by rule with an expiry (16 cells; uk/local_small_cell_rule.json, unsigned). A deferred cell still counts in its leg's control.
  • Make zero-valued calibration targets intentionally nonzero-weighted #104. The loss-scale options carry a census of zero-valued targets. The weighting rule is unchanged (UK sparse selection: keep household mass, composition, nation shares and per-area ESS at a tractable size — test the target-weight rule and the L2 penalty first, dataset size only if they fall short #1124 owns it).
  • Tooling. A rolled_forward_by_ratio value operation; both UK target kernels hash every module that decides a target value; tools/uk_target_surface_receipt.py writes and diffs receipts of the compiled surface; the national compile scans only each reference's own sources (the national parity receipts drop from about 2h25m to under 20 minutes on the 927k-fact feed, with identical candidate facts).

Value moves (receipt against the pre-change surface)

  • National: 1,267 rows. 37 added (12 nation household cells, 24 marginal-rate cells, dwp.hb.amount_pension_age), 168 moved (149 population rows, the 10 household-composition rows, land, SLC repayments, ISC, public sector employment, DfI), 2 removed (top UC payment-distribution bands the 2025 series does not publish).
  • Local: 21,788 rows. 919 added, 16 deferred, 18,374 moved (every age, census-household, tenure and UC cell; the four existing HMRC area targets; private rent through tenure).
  • Reconciliation factors: HMRC area cells to the national bands 0.996 to 1.019 (gate 2%; employment counts at 1.92%); nation household controls over constituency census cells 0.973 (North East) to 1.038 (London), Scotland 0.989, Wales 0.994, Northern Ireland 0.998; higher rate area cells to their 2025 rows 1.19 to 1.40; UC households by area to the GB caseload 0.921 (0.942 before this PR).

Verification

  • tools/uk_target_surface_receipt.py --communal runs clean end to end on the pinned feed.
  • Engine-free suites: microcosm-build UK and shared 7,105 of 7,106, microcosm-calibrate 361 of 361, microcosm-graph 727 of 729; tools/ci_test_plan.py verify ok. The failures are local-environment only and also fail at the base commit (pe-uk installed in the engine-free venv; a node package the Orrery parser tests need).
  • The Make zero-valued calibration targets intentionally nonzero-weighted #104 edit to microcosm.calibrate.solve moves the loader golden vector, the US seed digests, the US coverage evidence and the calibrate graph parity pins; all are re-pinned as main does for solver edits.
  • Not run: engine tests (the tax_band measure is tested against fakes only here), and the dense re-solve, so the effect on fit is not yet measured.

Departures from the plan

  • No closure gate in gates.json: closure fails closed at compile instead.
  • No council-tax band partition, and the preflight tool is not updated.
  • The A15 census-household uprating step is kept; it now targets the nations' sum and the nation bridge then rescales.

Open decisions (María)

Before merge:

  • Signatures on the 15 holds and the small-cell floor of 50.
  • England households: LFS regional controls (as built) or the England total with projection shares.
  • Scope: Scottish higher rate area cells and all additional rate area cells stay unbound. Scotland's 2023-24 area cells predate its advanced rate; additional rate area cells are rounded to the thousand and suppressed in 85 constituencies.
  • Accept the departures above, or build the gate and remove A15.

Can follow as issues:

  • Clip self-employment losses in the amount rows to match HMRC.
  • Salary-sacrifice relief growth.
  • The Northern Ireland household method.

After merge

  • Re-freeze the scoring register in uk-candidate-eval (the reconciled national values change the registry version).
  • PR B (rent) rebases on this branch.

🤖 Generated with Claude Code

juaristi22 and others added 30 commits October 10, 2026 15:54
…rget-surface receipt tool (#1123)

The full graph's target kernels and the national kernel each listed their own
modules, and neither listed the shared resolver (build/ledger_targets.py,
where linear_combination and the period rules live) nor the HMRC uprating
appliers, so an edit there could reuse a cached surface computed by the old
code. uk_runtime/target_compile_modules.py now names the modules that decide
a target value once; both kernels hash that tuple, and a test pins that every
direct microcosm.build import of a listed module is listed or excluded with
its reason (the country spec, covered by its fingerprint; spine modules
imported for a constant or a type), so the coverage holds transitively.

The compilation node's body moves into compile_uk_full_target_surface(),
which tools/uk_target_surface_receipt.py shares: the tool writes every
national row and every reconciled local cell with the reconciliation factors,
holds and coverage receipts, and diffs two receipts row by row, so each
target-surface change in #1123 states exactly which values it moves.
load_uk_full_target_inputs(include_validation_periods=False) compiles the
calibration year alone for the tool; the node is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d references do (#1123)

private-pension amount references to hmrc.itl_2026.total_income_growth_by_
total_income_band but left the contract (the regeneration source) and the
membership report on the flat OBR indices: a regeneration from the contract
would have silently put the flat indices back. The three contract targets now
declare the ITL band index with a selector note saying why, and the
regenerated membership report records the band factors the references
already apply. The national references regenerate with identical content; the
committed file is now the generator's exact output again (ASCII escapes, as
the regeneration test compares bytes), so test_committed_surfaces_regenerate_
from_pinned_feed[national] passes with the pinned feed.

A new test refuses any reference or hold whose uprating index differs from
its contract target's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ions, declared overlaps (#1123)

The UK cross-grain operator now has a nation grain between country and
region, lower rows that span several legs (England over its nine regions,
England and Wales over regions plus Wales), same-geography partitions, and a
refusal of a partial middle tier that would take a whole control onto the
legs it covers. Control agreement is checked to 1e-12 rather than bitwise.

The UK bridges, local routes, overlap relations and signature-incomplete
targets move from code to uk/cross_grain_declarations.json, registered in the
country package so the spec fingerprint covers them. A coverage check refuses
any local target without a route to a control, any undeclared overlap across
grains, and any stale declaration; uk/target_doctrine_exceptions.json
tolerates the 22 gaps that remain, each naming the #1123 change that closes
it. No target value moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… reconciled values (#1123)

Before this, only the joint local surface was reconciled and only its local
cells were written back: a region row rescaled to its country control fed
the constituencies below it while the national register the solve binds
kept the raw value. reconcile_uk_national_registry now reconciles the
national register in both target loaders, before the measure exclusions and
before the frozen scoring register is compared, so the full and the
national-only graphs bind one value per quantity.

Two relations reconcile there: exact measurement signatures across the
country, nation and region grains (the CGT regional cells under the UK
total), and the new declared band bridges, where a projected UK ITL Table 2.5
band, or a run of them, controls the SPI Table 3.11 regional cells nesting
in it. The ten ITL/SPI-region overlap exceptions close. Validation-period
registers stay as compiled. The joint local pass then refuses any factor
away from one on a group whose lower grain is a control grain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al at compile (#1123)

A compiled UK target whose Chronicle fact is older than the calibration
period must now either be uprated by a declared index or match a declared
hold in uk/uprating_holds.json (matched_period, in_year_snapshot,
control_rescaled, reviewed_no_index; each with reason, tracking and expiry,
approved_by left null until signed). Both target loaders check the full
compiled national register and the local register for the calibration year
and refuse an undeclared, expired or stale hold; windows of the year's own
subperiods (calendar-year means, monthly Stat-Xplore windows) are not holds.
The holds a later #1123 change closes sit in target_doctrine_exceptions.json
as undeclared_hold, each naming that change.

A local area target that declares an uprating_index is now uprated at
authoring and at compile exactly as a national target is (no local target
declares one yet). The receipt tool records the holds and the national
reconciliation. experiments/1123-target-vintage-audit.md records the C0
vintage audit behind the register.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…employer row's 2024-25 anchor (#1123 C6b)

HMRC Table 6.2's 2024-25 reliefs are the latest edition. The employer NICs
relief already anchors the sacrificed amount at 2024-25 and restates only the
rate (#1069 c11); the income tax rates and the employee Class 1 main rate are
unchanged from 2024-25 to 2025-26, so the other five relief targets follow it
as a declared reviewed_no_index hold rather than an undeclared one. Growing
every relief with earnings instead stays an open ruling. No value moves.

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

A zero target is a real fact (no recipients in an area, no dwellings in a
band): under the default scale max(abs(target), 1) it is measured in one
unit of its basis, so a positive estimate is charged its full miss. That was
true but incidental. The solve's and the scorer's target_loss_scales options
now carry a zero_targets census (count, scale range, loss-weight range and
zero-weighted rows), and tests pin a zero target's positive weight and loss
contribution under an amount/count half-and-half weighting. The weighting
rule itself does not change (microcosm#1124 owns that).

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

ONS's June 2026 public sector employment edition keys its quarter rows
03_2025, 06_2025, 09_2025, 12_2025, where the March 2026 edition wrote
dec2024. The series key stripped the year but kept the month, so the four
quarters read as four series and no monthly window could average them. A
numeric month beside its year (MM_YYYY or YYYY-MM) is now a period fragment
of a layout value, like a month name; a table number such as table_1_2 or a
thirteenth "month" stays identity.

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

The artifact is chronicle main at the #314 merge (PolicyEngine/chronicle#313):
927,239 rows, consumer_fact.v4 with an unchanged schema, 2.7 times the
825406f feed. It brings household estimates by nation and region, mid-2025
population for the UK, regions and every local authority with ONS's revision
of mid-2024, monthly UC by area, census residence and tenure tables, rent
inputs, and newer SLC, ISC, public sector employment, land and DfI editions.

Values that move, all to newer official editions:
- the 149 national population rows to ONS mid-2025 (UK 69,483,939);
- local-authority ages to mid-2025 (2,872 cells, Northern Ireland
  included); Scottish constituency ages to NRS's revised mid-2024 edition
  (452 cells); the UC May 2025 area cells to DWP's revised figures;
- SLC repayments (FY2025-26), ISC (January 2025), DfI receipts (FY2025-26),
  land (preliminary end-2025, bound explicitly: total GBP 6.87tn against
  7.12tn at end-2024) and public sector employment (the mean of its four
  2025 quarters, 6,176,000, instead of a December snapshot).

Three selectors move with the data: the ONS mid-2025 workbook lays its rows
out by geography, so the population selectors pin ages through
dimension_values and no longer require the age layout (without this the UK
total fell back to mid-2023); land binds the preliminary rows; public sector
employment declares its quarterly window. Three vendor selections pin the
periods their stages were built on, so spine inputs move only by publisher
revisions: mid-2024 population (UK and twelve regional totals) and the DfI
edition's rounding of eleven earlier values.

The hold register follows: 76 re-pin exceptions close; ISC is an in-year
snapshot; SLC student support (its provisional 2025/26 rows are summary
totals, not the products bound) and cars fuel duty (no fiscal-to-calendar
roll-forward yet) are reviewed holds; constituency ages are control_rescaled
under the mid-2025 regional controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ands (#1123 C7)

scaled_by_ratio restates a cell to a narrower universe, so its ratio is a
share in (0, 1]. Carrying an official estimate for an earlier period to the
target period needs growth: Wales's mid-2024 household estimate by its
mid-2025/mid-2024 population, Northern Ireland's census-2021 households by
LPS dwelling stock 2025/2021. rolled_forward_by_ratio computes
base * numerator / denominator, where the base is the reference's own
selector (one cell, or the sum of the cells it resolves at its latest
period), the numerator is pinned to the target period and the denominator to
the base's own period, each resolving exactly once; the growth is bounded to
(0.5, 2] so a mis-pinned operand cannot pass for growth. Every input, the
ratio and both periods are recorded on the spec.

This is the only change to the shared build/ledger_targets.py in #1123.

The base operand may move the base below the reference's geography (a
nation's estimate built from its districts' census cells); the cell keeps
the reference's own geography.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… area cells to the SPI bands (#1123 C8)

SPI Table 3.6 and the area Tables 3.14 and 3.15 cover UK income tax payers
only, and they agree within 0.5% at source. The national SPI band rows now
carry income_tax > 0 like the area rows; the employment and self-employment
counts count income != 0 at both grains, as HMRC counts a loss-making source
with nil income (Table 3.10 notes); the area filter concept is unified.

The area rows, held at 2023-24 until now, take HMRC's own all-ranges growth
from ITL Table 2.5 (taxpayer counts x1.0763, total income x1.1340, calendar-
2025 window over 2023-24). A new fan-out sum bridge then reconciles them per
grain to the national SPI band partition summed over every band of the full
compiled register (the measure-excluded GBP 1m-and-over band included),
refusing a factor more than 2% from one.

Fan-outs are never exact-signature controls: the coverage check no longer
counts a grouped national target as covering a local one, and the ten
fan-out overlaps that exposed (UC payment distributions, CGT banded tables)
are declared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on tenure under them (#1123 C9)

Nations win (decision of 2026-10-07): four new national targets carry each
nation's official household estimate for 2025 and control its census
household cells through a per-geography bridge:
- England's nine regions, ONS LFS April to June 2025 (summing to 24.329m);
- Scotland, NRS June 2025 (2,570,799);
- Wales, Welsh Government mid-2024 rolled to 2025 by Wales's mid-2025 over
  revised mid-2024 population;
- Northern Ireland, the eleven districts' Census 2021 households (768,810)
  rolled by LPS dwelling stock April 2025 over April 2021.
The UK household-composition partition no longer parents the census cells;
the national reconciliation scales it onto the nations' sum through a
declared sum bridge, and A15 uprates the census cells to that sum, with the
LFS UK total kept as a diagnostic and its gap recorded. The census tenure
cells partition their authority's households (share_of_parent), so they
follow the authority's reconciliation.

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

A census tenure cell's share is drifted, for English authorities, by its
category's share of the authority's dwellings in 2024 over 2022 (ONS SPREE,
vendored as ons_spree_tenure_dwellings.json). Dwellings move shares, never
levels: the level comes from the tenure partition. Authorities in the other
nations hold their census shares, said so on each spec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… receipt communal residents (#1123 C10)

The area UC caseload and its child bands move from the May 2025 snapshot to
the mean of the twelve calendar-2025 months of DWP's monthly Stat-Xplore
series (the three-plus band sums its three published counts), the window the
national UC rows use, so the bridge to the GB caseload reconciles like with
like. The child bands partition each constituency's caseload
(share_of_parent: the unknown-children category is not bound).

The receipt tool gains a census communal-establishment validation (--communal,
RM120 and UV101a): validation only, as decided; 114 constituencies and 64
authorities have at least 10% of their 18-24 residents in communal
establishments.

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

uk/local_small_cell_rule.json declares an absolute household floor (50,
proposed, unsigned) for council-tax band cells. The local generator's second
pass defers, by rule rather than by area name, every active cell below it
(16 today, including Blaenau Gwent band H and Barking and Dagenham band G),
records each deferred value, and carries the rule's expiry, which the loader
enforces at its review date. The joint surface still counts a deferred
cell's value in its leg's control, so deferring it does not inflate its
neighbours.

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

DWP's May 2025 UC area cells keep their established record-set spec id inside
the January 2025 to May 2026 monthly series (PolicyEngine/chronicle#314), so
consumers that select the May cells alone keep working. The window guard
compared spec ids and refused the calendar-2025 window, although the
publication, release key, raw file and parsed table are one. It now compares
the parsed record set; a spec id is a selector's identity, not a property of
the publication.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
National authoring compiled each reference against its own source's facts
and the operand facts found there. A roll-forward's growth can come from
another publisher than its base: Wales's household estimate (Welsh
Government) by ONS population, Northern Ireland's census households (NISRA)
by LPS dwelling stock. Operand selectors that name another source now read
that source's facts too, appended after the unchanged same-source pool, as
the runtime compile already does through reference_fact_selectors.

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

Northern Ireland's household control rolls its eleven districts' census
cells forward to the nation. The hierarchy needs one geography for the row,
so the national generator marks a roll-forward whose base operand moves the
geography as a composed row (composed_from_level, with the row's own
geography), the path region rows built from their authorities already take.

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

The generator forced a sum on dwp.uc.households_by_area_children_3plus for
the May-snapshot spelling; the contract now declares the calendar-2025
monthly window sum, which the forced operation overrode.

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

DWP publishes each UC child-count cell as its own record set inside one
release, raw file and table (PolicyEngine/chronicle#314). The window's
shared-publication key no longer compares record sets; the per-cell series
check still pins each cell to one record set across its months. The
three-plus band's selector names its three cells, and the authored
reference header lists rolled_forward_by_ratio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vendored resource keeps each row's concept at top level, not under
observed_measure, so the drift found no SPREE row for any authority. The
fixture now matches the vendored shape and a test drifts Hartlepool from
the packaged resource.

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

The re-pinned DfI edition restates 2024-25 Translink receipts to the
nearest £0.1m (49.6m Metro and Glider, 100.5m Ulsterbus), so the
recomputed price reads the vendored rows' new values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compile_uk_target_registry scanned the whole Chronicle feed once per
reference; on the 927,239-fact feed the national parity receipt took over
two hours. Facts are now indexed by source once, and a reference whose
selectors all pin source_name scans only those sources' facts, in feed
order. A selector without a source pin keeps the full scan, so the
candidate facts are exactly those of the full scan (checked on the pinned
feed for 68 references, the 28 ratio-operand references among them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every coverage gap and undeclared hold it tolerated is closed, so the
coverage check and the holds check now refuse any gap outright and the
resource leaves the package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and higher rate taxpayers by area (#1123 C8b)

Income Tax liabilities Table 2.2 counts taxpayers by marginal rate for each
English region and nation; the two new national targets bind them on the
region tier as the calendar-2025 window of HMRC's 2024-25 and 2025-26
projections, measured on the engine's tax_band. From 2024-25 HMRC's Scottish
additional row is the advanced and top rates together, as the engine groups
them.

SPI 2023-24 Tables 3.14 and 3.15 give higher rate taxpayers by constituency
and local authority. They bind for England, Wales and Northern Ireland
(area_scope): Scotland's 2023-24 cells predate its advanced rate. Each
region's or nation's Table 2.2 row controls its area cells through a
per-geography bridge, the cells keep their 2023-24 shares (a declared
control_rescaled hold), and at source the cells sum to within 1.2% of each
2023-24 row at both grains. The additional rate area cells are left
unbound: rounded to the thousand and suppressed in 85 constituencies, they
cannot sum to their controls.

The area metric is appended to the local metric list, so no incumbent
metric index moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1121 named the SPI amounts' ITL band index in the contract before this
branch rebased, so C2 adds only the test that pins every reference's index
to its contract target's; there is no user-visible fix to record.

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

National references: 1,267 active (the four nation household controls,
twelve cells; the 24 ITL Table 2.2 marginal-rate cells). Local references:
21,788 active (the 919 higher rate taxpayer cells; the 16 council-tax band
cells below the small-cell floor deferred; the UC child bands on the
calendar-2025 window). The local census, the release input coverage
manifest (the CGT conditioning resource's feed header moved with the
re-pin), the uk-data target parity register and the three signed compile
parity receipts are regenerated, the count pins follow, and the re-pin
runbook gains the manifest step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The zero-target census edited microcosm.calibrate.solve, which the calibrate
graph parity kernel, the spec-engine loader golden vector and the US
seed-protocol and seed-map digests all hash: the calibrate parity fixture is
re-pinned (tools/graph_parity_repin.py), the loader golden and
EXPECTED_HASHES carry the new digests (the commit before #104 reproduces the
old ones), and docs/evidence/spec-engine/us-f0-coverage.json is regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e calendar means (#1123)

The full national compile refused two undeclared holds and five stale
declarations. The Wales and Northern Ireland household controls resolve
older bases (mid-2024, Census 2021) but rolled_forward_by_ratio carries
them to 2025 by a ratio whose numerator is pinned there, so they are not
held. The UC payment-distribution and Scotland youngest-child rows were
November 2025 snapshots when the hold was declared; on the 942a1fa feed
they bind the calendar-2025 mean of the monthly series, so the
in_year_snapshot declaration no longer matches a hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The joint local surface refused its own fan-out sum control: the synthetic
control's signature carried filters at top level, and the reconciliation
canonicalizes only a 'measurement' mapping. The test now runs the synthetic
signature through that canonicalization.

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

Copy link
Copy Markdown
Collaborator Author

Independent adversarial review (Claude Code) at 67fc1dd04

Static review only. I read the code and the committed registers at this head and at the merge base aca40a692, and I checked the UC source package in Chronicle at 942a1fa. I ran no tests, no compile and no receipt tool, so every factor below is control ÷ the sum of the committed cells before reconciliation, the same ratio the code takes. Engine CI was still queued when I looked.

Verdict. The core machinery holds up. Cross-grain coverage is complete for the committed contract: 15 multi-grain ids, 28 overlap pairs, none undeclared. The holds cover every lagged national row, and the #104 edit to solve.py changes no numbers. But four things should change before this leaves draft:

  • the UC bridge reconciles two different concepts;
  • the 2% fan-out gate never sees the local-authority grain, which needs 2.1%;
  • the new national band bridges have no tolerance;
  • the signatures the body asks for aren't enforced by any code.

Must fix

  1. UC by area reconciles all-claims cells onto a paid-claims control.

    • National control: dwp.uc.households binds payment_indicator: "Yes" (uk_population_targets.json:14716).
    • Area cells: the selector (:28243) reads universal_credit_households with no payment dimension. The Chronicle package at 942a1fa (dwp/uc_households_by_constituency_january_2025_may_2026) has only the date and PCON24 fields, so it counts all UC households, nil-award claims included. docs/uk-uc-paid-target-contract.md is why the national rows moved to paid claims in the first place.
    • Size: the areas sum to about 6.73m against 6.20m for GB, hence the 0.921. Aligning the windows widened the gap (0.942 → 0.921), which points at concept, not timing.
    • The selector note says the bridge "reconciles like with like"; it doesn't.
    • Effects:
      • The uniform factor assumes the same nil-award share in every constituency.
      • The child bands keep their all-claims shares, so after the bridge they disagree with the national paid rows: +2.8% with no children, −1.5% with one, −3.5% with two and −5.1% with three or more. (The national no-children figure is the total less the child rows.) The solve gets conflicting targets across grains.
      • The coverage check can't see this, because the signatures differ.
    • Fix: extract the area series with Payment Indicator = Yes (UC_Households carries the field).
  2. The 2% fan-out gate is never applied at local-authority grain, which needs more than 2%.

    • Factors: at constituency grain the committed values reproduce the body exactly: 1.0096, 1.0192, 1.0082 and 0.9958. At local-authority grain they are 1.0118, 1.0211 (employment counts), 1.0160 and 1.0023. Torbay and Scilly have no SPI LA cell.
    • Why nothing refuses 1.0211:
      • hmrc.employment_income.count at constituency and LA forms an exact-signature group with no control-grain row, so constituency parents LA under the single-winner rule.
      • The sort key at cross_grain.py:256-266, (precedence[lower], -precedence[winning], id), runs constituency_over_la before the bridge's country_over_la.
      • So by the time the bridge pair runs, LA already equals the reconciled constituency total. Its factor is about 1, and _assert_uk_fanout_sum_factors (uk_runtime/ledger_targets.py:2579) only reads bridge groups.
    • The declaration says "the constituency and local-authority cells reconcile to it per grain… a factor beyond 2% is refused". test_uk_fanout_sum_bridges.py only builds a constituency receipt.
    • Fix: either check the bridge against the pre-reconciliation LA total, or gate the signature group too.
    • A related point: the gate's rationale is "the tables agree within 0.5% at source", yet employment counts land at 1.92%. The likely cause is that the area cells take all-ranges growth while the bands take band-specific growth. If so, the gate mostly measures that index choice, and with 0.08 points of headroom the next ITL re-pin may trip it.
  3. The 24 national band bridges move cells by up to 5.7% with no bound.

    • No bound: _apply_band_bridges (national_reconciliation.py:~229) takes factor = control / raw_total and refuses only a non-positive total.
    • Wrong reason: all 24 declarations give the same reason, "about 0.5 percent short".
    • Factors:
      • income tax £12,570–15,000: 1.0573
      • taxpayers £12,570–15,000: 1.0274
      • total income £12,570–15,000: 1.0228
      • income tax £15,000–20,000: 1.0142
      • the rest: 1.002–1.008
    • Knock-on: the £12,570 income-tax bridge includes the West Midlands cell deferred in target_fit_reviewed_exclusions.json:5. That deferral was measured against a 55.1m target, which becomes about 58.3m. The body's "Reconciliation factors" section leaves the band bridges out.
    • Fix: add a max_factor_shift as the fan-out bridges have, state the real gaps per bridge, and re-measure the deferral (UK landlords: bind the SPI net property amounts, leave property_wealth to the engine, add receipts and finance costs #1145 holds the same deferral as stale).
  4. Nothing enforces the signatures the body asks for.

    • Unsigned: all 15 holds and the small-cell rule are approved_by: null, status: "proposed".
    • What the loader checks: load_uk_uprating_holds (uprating_holds.py:62-84) checks only kind and reason. expires_on is optional (:169); every entry has one today, so that part is latent.
    • Contrast: the measure exclusions refuse an entry without approved_by, approved_on or expires_on (measure_simulation.py:719).
    • Small cells: the 16 cells are labelled signed_deferred in the membership while the rule is unsigned.
    • Fix: make proposed or a null approver fail closed, so the open decision can't be merged around.

Should fix

  1. "Value moves" doesn't match the committed registers, and no receipt is committed.

    • Active national rows go from 1,231 to 1,267: 36 added, not 37, and none removed.
    • Moved: 160 rows move at register level, against the body's 168.
    • Missing from the count: the moves this PR's own reconciliation makes (the band bridges on the regional SPI rows and the CGT regional rows). So the "before" receipt seems to postdate 7a7ddbdf0.
    • Can't be re-run: tools/uk_target_surface_receipt.py needs local .codex-work inputs.
    • Please commit the before receipt taken at aca40a692 and the after receipt, or at least their diff, with the band-bridge and both fan-out grains' legs.
  2. The doctrine has escape paths.

    • Relations: declared relations (31) only silence the static overlap check (cross_grain_declarations.py:373-395). Nothing tests "subset" (parts ≤ whole) numerically, so an infeasible pair would compile.
    • Coverage path: the coverage check runs only on the joint path (uk_runtime/ledger_targets.py:2464), not in the national-only loader.
    • is_uprating_hold: it returns False for any row carrying uprating_factor (uprating_holds.py:110). tenure_drift.py:119 stamps factor 1 on every non-English tenure cell, so the Welsh, Scottish and NI tenure cells (a few hundred) are held at census shares without a declaration.
    • Missing periods: a missing or unparseable period also returns "not held" instead of refusing.
  3. The 919 higher-rate area cells.

    • One factor per region: each region's factor (1.19 London to 1.40 NI LAs) keeps 2023-24 shares within the region. Across regions the growth is steeper where the base share is lower: about 1.37 for NI at roughly a 5% share, against 1.19 for London at about 12%. Within-region spreads are wider than that, so a uniform factor within a region probably overstates affluent areas and understates poor ones.
    • Rounding: every one of the 919 values is a multiple of 1,000, 192 are at or below 5,000, and the minimum is 2,000. That's the rounding the PR cites for leaving additional-rate cells unbound. NI constituencies sum to 89k against 87k for NI LAs.
    • Adjudications not amended: local_binding_adjudications.json full_frs_tei_band_unavailable still says hmrc_income_by_area binds only the employment and self-employment amount and count cells. population_universe_private_households still says mid-2024.

Minor and questions

  1. Land: the production-2023 parity receipt gains ledger_absent for the three ons.land.* rows, which resolved at 2023 on the base.
  2. rolled_forward_by_ratio:
    • The base sums every eligible fact at the latest period with no count or duplicate check (ledger_targets.py:~2359).
    • The operands aren't required to be the same series apart from the period; only the (0.5, 2] bound guards them.
    • The current NI and Wales values are right.
  3. SPREE drift: as far as I can tell it only moves social renting against everything else; the private-rent-to-owned ratio barely moves in any authority. Social-stock factors such as Rushmoor's (about 0.76) look like reclassifications, and nothing bounds them. That matters for PR B, which builds on tenure/private_rent.
  4. Question: Scottish tax_band. The engine applies Scottish thresholds to income including savings and dividends, while HMRC's Scottish rates apply to non-savings, non-dividend income. This affects only Scotland's national higher and additional rows. It's tested against fakes only, as the body says.
  5. Question: Scotland and Wales. The feed at 942a1fa appears to carry NRS 2025 households by council area and Welsh Government mid-2024 households by LA. The census-share hold keeps 2022 and 2021 shares instead. Is that deliberate, given rules 1 and 2 of the vintage audit's source preference?
  6. Stale text.
    • docs/uk-local-cross-grain-reconciliation-802.md:115 and :165-168 (A15 and the composition partition).
    • uk_local_target_census.json:848 and :871 (the "all cells bind" counts, which now include deferred cells).
    • d10b8b96b's rationale: those UC rows were already calendar averages at the base.
    • "eleven" DfI values: 17 changed.
    • The Make zero-valued calibration targets intentionally nonzero-weighted #104 changelog says zero targets "keep a positive loss weight", but the code only reports it.

Interaction with #1145 and #1167

Checked and fine:

  • The nations sum to 29,082,020, and A15 doesn't double-scale.
  • The nation household factors match the body.
  • Deferred small cells count in their leg's control and don't inflate neighbours.
  • Spine inputs move only by publisher revisions: the revised mid-2024 population and the DfI rounding. ons_household_expenditure_facts is metadata only.
  • The US and loader digest moves come from solve.py being an attested kernel, with no US value change.
  • The source-indexed national compile selects the same candidates by construction.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant