Skip to content

UK rowwise candidate: solve local and national targets as one matrix on the cloned spine (#762 PR A) - #852

Merged
juaristi22 merged 22 commits into
mainfrom
uk-rowwise-candidate-762
Sep 4, 2026
Merged

UK rowwise candidate: solve local and national targets as one matrix on the cloned spine (#762 PR A)#852
juaristi22 merged 22 commits into
mainfrom
uk-rowwise-candidate-762

Conversation

@juaristi22

@juaristi22 juaristi22 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

First of two PRs for #762 (plan: repos/uk-762-rowwise-candidate-plan.md, approved 2026-09-02) — the joint-solve machinery. The second PR carries the signed registers, the measured doctrine adjudication, the receipts, and the local-area release contract/assembly after the licensed runs.

What this does

One matrix, one weight vector, one solve. tools/build_uk_rowwise_candidate.py now clones the spine through the OA ladder at K, runs the policyengine-uk engine on the cloned frame, and solves the national families (compiled + measure-excluded, 364 activated references) and the local families (compiled references over both grains minus signed deferrals, plus the ladder's census-household rows) as rows of a single sparse constraint matrix in one calibrate() call under the local doctrine. No national-only solve exists on this path; the national line's diagnostics are only a comparison yardstick.

  • Surface (local_rowwise.py): a long-format multi-grain builder build_uk_rowwise_local_surface_matrix (constituency + local authority, present cells only, signed deferrals are absent rows, canonical grain/metric/area order, fail-closed on unreachable nonzero targets and per-grain roster coverage). The dense single-grain builder delegates to it and keeps its legacy area-major result bit-for-bit. Local rows are named by the compiled register's row_name, so the scorer joins by identity; area masks are materialized lazily from an int32 index.
  • National rows (UKRowwiseNationalRows, solve_uk_rowwise_weights_under_doctrine(..., national_rows=, target_weight_rule=, restore=)): local rows first, national rows after, one TargetSet; row-name alignment asserted over every compiled row; diagnostics and past-cap censuses reported per row kind; prepared slash-named measure columns are restored away before the frame rebuild and refused if any survive. rotated_uk_local_holdout keeps the national rows in every training fold.
  • Surface assembly + cross-grain (ledger_targets.uk_local_target_surface): compiled local registry → present cells (family via family_for_metric, compiled source family retained), ladder household rows for both grains, then apply_uk_cross_grain_reconciliation with the national control rows present (the standing country > constituency > la rule and bridges from Declare cross-grain target reconciliation for the UK local surface: national wins, sub-national rescales (#495 increment 6 companion) #802/UK local target activation: uk-data parity register, masking doctrine, council tax, PIPR measurements (#147 PR 2) #831 fire in the joint surface). A declared bridge whose missing national members are all in-force reviewed measure exclusions is classified unbound and listed under cross_grain.unbound_bridges in the plan and manifest instead of refusing the surface; a partial binding with any unreviewed missing member still fails closed by name. On the live surface this is the household-composition partition bridge (three cells are UK spine: relationship-to-head frame column, so the ONS household-composition split is measurable #791's exclusions to 2026-11-26), so the ladder's census household counts bind as published until that is adjudicated (plan item A11). A national target that fans out into several cells at one geography (UC payment-distribution bands, SPI income bands: 15 targets, 213 cells on the live compile) is a distribution, not a control: it hands the engine no control row, is dropped from the bound ids the engine sees, and is receipted as fanout_targets_not_controls (the compile activates only a subset of the DWP award bands, so their sum can never reproduce the caseload); every cell still solves as its own row. Eight declared age-band bridges (local ons.age.{b} at both grains ↔ the single-cell national ons.population.age_*_by_region UK totals; 80–89 has no local counterpart) let the local age bands rescale to the national totals despite the half-open vs inclusive edge encodings that defeat the exact-signature match.
  • Doctrine (local_doctrine.py): weighting vocabulary ("uniform", "grain_equal")grain_equal gives the national rows, the constituency rows, and the LA rows one equal share each, uniform within, derived from row labels through the national family_equal arithmetic; uk_local_doctrine_with_overrides receipts the single overridable field. UK_LOCAL_TARGET_LOSS_CAP 10.0 and UK_LOCAL_MAX_WEIGHT_RATIO 100.0 are unchanged.
  • Scale ladder (--sample-fraction {0.01, 0.10, 1.0}, --sample-seed): f100 never samples (sampled: false); below it, sample_uk_spine_frame (new beside the certified-compact policy in national_sampling.py) samples whole source families keyed on the spine's source_household_id, stratified by the raw row's region, through the shared frame sampler with one full-mass renormalization; orphan derivatives and multi-region families are refused. Found on the first licensed dry-run: the compact policy needs a clone_index column the spine does not carry. Below f100 the surface restricts to cells whose area has assigned rows (receipted as rung_surface), the battery runs and persists every entry but enforces only the ladder gate, and the manifest says releasable: false; at f100 every release-blocking entry blocks as before.
  • Engine on the clone: one scratch-mode UKMeasureResolver over the cloned frame resolves the national measures and feeds compute_household_metrics for both grains; --engine-blocks K resolves per clone block and rejoins by entity id (refused unless K equals --n-clones). Dry-run compiles both registries, assembles and reconciles the surface, checks binding adjudications, sizes the matrix analytically, and sweeps --candidate-clone-counts — without the engine, the solve, or any write.
  • Pins and inputs: --input-sha256, --ladder-sha256, --ledger-facts/-sha256/--ledger-manifest-sha256 (the chronicle consumer artifact, same loader as the national seam), optional --measure-exclusions and --register-json; --release-candidate requires every pin and refuses overrides, --skip-holdout, sampled rungs, and per-block engine runs.
  • Area-support reviewed exclusions: area_support_gate(reviewed_exclusions=) records an excluded area's measured shortfalls instead of failing, fails a stale exclusion (area now meets the floor) and an unknown key; the binding resolves exclusions_resource = new schema-2 register uk/local_area_support_exclusions.json on the shared exclusion clock (expired/premature entries fail). The register ships empty; entries are adjudications for the second PR. Gate-battery digests, the spec sha, the parameter-key pin, and docs/gate-battery-contract.md re-cut accordingly.
  • Manifest v2: identity chain (spine sha + sidecar provenance via the promoted load_bound_spine_sidecar / spine_provenance_from_sidecar, ladder sha + layer vintages, ledger shas, code pin, runtime), survey_year (2024) and calibration_year (2025; artifact microcosm_uk_2025_local.h5), sampling and rung receipts, geography, weights (kind chain, mass conservation, realized max ratio vs design), solve (rule + override receipt, per-kind censuses, measure resolution, cross-grain, binding adjudications, area-support exclusions), fit by family for local and national rows, support_limited_misses (failing cells vs per-area ESS), holdout, vintages, releasable.
  • Vintage receipts: the area authoring path now records uprating_holds like the national path — local_target_references.json gains uprating_from_period/uprating_to_period on the 17,077 cells whose facts predate the 2025 period (UC 2025-05, council-tax stock 2025 carry none; age 2024, SPI tax-year 2023, tenure 2021/22 do), membership gains the hold census; every other byte of the surface is unchanged.
  • UC child bands at benefit-unit grain (A1-4 ruling): the four constituency uc_hh_*_children metrics classify each UC benefit unit by its own num_children and sum benefit units into households, matching the national benefit-unit re-bind (Re-bind the DWP UC composition and TCL disability-children targets to the publisher's grain (#808, #809) #813); names and order unchanged, census descriptions regenerated.
  • Scoring inputs: tools/score_uk_local_candidate.py pins the merged UK local target activation: uk-data parity register, masking doctrine, council tax, PIPR measurements (#147 PR 2) #831 surface (19,618) and accepts a candidate superset with a rows_outside_register receipt; new tools/extract_uk_local_incumbent_surface.py turns the incumbent's wide weight arrays (one 2025 dataset shaped (650|360, 52846), no ids inside) plus the roster CSVs and the incumbent household table into the sha-pinned CSVs the scorer consumes, pairing by position and refusing length mismatches.

Verification (run independently of the implementation pass, on d2b31496)

  • ruff check . clean; ruff format --check on the 28 touched files clean (repo-wide format debt untouched).
  • tools/census_uk_local_targets.py --check and tools/census_uk_data_target_parity.py --check current; tools/ci_test_groups.py --verify ok (new test files land in the UK lane); git diff | grep -c "policyengine-uk-data#" → 0.
  • Targeted battery (rowwise/doctrine/candidate/scorer/gate-battery/bindings/gates/ledger/ladder/national-calibration/diagnostics/authoring/council-tax/spec-bundle/country-spec/holdout/contract-pins/release-certification/extractor): 562 passed on the first commit; after the rebase over UK spine: benefit-unit-grain SPI UC reporter draw with pre-take-up screen (#832); exact capital sentinel (#833) #835 and the three licensed-touch fixes (spine sampler, Ledger geography spelling, reviewed-exclusion bridges): targeted 443 passed, spec/country/contract-pins/contract 511 passed, cross-grain/ledger/candidate/rowwise 90 passed.
  • microcosm-data contract suite: 345 passed, 1 skipped.
  • CI groups run in full from tools/ci_test_groups.py --list: rest 6,098 passed, 63 skipped; spine-uk 1,712 passed, 16 skipped, 2 failed; uk engine 1,450 passed, 16 skipped, 2 failed — the 2 failures are test_cached_reference_regeneration_matches_committed_surface and test_cached_candidate_regeneration_matches_committed_evidence, reproduced identically on clean main (installed policyengine-uk 2.92.1 vs committed 2.89.0 licensed-cache evidence; Make the UK eFRS parity re-pin one reviewed command; hold every identity mirror in lockstep (companion to #747) #749's lane).
  • Re-cut pins (from real payloads on this base): UK spec sha 0733a81e…91eb9472…; gate policy bcbcfd55…d76f012f…; gates manifest 76c861d4…df8c9292…; composition fingerprint dfbad2aa…0de7f351….
  • Regenerated local_target_references.json / membership: 19,618 rows identical apart from the added uprating-hold fields on 17,077 rows; membership gains uprating_holds (17,077) and holds_by_target; every other membership field byte-equal.
  • End-to-end on the synthetic fixture: a K=2 f100 joint build (stub resolver, national + local + ladder rows in one matrix, manifest v2, Logbook rung f100, releasable: true) and an f001 run (rung receipt, restricted surface, releasable: false); the licensed spine, ledger artifact, and incumbent weights are not in the PR-CI environment by design.

Review notes

  • Implemented by Codex under the reviewed plan's protocol on origin/main @ 90293e0a; deviations were verified and are recorded in the plan doc's execution notes (dense-wrapper order compatibility flag; households stays the ladder-only metric; compiled spec.family is the source family, so surface rows classify by family_for_metric and keep source_family; ledger/pin arguments stay optional on the synthetic path and are mandatory under --release-candidate).
  • Nothing here binds a new family in production: the joint path refuses to solve until uk/local_binding_adjudications.json carries in-force entries for the five fenced families (second PR).
  • The two UK-lane failures on this base (test_cached_reference_regeneration_matches_committed_surface, test_cached_candidate_regeneration_matches_committed_evidence) are the known policyengine-uk 2.92.1 vs committed 2.89.0 evidence mismatch and fail identically on clean main.

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

🤖 Generated with Claude Code

@juaristi22
juaristi22 force-pushed the uk-rowwise-candidate-762 branch 2 times, most recently from 72c0c94 to 6882545 Compare September 2, 2026 21:03
juaristi22 and others added 19 commits September 3, 2026 17:46
…ed spine (#762 PR A)

One TargetSet, one weight vector, one calibrate() call: the compiled local
surface (both grains, present cells only) and the ladder household rows join
the activated national families as rows of a single sparse constraint matrix
over the K-cloned spine, with the policyengine-uk engine resolved on every
cloned row. Adds the long-format multi-grain surface builder, registry-named
local rows with lazy area masks, national rows in the doctrine solve and the
rotated holdout, the grain_equal weighting rule as a receipted override, the
f001/f010/f100 scale ladder with a below-f100 gate posture, sha and ledger pins,
per-clone engine blocks, a reviewed-exclusions channel for the area-support
gate (register ships empty), manifest v2 with survey and calibration years,
uprating-hold receipts on the local surface, support-limited-miss diagnostics,
a scorer that accepts a candidate superset, and the incumbent local-surface
extractor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#762 PR A)

The first licensed dry-run refused at the sampling step: the driver sampled
even at the full rung, through the certified-compact policy that keys families
on clone_index arithmetic the microcosm spine does not carry. The spine's
lineage is explicit (source_household_id, the support-clone index, and the
SPI, capital-gains, and band-donor flags), so the ladder now keeps f100
untouched and samples whole source families stratified by the raw row's
region below it, through the shared frame sampler with one full-mass
renormalization; orphan derivatives and multi-region families are refused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…al control rows (#762 PR A)

The second licensed dry-run refused in the surface assembler: national
compiled specs carry their geography under ledger_geography_level and
ledger_geography_id (country grain at K02000001 or K03000001), while only the
area-authored local specs also carry the local spelling the assembler read.
One private resolver now prefers the local spelling, falls back to the Ledger
spelling, refuses blanks and disagreements by target name, and checks a
national level against the contract's declared levels; the vintage census,
diagnostics registry, and the end-to-end stub use the same shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… A rebase)

The rebase resolution replaced the first digest in the file, which is the
Armenia constant, instead of the UK row; both rows now carry the values the
merged tree produces.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ions, with a receipt (#762 PR A)

The third licensed dry-run reached the cross-grain pass and was refused: the
household-composition partition bridge is partially bound because three of
its national cells are microcosm#791's reviewed measure exclusions. A partial
bridge whose every missing member carries an in-force reviewed exclusion is
now classified unbound and listed under unbound_bridges in the receipt, so
the lower side binds as published and the gap stays enumerable; any partial
binding with an unreviewed missing member still fails closed by name. The
driver passes the measure-exclusion receipt through and surfaces the entry in
the dry-run plan and the manifest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r geography (#762 PR A)

The fourth licensed dry-run reached the engine's control rule and was refused:
fifteen activated national targets fan out into several cells at one
geography (UC payment-distribution bands, SPI income bands), and the UC
family-type partition is the exact-signature control for the by-area UC
count. The UK surface assembler now groups national specs by contract target
and geography and hands the engine one control row per group, the summed
total for fan-out groups, receipted as fanout_controls_summed; every cell
still solves as its own row, and non-finite cells or mixed levels refuse.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ands at benefit-unit grain (#762 PR A)

The award-band table is the full UC caseload with overlapping cumulative bands,
and the compile activates only a subset of those bands, so a fan-out national
target can never act as a cross-grain control: the surface assembler now emits
no control row for it, drops it from the bound ids handed to the engine, and
receipts it as fanout_targets_not_controls (replacing the summed control).
Per Maria's A1-4 ruling the four UC child-band local metrics classify each UC
benefit unit by its own num_children and sum benefit units into households,
matching the national benefit-unit re-bind; names and order are unchanged and
the census descriptions are regenerated. The synthetic candidate tests pin an
empty support-exclusion register so real micro-LA entries never collide with a
synthetic roster.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The local age bands (ons.age.0_10 .. 70_80, half-open) and the national
single-cell UK totals (ons.population.age_0_9_by_region .. 70_79, inclusive)
describe the same integer-age populations under different edge encodings, so
the exact-signature match never fired and the local bands never rescaled to
the national totals. Per Maria's ruling each local band now has a declared
bridge to its UK control; the national 80-89 band has no local counterpart
and stays unbridged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t, with a receipt (#762 PR A)

The seventh licensed dry-run refused the council-tax groups from the other
side: the Scottish country controls are bound while every Scottish
local-authority council-tax cell is a signed deferral, so their leg has no
lower rows. The engine now takes licensed_empty_legs, derived at the UK front
door from the committed membership's signed deferrals per lower target and
leg (only when the deferral empties the whole leg): a licensed empty leg is
skipped and receipted (empty_legs_licensed), a control with no populated leg
is dropped and receipted (controls_without_lower_rows), and an unlicensed
empty leg still refuses by name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The loaded spine's household table carries no persisted household_weight
column; the typed frame weights are the authority. The dry-run clone-count
sweep now attaches them before summarizing per-area support, as the real
support path already does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e driver (#762 PR A)

The first f001 run on the live spine resolved the engine and then refused at
frame assembly: injected inputs such as region and the ESA flags exist on two
entities once injected, and the flattening rule refuses duplicate columns. The
driver now records the original columns, injects, materializes, and drops the
injected inputs before the prepared frame, the same lifecycle as the national
stage; the synthetic stub now injects a colliding input to pin it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The first f001 run cleared the engine and refused at the matrix builder:
at one percent almost every area has rows, but 10,429 cells carried a nonzero
target with no metric support in the sample. The builder's fail-closed check
is right at f100 and stays; below f100 the driver drops such cells before
the build and receipts them in rung_surface (dropped_unreachable_cells, by
grain and by family), next to the area-coverage restriction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sion from package metadata (#762 PR A)

The gate report kept the retired populace_uk_<survey_year>_rowwise_candidate
name beside the microcosm_uk_<calibration_year>_local artifact, and the
resolver receipt recorded the engine version as unknown because the module
exposes no __version__; a declared __version__ still wins (test doubles),
else the distribution metadata is the authority.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…usal (#762 PR A)

The first f100 run on spine-m refused 172 cells with a nonzero target and no
household support at K=4; five examples do not say which families carry the
miss. The refusal is the evidence for support-or-target work, so it now
carries the counts by metric and by grain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The K=10 measurement with per-clone engine blocks refused at the scratch
export: a one-clone block carries a K-th of the cloned mass while its log
still ends on the full-clone record, and the national-frame writer validates
that chain. Each block now carries an explicit subset record (old = cloned
total, new = block total, reason naming the block) before the scratch export;
the synthetic resolver stub validates the frame it receives so the chain is
pinned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…762 PR A)

The first K=10 f100 solve completed and the support gate then blocked the
artifact; write-then-block persisted only the gate report, so the fit,
stretch and past-cap evidence of an eleven-minute solve were lost. A
release-blocking failure at f100 now still writes the diagnostics, manifest
(releasable false, blocking_failures listed) and the artifact, records the
Logbook attempt as failed, and exits non-zero; a non-passing ladder verdict
remains structural and re-raises.

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

Found on the licensed bound-10 run (U10, R8): the battery passed every
release-blocking gate and the driver exited 0, yet the manifest said
releasable: false, and on the blocked runs blocking_failures listed the
diagnostic target_fit and weight_ratio lines beside the area_support
block. The gate-battery contract says diagnostic entries never block.

- blocking_failures now carries only failed release-blocking entries;
  diagnostic_failures carries the rest; release_gate_failures_not_enforced
  carries release-blocking failures the battery did not enforce (below f100
  only the ladder gate is enforced; a dev build tolerates absent evidence).
- releasable follows the battery: f100 and every release-blocking entry
  passed. failing_gate_ids still lists every non-passing entry.
- A block whose persisted report names no failed release-blocking entry
  re-raises (report and error disagree).

Two tests cover the diagnostic-only and the mixed block on the synthetic
fixture; the candidate file runs 16 passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ds (#762)

The UC child bands now classify each benefit unit by num_children (A1-4),
so the shared-lane fake simulation must carry it; and three tests that
write staging H5 through pandas need the pytables guard the rest of the
file already uses (the wheels venv has no tables). Verified with tables
present (all pass) and with the tables import blocked (guarded tests skip,
the rest pass).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ain (#762 PR A)

main merged #842 (age reorder) and #850/#857 under this branch; the merged
gates.json and spec move the policy, manifest, fingerprint and spec digests.
Recomputed from the live tree on the rebased base.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juaristi22
juaristi22 force-pushed the uk-rowwise-candidate-762 branch from 9bd0b39 to 05a4a69 Compare September 3, 2026 15:55
@juaristi22
juaristi22 marked this pull request as ready for review September 3, 2026 16:08
…t UK gate-battery pins (#762 PR A)

The rebase conflict on the data contract was resolved on this branch's
side, which dropped main's newer content (#850's UC-deduction entries,
#842's age_tail phase move, #857's denied-pool check, the certification
part digests). This branch's only legitimate delta there is the three
gate-battery digests re-cut on the merged gates spec; everything else is
main's.

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

Copy link
Copy Markdown
Contributor

Review pass at 2e9cde8c (Claude Code, high effort; worktree checkout, Python surface read against the PR body — the cited plan repos/uk-762-rowwise-candidate-plan.md is not in the tree, so this is against body and code — targeted suites run without the licensed engine: 576 passed in microcosm-build across 17 files, 228 in microcosm-data's contract tests, ruff check clean on all 37 changed Python files).

The design holds

"One matrix, one weight vector, one solve" is what the code does. solve_uk_rowwise_weights_under_doctrine builds one TargetSet with local rows first and national rows after and makes a single calibrate() call, spied by test_joint_doctrine_solve_compiles_local_then_national_once_and_restores. Alignment is by name and cannot pass misaligned: TargetSet rejects duplicate (name, period) keys, diagnostics are asserted against declared rows by row_name over every compiled row (local_rowwise.py:1164-1175), national rows against the registry likewise (:1100-1112), and calibrate maps loss weights by key. The long-format surface builder is genuinely fail-closed — unreachable nonzero targets, uncovered assigned areas, off-roster areas and duplicate cells all raise — and row order is a stable rank sort over tuple rosters with no set iteration. Lazy int32 area masks are bounded by roster size and transient; the compile sparsifies per target, so nothing about K=15 threatens memory or indexing.

I also checked the "bit-for-bit" claim for the legacy dense path independently: loading origin/main's local_rowwise.py beside the PR's and running both on the test_uk_local_rowwise fixture gives identical CSR indptr/indices/data, identical targets, area_codes, assigned_areas, household_ids, metric_names and metric_values, and an equal target_frame on all common columns (the new one adds family and target_name). The claim is true. Three things to fix before approving, none of them in the solve itself.

Findings

1. Should-fix — --engine-blocks K can still write releasable: true, and nothing guards the ×K aggregate artefact from your own #736 erratum. build_uk_rowwise_candidate.py:592 exposes --engine-blocks; _validate_cli_args allows any value equal to --n-clones (:2491), and the refusal at :2469-2470 fires only under --release-candidate. The manifest's "releasable": args.sample_fraction == 1.0 and all_gates_passed (:2271) has no engine_blocks term, so a non-release f100 run at blocks=K writes a releasable artifact with the deviation recorded only under solve.measure_resolution. _resolve_candidate_engine_surface (:333-365) rejoins per-block outputs by entity id with duplicate and missing-id checks, but there is no check that a per-block-resolved allocated aggregate sums to the whole-frame value — exactly the mechanism that produced ×15 on corporate_land_value, and the stub resolver in test_candidate_engine_surface_resolves_real_per_clone_blocks returns np.arange, so it can only pin id order. Cheapest fix: make releasable require engine_blocks == 1, or drop the hatch now that single-block is the release posture. The default path is single-block and the end-to-end test pins one resolver construction per run, so the production path is fine; the hole is the flag.

2. Should-fix — the gate partition is fail-open on a missing criticality key. _gate_failures_by_criticality (:1817) buckets a failed gate as diagnostic unless payload.get("criticality") == "release_blocking", and release_gate_rows (:2049-2051) filters the same way, so a failed gate whose persisted payload lacks the key drops out of both the blocking list and all_gates_passed. Today the battery raises GateBatteryBlockedError first and the except branch re-raises when the blocking list is empty, and if every key is missing release_gate_rows is empty and the artifact is unreleasable — so the live exposure is partial schema drift on one entry. Unknown or missing criticality should bucket as blocking; that is the fail-closed reading of "partition by criticality".

3. Should-fix — the delegation test does not pin what its name claims. test_dense_builder_delegates_without_changing_legacy_result (test_uk_local_rowwise.py:376) compares the post-PR dense wrapper against the post-PR long-format builder with assert_allclose — self-consistency, not equality with the pre-PR result. Since the result really is bit-for-bit (above), an exact-equality check against a frozen fixture from main would guard the claim rather than restate the delegation.

Questions

  • restore post-condition. Frame ids are asserted equal to problem ids at entry (local_rowwise.py:1057) but not after restore(result.frame) (:1283) before weights are written back by position (:1300). CalibrationFrameAdapter.restore rebuilds from the original tables so order holds today; a reordering restore would misattribute weights silently. One re-assert matches the module's other boundary guards.
  • Fan-out keyed on target_id alone. fanout_target_ids drops a target as a control at every geography if it fans out at any non-bridge geography, so a control single-celled at one geography is dropped silently; and a bridge control that fans out is kept as multiple same-(target_id, geography_id) reconciliation rows. Neither is live today; neither is guarded.
  • Holdout scale under grain_equal. rotated_uk_local_holdout recomputes held_weights over the two held local grains while training weights spanned three grains at 1/3 each, so the report-only holdout is not on the training scale. Worth stating in the receipt if it stays.

Nits

exclusions_evaluated_on: date.today() in the gate battery (:1726) makes verdicts wall-clock-dependent once local_area_support_exclusions.json gains entries (it is empty on this PR; PR B fills it). ruff format --check flags battery_bindings.py:397, pre-existing and outside this PR's hunks.

Not verifiable here

Live-surface counts (364 national, 19,618 local), the age and household-composition bridges on the real compile, ×K behaviour on the real engine, the incumbent extractor on the real H5, and every fit or holdout number all need the licensed engine and data.


Ready to approve once 1 and 2 land; 3 is small and can ride along. The solve, the alignment discipline and the fail-closed surface builder are the substance, and they check out.

juaristi22 and others added 2 commits September 4, 2026 16:13
…riticality, frozen golden for the dense builder (#762 PR A)

- releasable now requires engine_blocks == 1 beside the full rung and the
  release-blocking gates; the manifest writes the posture (release_posture)
  so a reader sees which leg failed. Per-block resolution mis-measures
  population-normalised formulas (the xK land-value artefact, #736), so a
  blocked run is diagnostic-only whatever its gates say.
- Gate criticality reads fail closed: only an explicit 'diagnostic' entry is
  exempt from vetoing; missing or unknown criticality is release-blocking in
  both the failure partition and all_gates_passed.
- The dense builder is pinned bit-for-bit against a golden frozen from
  origin/main's local_rowwise.py on the fixture; the old delegation test is
  renamed to what it checks (wrapper and long-format builder agree).
- restore's post-condition is asserted: the restored household rows must be
  the problem's rows in order, since weights are written back by position.
- Fan-out guards: a bridge control that fans out at any geography is refused;
  a target single-celled at one geography but dropped by the target-id rule
  is receipted rather than dropped silently.
- The holdout report declares its loss-weight scale (held local grains only).
- exclusions_evaluated_on comes from the run's own clock, not date.today().
- battery_bindings.py: pre-existing ruff format nit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Re-pins the UK spec sha on the merged tree (5751a2b7); main changed an
attested module since the last re-pin. No other conflicts.

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

Copy link
Copy Markdown
Collaborator Author

Thanks, all three land in 8be32dc (main merged in b875d4c, one pin re-cut).

(1) releasable now requires engine_blocks == 1 beside the full rung and the release-blocking gates, and the manifest writes release_posture so the failing leg is visible; the per-block aggregate check you describe would need a whole-frame resolution, so the flag is closed rather than the artefact detected.

(2) _is_release_blocking treats anything but an explicit diagnostic as blocking, in both the partition and all_gates_passed.

(3) tests/golden/uk_local_rowwise_dense_fixture.json is main's result on the fixture, compared exactly; the old test is renamed to the wrapper/long-format agreement it checks.

Questions: restore's post-condition is asserted (the test uses a stand-in because uk_national_frame refuses an unsorted id column); a bridge control that fans out raises, and a single cell dropped by the target-id rule is receipted; the holdout report carries loss_weight_scale: held_local_grains_only.

Nits: exclusions_evaluated_on is the run's started_ts.date(), and the format nit is fixed.

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second pass at b875d4c4 (Claude Code, high effort; worktree checkout, the fix commit 8be32dc9 read against the findings, golden regenerated from origin/main's builder, targeted suites run: 647 passed across test_uk_local_rowwise, test_uk_rowwise_candidate, test_uk_ledger_targets, test_gates, test_country_spec, test_spec_engine_country_bundles and the data contract tests; ruff check and ruff format --check clean on every file differing from main).

All three closed; two small residuals, neither worth holding for.

1. Verified. _release_verdict (build_uk_rowwise_candidate.py:1838-1863) requires single_block_engine, full_rung and release_blocking_gates_passed together and returns the posture; the final manifest writes both releasable and release_posture (:2314-2315), the dry-run plan gained the engine_blocks == 1 term (:1920), and the blocked path stays a literal False. No site can emit true with blocks > 1. Closing the flag rather than detecting the artefact is the right reading — a per-block aggregate check would need a whole-frame resolution to compare against, which is the thing single-block already is. Residual: test_release_verdict_requires_single_block_engine is a unit test of the helper; nothing runs --engine-blocks K on f100 and asserts the manifest's releasable is false and its release_posture names single_block_engine. The helper is small enough that this is a nit, but it is the assertion that would catch a future caller bypassing _release_verdict.

2. Verified. _is_release_blocking (:1825-1835) exempts only an explicit criticality: diagnostic, and both the partition (:1820) and release_gate_rows feeding all_gates_passed (:2086-2092) go through it. test_gate_criticality_reads_fail_closed covers the missing key, an unknown value, and a failed gate with no key landing in the blocking list; it fails on the previous head.

3. Verified. I regenerated tests/golden/uk_local_rowwise_dense_fixture.json from origin/main's local_rowwise.py on the same fixture and every key is equal, so the golden is main's output. test_dense_builder_result_equals_frozen_pre_pr_fixture compares indptr/indices/data, targets, area codes, assigned areas, household ids and metric names and values exactly. Residual: target_frame goes through assert_frame_equal with the default check_exact=False, so that one part is tolerance-based; the matrix data, which is what matters, is exact.

Questions, all answered. The restore post-condition is asserted right after restore(result.frame) and before the write-back (local_rowwise.py:1284-1290); the stand-in test fails on the old head, though for the older slash-column guard's reason rather than a demonstrated misattribution, so it is adequate rather than sharp. A bridge control that fans out now raises and a single-cell drop under the target-id rule is receipted (ledger_targets.py:992-1016), both with tests that fail on the old head. The holdout report carries loss_weight_scale: held_local_grains_only (:1470), unasserted but visible.

Nits. exclusions_evaluated_on is the run's started_ts.date() and the format nit is gone. The merge's one re-cut pin is the UK spec_sha256 in test_spec_engine_country_bundles.py; the contract digests are unchanged and consistent.


Nothing outstanding. Ready from my side; the two residuals (an end-to-end releasable assertion under --engine-blocks K, and exact comparison on the golden's target_frame) can ride with PR B.

@juaristi22
juaristi22 merged commit 96faa5d into main Sep 4, 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.

2 participants