Adopt the UK local-geography and firms contracts and compile the local target surface (#708, #759) - #795
Adopt the UK local-geography and firms contracts and compile the local target surface (#708, #759)#795juaristi22 wants to merge 14 commits into
Conversation
c5b5424 to
394ac6a
Compare
|
Automated review pass (Claude Code, high effort, diff only — no execution). Code hunks reviewed; the ~1.05M lines of generated JSON were not read row by row. Four findings, and the first is a merge-block. 1.
|
|
Thanks — all four findings and the closing note are dispositioned in 1 (merge-block) — accepted, resolved beyond the ask. The reviewer's ruling on this thread 2 — accepted. Code/length assertions on every positionally-consumed source: code-keyed families 3 — accepted with a ruling. A missing 4 — accepted. The parameter forward is restored on the national path; declared parameters reach Closing note — accepted, and it caught something on its first run. The crosswalk's boundary Dispositions in 🤖 Generated with Claude Code |
d1fc092 to
7414eea
Compare
|
Re-review of The corrective posture is right, and it is a better outcome than what I asked for. Correcting #468 at extraction — name join, attach after the boundary mapping on native PCON24/LAD codes, child splits recomputed from corrected totals — makes the fixture an independent statement of truth rather than a mirror of the upstream defect, and it decouples this PR from the uk-data fix. Splitting the UC rows into the two ruled classes (982 totals at the uniform national rescale, 2,525 child-split rows as imputation-vs-published) is what makes the remaining scatter interpretable instead of suspicious. Four gaps in the new code. None of them undo that; all four are the same shapes as the original round, one layer in. 1.
|
7414eea to
8b8d4e4
Compare
|
All four dispositioned in 1 — accepted, fixed before merge as asked. Largest-remainder allocation with a per-area closure assertion: the four buckets now sum exactly to the corrected total, and the fixture proves the property about itself rather than inheriting it. Re-extraction moved exactly 225 split rows by at most 1 household each; nothing else changed, and the receipt counts are stable (23,834). 2 — accepted. A positionally-consumed source without a 3 — accepted, all three escapes. An unstamped fact, an unknown code prefix, and a level with no declared vintage each refuse by name. The unstamped case got your wording in the error text, since it is the case the gate most needs to catch. 4 — accepted, both halves. The join asserts totality in both directions (unmatched publisher rows fail with their names listed), and a normalization collision is now distinguished from a true duplicate in the error, so whoever hits it sees the real condition. The commit also carries a rebase onto 🤖 Generated with Claude Code requesting review again @vahid-ahmadi |
|
Third pass over The round-2 fixes hold, and I verified rather than assumed:
Two residuals and one thing worth confirming. 1.
|
|
All three dispositioned in 1 — accepted, and thank you for the precision of the diagnosis. The assertion now compares the allocated buckets against the value the fixture actually stores ( 2 — accepted. A declared level the roster lacks now refuses by target and level instead of skipping into 360/361 — confirmed, by name. The incumbent's 🤖 Generated with Claude Code |
…le machinery Re-homes the two remaining UK selection contracts from Chronicle per the chronicle#166 ruling (Chronicle is facts-only; selection contracts live in the consumer), and adds the machinery that will compile an area-grain target surface from Chronicle facts. Mirrors the national precedent (#707 contract, #735 authoring/compile). Contracts (#708): - uk/uk_local_geography_targets.json: 25 targets in profile order, selectors translated to the wave-3 record-set vocabulary. The Chronicle profile is stale against the facts it was meant to select -- only 4 of 25 selectors resolved; the 8 age selectors matched every band and missed the NRS/NISRA legs entirely, so Scotland and Northern Ireland were unreachable. Each correction is signed in profile_parity.corrected with the original selector and the reason. - uk/uk_firms_targets.json: 8 targets verbatim (all selectors already resolve), closed over firm_generation.UK_FIRM_TARGET_IDS. - local_targets loads the committed contract instead of duck-typing a Chronicle-exported profile object; contract order still equals metric_names(area_type) minus the ladder-derived households column. Shared machinery: - record_set_spec_id selector key: the nation-neutral, band-resolved vocabulary the local contract selects by. - count_x_mean value operation: SPI publishes count/mean/median but no amount measure, so the two amount targets resolve as count x mean per area, with both member facts ordered and recorded in provenance. Local surface (#759, machinery only): - local_area_crosswalk.json + generator: 650 constituencies at pcon_2024 and 361 local authorities at per-nation vintages, bound to the ladder artifact sha. - author_area_target_references: roster-driven fan-out over (target, area). Absences are refused unless signed, signings that compile are refused as stale, and signed areas outside the roster are refused -- a missing area can never become a silent drop. - country_spec loading and compile_uk_local_target_registry for the local surface. The UK spec bundle digest moves because country_package.json is hashed into it. Not included, and deliberately: the committed reference/membership pair, the parity fixtures and gate entries, and the national regeneration -- each needs an input this run did not have. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runs the local reference compile against the staged consumer-facts artifact and commits the resulting surface, with every absence signed against measured feed coverage rather than waved through. Feed: 108,112 rows, facts_sha256 4395a4e7…, built from Chronicle main @33ca98a. Surface: 18,631 candidates over 25 contract targets x their declared levels (650 constituencies, 361 local authorities); 17,077 active. Deferrals, each signed with the coverage fact that causes it: - Universal Credit is published for Great Britain only, so the 18 Northern Ireland constituencies and 11 Northern Ireland local authorities have no UC facts (29 rows on the total, 72 on the child splits). - Equivalised income is published at MSOA grain; the feed carries no local-authority rows, so all 3 targets defer at their declared level. - Every private-rent fact in the feed is dated 2026-06, after the 2025 target period, so the family cannot bind at this period; Scotland is published at BRMA grain and Northern Ireland is absent. - Four SPI cells are genuinely missing upstream: E14001416 publishes a self-employment count with no mean, and two local authorities carry no SPI target measures. Fixes an age-selector defect this compile exposed: the corrected selectors still pinned source_name "ons", which excludes the NRS and NISRA legs and would have left Scotland and Northern Ireland unreachable -- the original profile's central bug, carried one step further. Dropping the pin resolves all 8 bands across all four nations (8,088 references). The census artifact regenerates with no source left documented_unpinned: five are pinned to the feed, and the two that cannot bind carry their signed reason. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…igest re-cuts Wires the two release-blocking preflight gates over the compiled local surface and re-cuts the battery digests once: - uk_ledger_compile_parity_local_incumbent_2025: the value-parity gate against the incumbent fixture (23,545 rows extracted at 12a1e028 by replaying both local loss builders' y-sides, raw and calibrated values recorded per row with each family's scaling factor). The ledger_compile_parity binding gains a registry_artifact parameter (allowlisted, defaulting to the national key) and UKGateBinding an artifact_selector so the requirement follows the parameter; the national driver compiles and passes uk_ledger_compiled_local_registries. - uk_target_surface_local_default_2025: the structural fixture-A gate — the in-code metric_names(area_type) surface fanned over the crosswalk roster, reconciled against the compiled registry with the membership's signed area deferrals as reviewed exclusions. No values are synthesized. The households column is excluded by rule, not absence: census_households binds from the OA-ladder artifact (microcosm#542), never from Chronicle facts, so its 1,011 area rows carry a ladder-derived reviewed exclusion and the gate passes with zero unexplained misses. Every row of the local signed-differences receipt (23,837) carries a ruled rationale; none keeps the classifier's generic text: - The 3,510 UC rows are an incumbent-defect class, not calibration drift: policyengine-uk-data#468 (the incumbent's local UC targets are positionally misaligned; verified by name-join, which reproduces the same Stat-Xplore publication at its uniform national rescale). Signing them as drift would have been a blanket amnesty over a diagnosed defect. - Deferral families cite their causes: NI UC and 2026-06-only PIPR carry the chronicle#200 asks; equivalised income carries the MSOA-grain ruling; the 9 SPI cells genuinely missing upstream are named individually. - Vintage/scaling classes (age x0.9 literal, SPI projection ratio, per-nation census tenure) and the 13 ruled out-of-contract exclusions (devolved-rent constants, council tax) keep their adjudicated texts. The incumbent-name guard allowlist gains exactly the fixture and its extractor - the two files whose purpose is citing the incumbent as provenance. Gate battery digests re-cut from the live producer payload: policy cdc8d49a..., manifest aa03ca22..., fingerprint c43de62c...; UK spec bundle 213274cb.... Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The incumbent-reference guard forbids the live tree from naming the retired UK data package, and the receipt's 3,510 UC rows carried the long-form citation. Use the repo's short form instead of widening the guard's allowlist: those exceptions are reserved for artifacts that must carry the incumbent's name as provenance (the sha-locked parity references and the fixture's source_repository field), which a rationale citing an issue does not need. Every other citation in a non-allowlisted file uses the short form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aria's ruling)
Merges uk_national_targets.json and uk_local_geography_targets.json into
uk_population_targets.json - the declaration-layer analog of the one-dataset,
filter-by-geography doctrine. All 214 targets survive at their declared levels
(the overlapping families are different cuts, not duplicates); firms stay a
separate contract.
The local UC ids consolidate into the national dwp.uc.* namespace as
dwp.uc.households_by_area and dwp.uc.households_by_area_children_{0,1,2,3plus}
- dwp.uc.households itself is taken by the national caseload target. Metric
names and selectors are untouched; each rename records renamed_from so the
provenance chain to the retired Chronicle profile survives.
The two accounting blocks partition rather than merge: registry_parity keeps
its closure over the 189 national ids, profile_parity over the 25 local ids,
and a new test asserts every target is accounted by exactly one block.
The committed pair and receipt regenerate names-only - normalized payloads are
equal, with 7,020 name-field diffs in the references, 16 in the membership,
and 3,610 renamed receipt rows (the UC family); the 3,510 uk-data#468 and 389
chronicle#200 citations survive at identical counts. Both generated artifacts'
self-descriptions now name the merged contract (the committed national
references keep their values - regeneration stays held on chronicle#203 - but
their description no longer points at a file that does not exist).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rebase union placed the two local gate entries after the national parity pair in gates.json but after the spine block in the test's expected list; the test asserts file order exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebases the branch onto 58a09bd (#793, the #757 release-cut certification). That merge shipped the executable home our local-surface gates were parked on: the scope comment said they run "when the certification producer lands" - it landed, so the wiring is now obligatory rather than deferred. The release-cut battery runs release-candidate strict, where an evidence_absent gap blocks, and its preflight previously supplied only the national registries. - tools/certify_uk_release_cut.py compiles the local surface at 2025 from the same sha-pinned consumer artifact (through the new packaged load_uk_local_area_crosswalk) and passes it to the battery. - run_uk_release_cut_battery takes local_ledger_registries as a required argument and supplies uk_ledger_compiled_local_registries beside the national key; the national battery is now 18 gates, and the scope comment names its landed runner. Rebase resolutions follow the standing recipes: entry tables and gate lists union both sides; every digest re-derives from its live producer (battery policy 89cde234, gates manifest fdb79add, spec fingerprint 93b33063; the UK spec bundle digest is unchanged at 777c40f5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cation pins Adding the two local-surface gates to UK_NATIONAL_GATE_SCOPE moved the release_cut certification part: its frozen scope copy gains the two ids, and its part digests re-derive from the live scoped manifest (gates manifest 5246e8bc, policy ca983461). The battery triplet, part scopes, and part digests now all agree with their producers - the three mirror layers the #793 certification discipline checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fences real Maria's ruling on the review round overturns replicate-and-document for the UC family (the A&S row-30 precedent): the fixture now CORRECTS the incumbent's positional misalignment at extraction instead of reproducing it. UC counts join to their areas by name -- the join the incumbent never does -- attach after the boundary mapping on native PCON24/LAD codes, and the child splits recompute from the corrected totals with the incumbent's own country-proportion buckets keyed by each constituency's true country. Every roster area must resolve (650/650 and 360/360 matched) or the extraction refuses. The UC parity rows become interpretable for the first time and split into two ruled classes: 982 totals rows are the incumbent's uniform national rescale (fixture/ours 0.8925-0.9003), and 2,525 child-split rows are an imputation-vs-published class -- the incumbent imputes splits from GB country shares, ours are the published per-area buckets, and the 0.77-1.19 scatter is the real geographic variation the imputation flattens. Three rows now match exactly (receipt 23,837 -> 23,834 differences). The rest of the review lands alongside: - extractor code/length assertions on every positionally-consumed source (finding 2), with the code-keyed families asserted against their rosters; - the crosswalk's boundary vintages become operative on the compile path (closing note): a matched fact on a non-equivalent frame fails the compile by name. Publisher-equivalent frames are declared accept-sets -- ONS lists devolved areas on its lad_2023 lookup over boundaries unchanged since ca_2019/lgd_2014, which the first live run of this check surfaced; - geography_levels closure test plus a runtime warning when a geography-less target defaults to the national surface (finding 3, per the ruling: the default is doctrine, the omission is what must be loud); - the national target_surface parameter forward restored (finding 4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three new name-join zips gain strict=True — an assertion these joins genuinely want (a length mismatch is exactly the class they exist to refuse). Re-extraction confirms byte-identical fixture rows. The errors were caught locally but masked by piping ruff through tail, which swallowed its exit code; verification commands now keep ruff last-in-chain unpiped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…join directions Vahid's re-review endorsed the corrective posture and found four gaps of the same shape one layer in - guards that silently skip. All four close: 1. The corrected child splits now allocate by largest remainder and assert closure per area: the four buckets sum exactly to the corrected total (the independent rounding drifted up to +/-2 households). Re-extraction moved exactly 225 split rows by at most 1 household; nothing else changed, and the fixture now proves the parts-sum-to-whole property about itself. 2. A positionally-consumed source without a 'code' column refuses instead of degrading to a length-only check that any right-sized permutation passes. 3. The vintage gate's three silent escapes are refusals: an unstamped fact, an unknown code prefix, and a level with no declared vintage all fail by name - the gate exists to prove the frame, and a fact that declines to say which frame it is on is the case it most needs to catch. 4. The name join asserts totality in both directions (unmatched publisher rows fail with their names listed), and a normalization collision is distinguished from a true duplicate in the error text. Also rebases onto main 4b133a8 (#805 and the envelope-pin re-cut train) with the UK spec pin re-derived, so the fixture regeneration ran once on the final base. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r-less levels The round-2 closure assertion compared the allocated buckets against a locally-rounded copy of the total, so it could not fail on its own construction while the fixture stores the unrounded publisher count. It now asserts against the stored value itself: correct today because every total is integral, and refusing loudly the day a publisher ships one that is not. Re-extraction is byte-identical. The authoring core refuses a declared geography level the roster does not carry, instead of skipping it into a not_applicable membership status that reads as nothing-wrong. The refusal is safe under the merged population contract because the local generator already pre-filters targets to roster-covered levels; regeneration of the real surface is byte-identical at 17,077 active. The 360-vs-361 question resolves by name: the incumbent's LA roster carries N09000001-N09000010 only, omitting N09000011 (Newry, Mourne and Down) entirely. The ledger_only rationale now states the confirmed roster gap by code and district rather than describing it generically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The committed local reference pair, parity fixture, and signed-differences
receipts added ~40MB of declared-JSON resources to the UK bundle, and
load_bundle was routing every resource kind through the pure-Python YAML 1.2
scanner: load_country_spec('uk') went from 4.5s on main to 37.6s, which
multiplied across every CI lane that loads the UK spec (engine-uk hit the
180-minute job timeout on an earlier head).
Resources declared legacy_json now parse with json.loads via load_json_strict,
which keeps the load_yaml12 value model and refusals: duplicate mapping keys
and NaN/Infinity constants are rejected explicitly, and JSON grammar already
guarantees a single document with string keys and no tags or aliases. Spec
digests do not move — legacy_json resources contribute byte receipts only,
never normative projections. All 70 existing legacy_json resources across both
country bundles are strict JSON; load_country_spec('uk') is back to 3.7s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74a6e6f to
cf70c1b
Compare
…without the ladder The vintage accept-set revision (constituency ['pcon_2024']; per-nation LA lists with the ONS lad_2023 stamps for unchanged devolved boundaries) updated the committed crosswalk and the runtime enforcement tests, but this pin test still asserted the old scalar shape and was not re-run in the tiered local sweeps. The generator-comparison test also imports the sha-pinned OA ladder npz, which is git-ignored and never present in CI; it now skips there with the suite's artifact-not-mounted convention and keeps running on machines that mount the artifact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes #708, closes #759.
Migrates the last two UK calibration-target contracts out of Chronicle per the
chronicle#166 ruling, and compiles the UK
local target surface from the wave-3 facts — the area-grain analog of #622, and the missing link
in the #495 local lane.
One PR for both issues by adjudication: #708's selector corrections are only proven once the local
facts compile, so the corrections and their compilation land together. The commit ladder keeps #708's
material first.
What lands
build/uk/with signed selector corrections, consumer re-pointed; plus the shared machinery the compile needs: therecord_set_spec_idselector key, the declaredcount_x_meanoperation, the local-area crosswalk, roster-driven area authoring, the generation CLI,country_specloading andcompile_uk_local_target_registryuk_national_targets.json+uk_local_geography_targets.jsonmerge intouk_population_targets.json(all 214 targets kept at their declared levels, per-targetgeography_levels; firms stay separate); local UC ids consolidate intodwp.uc.*asdwp.uc.households_by_area*, each recordingrenamed_fromso the provenance chain to the retired Chronicle profile survives; the two accounting blocks partition rather than merge (registry_parity×189,profile_parity×25, partition-tested); the committed pair and receipt regenerate names-only — normalized payloads equal, citation counts identicaluk_ledger_compiled_local_registriesbeside the national key (the release-cut battery is 18 gates)target_surfaceparameter forwardNational regeneration: held, with the reason on the record
The plan's final step was to regenerate the national references at the refreshed feed. The dry run
came back worse (408 → 389 active) for two upstream causes, both diagnosed and both the
fail-closed machinery doing its job: Chronicle renamed the national UC-by-children
source_conceptbetween feed vintages (breaking 10 selectors), and the new council-tax nation-total facts now also
match the 9
scotgov.council_tax_stockselectors (multi_fact). Committing that would have degradedthe national surface, so the committed national references keep their values and the refresh follows
chronicle#203 plus the selector updates in the lane that owns the national contract. The regenerated
evidence is preserved for that work.
The local surface
17,077 active references of 18,631 candidates over 650 constituencies and 361 local authorities
(25 contract targets × their declared levels), compiled from a sha-pinned Chronicle artifact
(108,112 rows,
facts_sha256 4395a4e7…, Chronicle main33ca98a).Every absence is signed against measured feed coverage, never waved through:
2026-06, after the 2025 target periodA defect this compile exposed on our own side
The first compile pass showed the corrected age selectors still pinned
source_name: "ons", whichexcludes the NRS and NISRA publisher legs — the original Chronicle profile's central bug, carried one
step further. Dropping the pin resolves all 8 bands across England, Scotland, Wales and Northern
Ireland. This is exactly what compiling against real facts is for.
Parity gates
Two release-blocking preflight gates:
uk_ledger_compile_parity_local_incumbent_2025— value parity against a fixture extracted frompolicyengine-uk-data@12a1e028by replaying both local loss builders' y-sides (23,545 rows, 38metrics), recording raw and calibrated values per row with each family's scaling factor so a
difference can be attributed to the source statistic or to the incumbent's solve-time scaling.
uk_target_surface_local_default_2025— the structural check: the in-codemetric_names(area_type)surface fanned over the crosswalk roster, reconciled against the compiled registry. No values are
synthesized, because that surface has none.
All 23,834 receipt rows carry a ruled rationale; none keeps the classifier's generic text.
Two upstream findings, both filed
policyengine-uk-data#468 — the
incumbent's local UC targets are positionally misaligned, and the fixture fix-and-signs it. Name-only
spreadsheet rows are joined by row position to code files in a different order: 8 of 360 local
authorities land correctly, and 0 of 650 constituencies (the UC vector is PCON24-native yet paired
with 2010-ordered rows and then boundary-mapped on top). Birmingham calibrates to West Oxfordshire's
count, Hackney to the Isles of Scilly's. Verified by name-join, which reproduces the same Stat-Xplore
publication our facts carry at the incumbent's uniform national rescale — same data, permuted.
Per the reviewer's ruling (and the A&S row-30 precedent: fix-and-sign replaces
replicate-and-document), the fixture corrects the misalignment at extraction: UC counts join to
their areas by name, attach post-mapping on native PCON24/LAD codes, and the child splits recompute
from corrected totals with the incumbent's own country-proportion buckets keyed by true country. The
UC parity rows become interpretable for the first time, and split into two ruled classes: the 982
totals rows are the incumbent's uniform national rescale (fixture/ours 0.8925–0.9003), and the 2,525
child-split rows are an imputation-vs-published class — the incumbent imputes splits from GB country
shares while ours are the published per-area buckets, so the scatter (0.77–1.19) is the real
geographic variation in family size the imputation flattens. Three rows now match exactly.
The extraction refuses unresolved joins (650/650 and 360/360 areas must match by name), and the
crosswalk's declared boundary vintages are now operative on the compile path: a matched fact on a
non-equivalent boundary frame fails the compile by name, with equivalent publisher frames (ONS lists
devolved areas on its lad_2023 lookup over unchanged boundaries) declared as accept-sets.
chronicle#200 — two evidenced fact asks.
(1) Emit PIPR 2025: the package already preserves the whole sheet (49,266 source rows, 4,284/year for
2015–2025) and only
selected_rowsnarrows fact emission to 2026-06; calendar-2025 carries all 12months with 316/316 E&W local authorities, so widening it closes 314 of the 361 rent deferrals with no
new acquisition. (2) Port NI Universal Credit from the DfC May-2025 supplementary tables the incumbent
already binds, closing the 29 NI totals.
Incumbent left-behind audit
The incumbent binds 38 metric columns, not 25. The 13 outside our contract are signed exclusions
with ruled rationales: 4 devolved private-rent columns whose y-values are hardcoded constants in
devolved_housing.pywith no source or year, and 9 council-tax columns the contract does not declare(the wave-3 facts exist; the microcosm-side family work is recorded as follow-up scope).
Also confirmed by the audit: the NOMIS ASHE workbook feeds no loss matrix, the SPI
pension/total-income columns never bind, and there is no age target above 79.
Adjudications recorded
Selector corrections land here as fix-and-sign with a
profile_parityaccounting block; one sharedfeed refresh with the national movement signed in this PR; SPI amounts bind through a declared
operation rather than deferring; the committed reference pair keeps the literal national row shape
(12 MB references + 11 MB membership, measured and accepted, rather than a compact encoding).
Deferred, deliberately
The national-surface refresh is held, with its cause filed. Regenerating the national references
against the current feed was attempted and came back worse (408 → 389 active): Chronicle's
April–December UC packages renamed the family's
source_conceptstrings, and the new SLGFScouncil-tax yield rows reuse the ctaxbase band measure ids, so 19 targets defer — correctly, under
the fail-closed compile. Both causes are upstream and filed as
chronicle#203; the refresh follows once it
lands and a fresh feed is staged. The committed national references are untouched by this PR. (The
local surface is unaffected — the per-area packages kept their vocabulary, which is how its 17,077
references compile on this same feed.)
The support oracle (#495 increment 4's measurement half) needs licensed microdata and is not in
this PR. chronicle#172 retires the
Chronicle-side profile surface after this merges — that sequencing is unchanged, and it is why the
consumer adoption lands first.
🤖 Generated with Claude Code