Skip to content

UK local target activation: uk-data parity register, masking doctrine, council tax, PIPR measurements (#147 PR 2) - #831

Merged
juaristi22 merged 7 commits into
mainfrom
uk-local-target-activation-147
Sep 2, 2026
Merged

UK local target activation: uk-data parity register, masking doctrine, council tax, PIPR measurements (#147 PR 2)#831
juaristi22 merged 7 commits into
mainfrom
uk-local-target-activation-147

Conversation

@juaristi22

@juaristi22 juaristi22 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Second of two PRs for #147 (plan: repos/uk-147-local-credibility-plan.md) — the target activation half. Stacked on #827 (uk-cross-grain-reconciliation-802) to consume its cross-grain machinery; retarget to main when #827 merges. The gate-tooling half is #830. Closes #147.

What this does

  • I1 — the "no gaps" parity register: build/uk/uk_data_target_parity.json — 78 concerns, each declaring which entries of uk_data_target_inventory.json it covers. The inventory is a sha-stamped snapshot of the incumbent's 54 target-bearing modules at 8629dbb, extracted statically by tools/extract_uk_data_target_inventory.py (every targets/sources/*.py and local_areas/*/targets/*.py globbed; anchors and credibility tests listed). CI enforces a bijection between concerns and the committed snapshot in both directions, so an uninventoried or uncovered module fails the register by name. The per-file hashes are re-verified against a checkout by verify_uk_data_target_inventory_against_tree (env-gated test, MICROCOSM_UK_DATA_TREE), not continuously — 'uk-data moved' is caught on re-extraction. Enumerated against uk-data's origin tip 8629dbb (including post-checkout additions: uk-data#458 UC caseload targets, uk-data#474 DfE corrections), each ported_national (17) / ported_local_declared (37) / routed (18) / blocked_source (2) / reviewed_exclusion (4), every non-ported row carrying a Chesterton fence {origin, purpose, verdict_basis}, the 25 local contract targets classified red-line with a presence drift-test. Census-pattern tool (tools/census_uk_data_target_parity.py --check) + resource registration. Routing follows the session rulings: bus/rail → WS-E/F (Decide UK panel, public transfer, and other product-scope leftovers from uk-data #148), NIC Class-3 doctrine (uk-data#88), regional land → A5 (uk-data#357 unmerged, chronicle#205 pending), mortgage → uk-data#453 blocked source, Band D £ → non_linear, devolved rent constituency anchors → never ported (fabrication doctrine).
  • I2 — masking doctrine: the local census gains a doctrine block recording the enforced semantics — AreaSignedDeferral is the deliberate compile-time mask (unsigned absence raises, stale deferrals raise), the solve surface is dense-and-finite by design, zero targets are legal and intentional, and no local target is ever fabricated by allocating national/country totals across areas (the incumbent's devolved_housing.py recorded as the cautionary non-port). Includes the Port UK local target surface and credibility gates from uk-data #147 acceptance-criteria → mechanism mapping.
  • I5 — council tax LA surface, to "bindable, adjudication pending": eight append-only band metrics, value-free contract rows on the wave-3 VOA record sets — 2,541 cells active (A 317, B–G 318 each, H 316), 347 measured signed deferrals (Scotland 256, NI 88, City-of-London band A, Welsh band H), the new voa_dwellings_vs_household_frame fence, and the country-over-LA collision declared through Declare cross-grain target reconciliation for the UK local surface: national wins, sub-national rescales (#495 increment 6 companion) #802's CrossGrainRule/CrossGrainBridge (Wales + Scotland legs). England's national council-tax control sits at region grain, which the Declare cross-grain target reconciliation for the UK local surface: national wins, sub-national rescales (#495 increment 6 companion) #802 precedence vocabulary (country>constituency>la) cannot yet express — recorded as a documented machinery gap in the register, not invented around.
  • I6 — PIPR rent, measured: the pinned feed carries only 2026-06 PIPR observations (348 facts: 316 LA + 18 Scottish BRMA + region/country rows) — no history at or before the 2025 target period — so all 314 E&W cells stay deferred with the measurement attached rather than forward-dating targets. Scotland stays at BRMA grain pending a signed BRMA→LA bridge; NI absent. The deferral wording is re-signed from these measurements (A4).
  • I7 (register only): build/uk/local_validation_levels.json — nine report-only benchmark rows with in_sample flags; five feed-backed now (incl. the per-country council-tax checks aimed at uk-data#448), four awaiting_facts (rent country totals, Welsh UC country benchmark, regional PSE). Scorer wiring stays with UK local candidate gate battery, release diagnostics, and scorer (#147 PR 1) #830's tooling.

Surface after regeneration: 19,618 active references, 1,901 signed deferrals (byte-stable rerun). local_binding_adjudications.json untouched — nothing binds here; solve-binding remains an adjudication event.

Verification (run independently)

  • ruff clean; both census --check tools current; explicit suite 329 passed; broad -k selector 265 passed, 5 skipped; ci_test_groups.py --verify ok; zero policyengine-uk-data# citations.
  • One deselected failure is pre-existing and unrelated: test_uk_parity_reference.py::TestEfrsParityReference::test_cached_reference_regeneration_matches_committed_surface fails identically on the clean Declare cross-grain target reconciliation for the UK local surface: national wins, sub-national rescales (#495 increment 6 companion) #802 base — the locked policyengine-uk now exposes bus_fare_spending/employment_sector/sic_industry_division, which the committed eFRS parity reference predates. Flagged separately for its own fix.

María sign-off carried in this review round

Refs #147, #495, #665, #827. Not for merge without María's explicit go.

🤖 Generated with Claude Code

@juaristi22
juaristi22 force-pushed the uk-local-target-activation-147 branch from 0b1c2c2 to 74c2330 Compare September 1, 2026 10:28
@juaristi22
juaristi22 changed the base branch from uk-cross-grain-reconciliation-802 to main September 1, 2026 10:28
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Automated review pass (Claude Code, high effort, diff plus branch reads — no execution). Draft, so nits are skipped. Three findings; the first is structural and I would want it settled before this leaves draft.

1. uk_runtime/data_target_parity.py:468 — the parity register cannot fail for any local target

_local_contract_rows() generates every local parity row from Microcosm's own uk_population_targets.json, and stamps each one status="ported_local_declared" / classification="red_line_local_contract_target" unconditionally. Nothing in the module ever reads uk-data.

So the register is a restatement of the Microcosm contract rather than a comparison against the thing it claims parity with. Two consequences:

  • A uk-data local family that Microcosm never ported produces no row at all — silent absence rather than a declared gap, which is the one outcome a parity register exists to make impossible.
  • test_red_line_rows_are_present's len(local_rows) >= 25 plus all(status.startswith("ported_local_")) is entailed by construction: both assertions are satisfied by the generator regardless of what uk-data contains.

The sharpest version of the problem: this register would report full local parity in the presence of uk-data#468's misaligned constituency and LA UC vectors. That issue is still open — 0 of 650 constituencies and 352 of 360 local authorities currently receive another area's UC count — so "parity" here is being asserted against a reference nobody has compared against, and the register's green state carries no information about the misalignment at all.

The only actual uk-data evidence in the change is a free-text citation of an untracked .codex-work/REFERENCE.md tree, which no test or CI path can verify. #795 hit the adjacent version of this and resolved it well — the fixture there stopped replicating uk-data and started correcting #468 at extraction, with the name join asserted total in both directions. Whatever the right call is here (read uk-data and compare, or declare explicitly that this register is contract-side only and rename it accordingly), the current shape lets a green register stand in for a comparison that never ran.

Worth noting the related roster fact while you are in this area: uk-data's local_authorities_2021.csv omits N09000011 (Newry, Mourne and Down) entirely, so it carries 360 LAs where the crosswalk declares 361. Any parity assertion against that file needs to handle the gap explicitly rather than absorb it.

2. tools/generate_uk_local_target_references.py:167 — the deferral masks rot silently

The new council-tax deferral area sets are filtered with if area_id in local_authority_ids and passed to add(), which is a no-op when area_ids is empty or when the target_id is absent from the contract.

So a roster change or a target rename makes the City-of-London band-A and Wales band-H masks — and, through the same path, the 32 Scottish and 11 NI band masks — disappear with no diagnostic. The "347 signed absent cells" figure asserted in the census notes and pinned by test_council_tax_signed_deferrals_pin_exact_gaps then silently shrinks rather than refusing, and the test agrees with the smaller number because it recounts what was generated.

An assertion that every declared mask matched something, with the unmatched ones named, is the shape that has worked elsewhere in this lane.

3. tools/build_uk_ledger_compile_parity_signed_differences.py:378 — narrowed branches drop rows out of signed coverage

The replacement branches narrow council-tax band coverage from "any kind" to kind == "calibration_drift" or kind == "fixture_only". A voa/council_tax/* or council_tax/band_* row with any other kind — the 292 ledger_only rows, for instance — now falls through every council-tax branch and is emitted without a signed rationale, where the old _COUNCIL_TAX_METRICS branch covered it unconditionally.

If the narrowing is deliberate, the uncovered kinds presumably need their own branch or an explicit exclusion; as written they just stop being signed.


1 is the one that matters. 2 and 3 are both "a register or a coverage rule that quietly shrinks" — the same class, and both cheap to close with a matched-everything assertion.

@juaristi22

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (74c2330a) now that #827 has merged, and the PR base is retargeted from uk-cross-grain-reconciliation-802 to main — the diff now reads standalone and GitHub reports it mergeable.

The rebase was clean (no conflicts), but it did pull in #802's round-2 commit 988f34db ("scope the sign and order rules correctly"), which revises the cross-grain machinery this PR's council-tax collision declaration consumes. Re-ran the affected lanes against the revised rules: 66 passed across test_cross_grain, test_uk_council_tax_targets, test_uk_ledger_targets, test_uk_data_target_parity, test_uk_pipr_deferrals, test_uk_local_validation_levels, and test_uk_local_target_census.

The England region-grain gap noted in the description still stands as a documented parity-register row: #802's precedence vocabulary is country > constituency > la, and England's national council-tax control is declared at region grain, so the collision cannot be expressed as a rule yet. Worth deciding whether the precedence grows a region rung — that is an #802-side question, not something to invent here.

…s, doctrine, council tax, PIPR measurements (#147)

I1: uk_data_target_parity.json — 78 concerns enumerated against the uk-data
origin tip (8629dbb), every non-ported row fenced {origin, purpose,
verdict_basis}, red-line local contract targets classified, census-pattern
tool + drift gate. I2: the census gains a doctrine block recording the
masking semantics (AreaSignedDeferral is the deliberate compile-time mask,
zeros are legal, fabrication by allocation is forbidden) and the #147
acceptance-criteria mapping. I5: eight LA council-tax band metrics appended,
value-free contract rows on the wave-3 VOA facts — 2,541 cells active, 347
measured signed deferrals (Scotland 256, NI 88, City of London band A,
Welsh band H), the voa_dwellings_vs_household_frame fence declared, and the
country-over-LA collision expressed through the #802 cross-grain rule
(England's region-grain control recorded as a machinery gap, not invented).
I6: the four PIPR deferrals re-signed from measurement — the pinned feed
carries only 2026-06 observations, so all 314 E&W cells stay deferred
rather than forward-dating 2025 targets. I7: local_validation_levels.json —
nine report-only benchmark rows, five feed-backed, four awaiting facts.
Surface: 19,618 active references, 1,901 signed deferrals, byte-stable
regeneration. Binding adjudications untouched: everything lands as
"bindable, adjudication pending."

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Rebased onto bc43f2f0 (#829 merged). Clean rebase, no conflicts; ruff clean and the target-activation lane is green — 81 passed across the parity register, council-tax, PIPR, validation-levels, census, cross-grain, ledger-targets, and population-targets suites.

No review findings outstanding on this PR; still awaiting review.

@juaristi22
juaristi22 force-pushed the uk-local-target-activation-147 branch from 74c2330 to b09860d Compare September 1, 2026 11:50
@juaristi22
juaristi22 marked this pull request as ready for review September 1, 2026 11:55
juaristi22 and others added 3 commits September 1, 2026 14:21
The parity register's DfE evidence string mixed both conventions in one
line: the first path used the sanctioned "uk-data ..." prefix and the
second slipped into the package directory name, which the live-tree guard
in test_us_plan.py forbids outright. That guard runs in the US lane, so a
UK-scoped local run never executes it and the string reached CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…147)

Registering uk_data_target_parity.json and local_validation_levels.json in
country_package.json moves the UK bundle's spec_sha256, which is pinned in
test_spec_engine_country_bundles.py. That test runs in the fast `rest`
group, so a UK-scoped local run never executes it. Re-pinned from
load_bundle("uk").spec_sha256, the sanctioned spec-identity re-pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#147)

The council-tax band metrics are appended after "households", which is what
the positional-stability rule requires: every metric already in the surface
keeps its index, households included. Moving them ahead of households to
satisfy the old assertion would have renumbered households itself and broken
the very invariant the assertion was written to protect.

That assertion — metric_names(area_type)[-1] == "households" — was a proxy
that only held while households was the newest metric. It is replaced with
the property it stood for: the committed prefix through households is pinned
exactly for both grains, so an insertion or reorder fails while an append
passes. The comment in local_targets.py no longer claims households is last.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Second pass at eaf3ad43 (Claude Code, high effort; diff via gh pr diff, generated-JSON and fixture hunks skipped, no execution).

Taking the three prior findings first, since the substance of the round turns on the first one.

1. Still open — the register remains entailed by construction

uk_runtime/data_target_parity.py:474 is unchanged in substance. _local_contract_rows() still iterates load_uk_population_contract()["targets"] — Microcosm's own uk_population_targets.json — and stamps status="ported_local_declared" / classification="red_line_local_contract_target" unconditionally. Its evidence string still points at the untracked .codex-work/REFERENCE.md.

I looked specifically at whether the PR body's "78 concerns enumerated against uk-data's origin tip 8629dbb" makes this checkable, and it does not:

  • _REFERENCE declares incumbent_tree=".codex-work/uk-data-8629dbb", incumbent_commit="8629dbb" and verified_on="2026-08-31" as literals. Nothing dereferences them.
  • The 78 concerns are the hand-written _CONCERNS tuple. _validate_rows() checks only that concern_id / classification / evidence are present, plus reason and a 3-key fence on non-ported rows — i.e. that hand-maintained text is non-empty.
  • assert_uk_data_target_parity_current() compares the committed JSON against build_…(). So CI verifies that the file matches the literals; it cannot verify that the literals match uk-data.
  • Neither the module nor the new tools/census_uk_data_target_parity.py (which only calls write_ / assert_…_current) reads uk-data or any pinned extract of it.

So a uk-data concern absent from _CONCERNS is still undetectable, and the register still reports full local parity while uk-data#468's misaligned constituency and LA UC vectors are live. The enumeration is a careful, honest inventory — I am not disputing its accuracy — but its correctness rests on the care taken on 2026-08-31 and nothing re-establishes it afterwards, which is what "parity register" implies to a later reader.

The cheapest thing that would change the character of it: commit a pinned extract of the uk-data target surface (or its manifest of concern ids) and have assert_…_current diff _CONCERNS against that, failing on either side's unmatched entries. Then a uk-data addition shows up as a register failure rather than as silence, which is the property #795 ended up with when its fixture stopped replicating uk-data and started deriving from it.

2. Still open, and now with more callers

tools/generate_uk_local_target_references.py:167 — the if area_id in local_authority_ids filters are retained and extended to the new council-tax masks: council_tax_city_band_a_area_ids over the single-element ("E09000001",), and council_tax_wales_band_h_area_ids over ("W06000019", "W06000024"). add() is untouched by the diff, so it is still a silent no-op on an empty area_ids or an unknown target_id.

With single-element and two-element literal tuples the exposure is concrete: if the crosswalk ever drops City of London or either Welsh authority, or the band target ids are renamed, those masks disappear with no diagnostic — and the new tests recount what was generated, so they agree with the smaller number.

3. Half closed

housing/council_tax_net now routes through an unconditional elif metric == "housing/council_tax_net", which fixes that metric. But the band metrics are still gated on kind == "calibration_drift" / kind == "fixture_only", so band rows of any other kind — notably the ~292 ledger_only — still fall through the chain and are emitted with no signed rationale. The narrowing survives for the band family.

4. New: the NI and Scotland band rationales assert a roster the reference does not carry

tools/generate_uk_local_target_references.py:271 — the NI and Scotland band masks are derived from local_authority_ids, while the rationales assert "11 Northern Ireland local-government districts" and "32 Scottish authorities" as prose.

uk-data's local_authorities_2021.csv carries N09000001N09000010 and omits N09000011 (Newry, Mourne and Down) entirely — 360 authorities against the crosswalk's 361. Nothing in the diff reconciles the two rosters, so a missing NI authority narrows the mask silently, and the 361-based assertions are tested only against Microcosm's own crosswalk. Given finding 1 leaves uk-data unread, this is the same gap showing up in the place where a specific count is claimed in prose.


1 is the one that matters, and it is the same question as last round rather than a new one: everything else here is a well-built inventory, and the inventory is exactly what cannot currently be checked. 2 and 4 are both cheap matched-everything assertions. 3 needs the band branch to cover its remaining kinds or exclude them explicitly.

…147)

The register was entailed by construction: local rows were generated from
Microcosm's own uk_population_targets.json and stamped ported_local_declared
unconditionally, the 78 concerns were hand-written literals, and the
incumbent commit and verification date were strings nothing dereferenced.
Several evidence strings cited .codex-work/REFERENCE.md, which is untracked.
So CI verified that the artifact matched the literals, never that the
literals matched uk-data, and a family that was never ported produced
silence rather than a declared gap.

uk_data_target_inventory.json is now a committed, sha-pinned static extract
of the incumbent's 50 target-bearing modules at 8629dbb, produced by
tools/extract_uk_data_target_inventory.py without importing the package.
Every concern declares the inventory entries it covers, and the register
assertion requires a bijection in both directions, so an unported uk-data
family fails the register by name. The untracked citations are gone.

Two masks that could shrink in silence now refuse: signed-deferral
registration raises on an unknown target id or a roster that filters to
empty (naming the unmatched ids, with an explicit allow_empty opt-out), and
the Scottish and Northern Irish council-tax masks assert the 32 and 11
crosswalk authorities their rationales claim, reporting the measured count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Findings 1, 2 and 4 are fixed in 1be93efd. Finding 3 I dispute — measurements below.

Apologies for the first pass going unanswered; I was firefighting this PR's CI and missed it. Both rounds are addressed here.

1 — the register is now checkable against a pinned extract

You were right, and right that it was the finding that mattered: the register verified that the artifact matched the literals, never that the literals matched uk-data. A family we never ported produced silence, which is the one outcome a parity register exists to prevent — and the evidence pointed at an untracked file.

Implemented as you suggested, in the #795 shape:

  • uk_data_target_inventory.json — a committed, sha-pinned static extract of the incumbent's target-bearing surface at 8629dbb: 50 entries (23 target_source, 15 credibility_gate, 4 local_target_producer, 3 dataset_anchor, 3 helper, 2 local_matrix), each with its path and file sha256, produced by tools/extract_uk_data_target_inventory.py by parsing the tree — nothing imports the package.
  • Every concern declares covers, and assert_uk_data_target_parity_current() requires a bijection in both directions: an inventory entry no concern covers fails, and a covers id absent from the inventory fails. Helper modules are exempt only through a named constant, not by omission.
  • The .codex-work/REFERENCE.md citations are gone — evidence now names the committed inventory and the concrete incumbent path.

Two things I verified rather than took on trust:

  • The extract is real. I re-hashed all 50 entries against the archived tree: 50/50 match byte-for-byte.
  • The bijection bites. Injecting a fake targets/sources/brand_new_target_family into the inventory produces UK data-target inventory entry id(s) have no parity concern: ['…brand_new_target_family']. So a uk-data addition now shows up as a register failure rather than as silence — the property you asked for.

Registering the resource moved the UK bundle digest again; re-pinned to 0733a81e… from load_bundle("uk").spec_sha256.

2 — the masks refuse instead of shrinking

add() was if target_id in target_ids and area_ids:. It now raises on an unknown target id and on a declared roster that filters to empty, naming the unmatched ids, with an explicit allow_empty=False opt-out at the call site rather than silence. The City-of-London and Welsh band-H literals are no longer pre-filtered, so a roster change trips the refusal instead of quietly producing a smaller mask that the pinning tests then agree with.

3 — I don't think this one holds; every row is signed

The claim is that band rows of other kinds, "notably the ~292 ledger_only", are emitted without a signed rationale. Measured on the committed artifact:

  • the kind vocabulary is exactly three values — calibration_drift 17,957, fixture_only 4,219, ledger_only 292;
  • all 1,493 council-tax rows are calibration_drift (1,175) or fixture_only (318) — none are ledger_only, so the two narrowed branches cover every council-tax row that exists;
  • the 292 ledger_only rows are not council-tax rows, and the chain ends in an unconditional elif kind == "ledger_only" that signs them;
  • 0 of 22,468 rows lack a reason.

So the narrowing drops nothing: for a band row to fall through it would need a fourth kind, and there isn't one. If you'd rather the band branches covered ledger_only explicitly anyway — belt and braces against a future kind — say so and I'll add it, but I didn't want to change working coverage on a hypothesis.

4 — the counts are asserted where the masks are built

Half right, and the half that is right is now closed: the Scottish and NI masks assert exactly 32 and 11 crosswalk authorities, refusing with the measured count, so prose and roster cannot drift apart.

On the uk-data roster framing, I think this one doesn't transfer: these masks are about the pinned chronicle feed's coverage of the Microcosm crosswalk, not about uk-data's roster — the Scotland rationale says "crosswalk" explicitly. Verified: the crosswalk carries 361 local authorities, E296 / S32 / W22 / N11, N09000011 included. uk-data's 360-row local_authorities_2021.csv omitting Newry, Mourne and Down is a real fact about a different surface; it is now recorded in the inventory rather than folded into these masks.

One interaction worth flagging: the new count assertion runs before the per-deferral checks, so the CLI test whose fixture carried no local-authority level at all began failing on the roster guard instead of the contract refusal it exists to test. The guard is correct there — you cannot sign "all 32 Scottish authorities" against an empty roster — so I gave that fixture a roster the masks accept and left the guard alone. Both guards have their own regressions (test_council_tax_country_masks_refuse_roster_count_drift, test_declared_deferral_roster_matching_no_crosswalk_area_refuses).

Verification

ruff clean; both censuses --check current; reference and membership artifacts byte-stable across regeneration (19,618 active); target lane 102 passed; spec/country-spec 152 passed; ci_test_groups.py --verify ok; the incumbent-name guard passes, with the two new JSON artifacts added to its allow-list (they must carry real module paths, same posture as #795's extractor).

The spec-only country-package guard forbids executable-looking keys, and
"entry" is one of its forbidden tokens, so every inventory row tripped
test__given_country_specs__then_no_python_entrypoints_are_declared once the
artifact was registered as a uk/ resource. It is the key name that offends,
not the values: the entrypoint heuristic needs a module:callable colon, and
the ids carry none. Renamed the field across the extractor, the register,
the tests, and the regenerated artifact.

This is what failed engine-shared, fast/rest and wheels; the UK bundle
digest is unchanged, since it covers resource registration rather than
legacy_json contents.

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

Copy link
Copy Markdown
Contributor

Third pass at 19d7ee56 (Claude Code, high effort; branch checkout, targeted suites run, inventory shas re-hashed against the uk-data tree at 8629dbb). Thanks for the thorough reply — taking the four findings in order, then two new items.

1. Substantially closed; two residuals worth stating

The fix is the one I asked for and it changes the character of the register: _assert_inventory_bijection refuses in both directions (data_target_parity.py:786-790, :811-817), exemptions must exist and be helper-kinded, and the two new tests exercise both sides. I re-hashed a sample of inventory entries against the raw files at 8629dbb and they match. The 19d7ee56 rename is semantically neutral and the ids stay derived from the path at both ends.

Two things keep it short of "checkable against a pinned extract" as the description now claims:

  • The extractor's scope is itself a hand list outside targets/sources/. tools/extract_uk_data_target_inventory.py:17-58 globs targets/sources/*.py, but LOCAL_TARGET_MODULES, DATASET_ANCHOR_MODULES and CREDIBILITY_GATE_MODULES are enumerated by hand, so the bijection can only bite on the globbed directory. Concrete case at 8629dbb: uk-data carries datasets/local_areas/local_authorities/targets/{create_employment_incomes,create_total_incomes,fill_missing_age_demographics}.py, the LA twins of the three constituency scripts the inventory does list as local_target_producer and that _local_contract_covers (:593-613) cites for the hmrc.* and ons.age.* rows. Neither set is imported by any Python module in the tree (both read local CSVs as standalone scripts), so the asymmetry is an inventory-scope accident rather than a judgement — but it is precisely the "silent absence" the register exists to prevent, showing up one directory over. Globbing datasets/local_areas/*/targets/*.py (or listing the LA three with the same fence the constituency three carry) closes it.
  • The per-file sha256s are recorded but never read. No code in data_target_parity.py consumes entry["sha256"]; setting an entry's hash to all zeros and extracted_on to 1999 still passes assert_uk_data_target_parity_current. And incumbent_commit is a literal compared to a literal (:747 against _REFERENCE). So --check catches "forgot to regenerate" and "hand-edited JSON", not "uk-data moved". That is fine if it is what the description says; as written the description implies more. Either wire the hashes into something (even a test that re-hashes when a UK_DATA_TREE env var is set) or word it as a manually re-extracted, sha-stamped snapshot.

2. Verified fixed

generate_uk_local_target_references.py:218-233 raises on an unknown target id and on a roster that filters to empty, naming the ids; allow_empty is used only at its definition and one call. Both regressions exercise the refusal.

3. Withdrawn — your measurement is right, with one footnote

Reproduced: kinds are {calibration_drift 17957, fixture_only 4219, ledger_only 292}, all 1,493 council-tax rows fall in the two covered kinds, 0 of 22,468 rows lack a reason. The structural reason no row can go unsigned lives upstream of the emitter: build/gates.py assigns a generic reason at classification time (the classifier block around :262-323) to every kind, and the emitter's docstring says unruled rows keep it rather than inventing one. Footnote: that classifier vocabulary has a fourth kind, ledger_absent (build/gates.py:270), that the measured artifact happens not to contain — still signed by the generic path, so the conclusion stands. Nit: _COUNCIL_TAX_BAND_FIXTURE_ONLY_RATIONALE (:380-381) is unreachable on current data, since there are zero band fixture_only rows.

4. Verified fixed

:180-189 refuses unless exactly 32 S* and 11 N* crosswalk ids; both new tests drop one id and match the measured count in the message. Agreed that the uk-data 360-row roster is a different surface — recording it in the inventory is the right place.

5. New: the stale-deferral doctrine only runs on a machine that has the feed

The doctrine block says unsigned absence raises and stale deferrals raise, and both are real (build/target_reference_authoring.py, the "candidate now compiles" refusal at :336 and the unsigned-absence refusal just above it). But they fire inside the generator, and the generator's --ledger-facts, which was required=True on main, now defaults to .codex-work/consumer_facts_uk.jsonl (tools/generate_uk_local_target_references.py:85) — an untracked path that also appears in local_target_census.py:83-84, local_validation_levels.json:6 and local_target_reference_membership.json (source_fact_feed). The finding-1 cleanup removed .codex-work from parity evidence, which is all test_uk_data_target_parity.py:107 checks. So in CI nothing can re-run the generator, the PIPR test pins the hand-written prose ("348 facts", "2026-06") rather than the feed, and a feed that later carries PIPR history will not trip anything until someone regenerates locally. Not a correctness defect in this PR — the compiler-derived statuses check out (314 no_fact_at_or_before_period, 47 no_fact_for_area) — but the doctrine block should say "raises on regeneration" rather than imply it is continuously enforced, or the feed pointer should stop being an untracked default.

6. Nit

extract_uk_data_target_inventory.py:123 writes date.today(), so the inventory is not byte-stable across days. Not reachable by --check, but it will churn the artifact on every honest re-extraction.

Checked and clear

Every headline count reproduces from the committed JSON: 78 concerns with the stated split; 2,541 active / 347 deferred council-tax cells with S 256, N 88, City of London band A, and the two Welsh band-H authorities; 314 PIPR deferrals; 19,618 active and 1,901 signed deferrals overall (the 1,901 includes one signed_deferral_compile_error on hmrc.self_employment_income.amount@E14001416, a mean fact missing where the count is present — signed, so correct). The parity register regenerates byte-identical. Region grain is loud, not silent: cross_grain.py:117-121 raises on any grain outside country/constituency/la. No allocation code anywhere in the generator or compiler. ruff clean; 284 tests pass across the touched files locally.


Nothing here blocks. 1's two residuals are the substance: the LA-twin omission is a five-line extractor change, and the description should either use the hashes or claim less for them. 5 is a wording fix to the doctrine unless you want to track the feed.

…feed (#147)

Third review pass on the parity register. The extractor globbed only
targets/sources/ and hand-listed the three constituency producer scripts, so
their local-authority twins under datasets/local_areas/local_authorities/
targets/ were absent from the inventory — the exact silent absence the
register exists to prevent, one directory over. Every local_areas/*/targets/
script is now globbed, the LA twins are covered by the same rows their
constituency counterparts are, and a test pins both grains.

The per-file sha256s were recorded but never read. The loader now validates
their shape, and verify_uk_data_target_inventory_against_tree re-hashes
every entry against a checkout; an env-gated test runs it when
MICROCOSM_UK_DATA_TREE is set. The register's reference block says plainly
what the snapshot is: manually re-extracted and sha-stamped, checked in CI
against the committed snapshot, re-verified against uk-data only on demand.

The generator's --ledger-facts had gained an untracked default; it is
required again, and the census doctrine states that the unsigned-absence and
stale-deferral refusals fire at regeneration, not continuously in CI. The
extractor takes --extracted-on explicitly so an unchanged tree re-extracts
byte-identically instead of churning on the date.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Thanks for re-hashing the sample and reproducing the finding-3 numbers. All of this round is in 0b4b7f5d.

1 — both residuals closed

The LA twins. You're right that this was the register's own failure mode showing up one directory over, and right that it was a scope accident rather than a judgement. The extractor now globs datasets/local_areas/*/targets/*.py instead of hand-listing the constituency three, so any grain's producer scripts land in the inventory and must be covered. The three LA scripts are in (54 entries now, from 50 — plus the constituency targets/__init__.py, classified helper and exempted by name like the targets/sources/ helpers). _local_contract_covers cites each grain's producer for the rows bound at that grain, so an hmrc.* or ons.age.* row at LA grain now covers local_authorities/targets/create_employment_incomes.py etc. alongside the constituency twin, and a test pins both grains.

The hashes. Also right: recorded, never read. Two changes, so the description can claim exactly what is true:

  • verify_uk_data_target_inventory_against_tree(tree) re-hashes every entry against a checkout and refuses on a missing file or digest mismatch, naming each. A test runs it when MICROCOSM_UK_DATA_TREE is set and skips otherwise; I ran it here against the archive at 8629dbb and it passes. The loader now also validates hash shape and extracted_on as an ISO date, and there's a test that a drifted file trips the verifier.
  • The register's _REFERENCE gained an inventory_nature field stating plainly that the snapshot is manually re-extracted and sha-stamped, that CI checks the concerns against the committed snapshot, and that "uk-data moved" is caught on re-extraction, not continuously. The PR body now says the same. I'd rather the artifact undersell than let a reader infer a live tripwire.

2, 4 — noted, thanks for verifying

3 — withdrawn; footnote acknowledged

Good catch on ledger_absent in the classifier vocabulary — I measured the artifact, not the vocabulary, and should have said so. Conclusion holds, as you say, because the generic reason is assigned at classification for every kind. On the nit: _COUNCIL_TAX_BAND_FIXTURE_ONLY_RATIONALE is unreachable on today's data (the 318 fixture_only council-tax rows are all council_tax_net), but the classifier can produce band fixture_only rows, so I've left the branch as a defensive rationale rather than delete a signing path on current-data grounds. Happy to remove it if you'd prefer the code match the data.

5 — the feed is named explicitly again; the doctrine says where it fires

Agreed on both halves. The --ledger-facts default to an untracked .codex-work/ path came in as a compatibility shim in the previous round; it's required=True again with a comment saying why (licensed, untracked, must never silently bind to a stale local copy). The local_target_census.py pin and the membership source_fact_feed still record that path, but as provenance of what was compiled, which is what they are for.

The doctrine block gained an enforcement_point entry: both refusals fire inside the reference compiler, which runs only on regeneration against the pinned feed; CI verifies the committed compile through the census and membership drift gates and cannot re-run the compiler, so a feed that later carries PIPR history is caught at the next regeneration, not continuously. I chose the wording fix over tracking the feed — it's 384 MB of licensed consumer facts.

6 — byte-stable

--extracted-on is now an explicit, required ISO date, so re-extracting an unchanged tree is byte-identical (verified: cmp clean across two runs).

Verification

ruff clean; parity and census --check current; inventory re-extraction byte-identical; env-gated hash test passes against the archive; target lane + spec-only + bundle + country-spec + incumbent-name guard 230 passed, 1 skipped (the skip is the env-gated test without the variable); ci_test_groups.py --verify ok; the incumbent-name sweep of the diff shows only the two allow-listed JSON artifacts.

@juaristi22
juaristi22 merged commit 2233cec into main Sep 2, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port UK local target surface and credibility gates from uk-data

2 participants