Repository navigation
Fix #792: derive band edges from the compiled register, never the pruned roster - #803
Conversation
7ec0c64 to
baef8ac
Compare
baef8ac to
4429487
Compare
|
Automated review pass (Claude Code, high effort, diff only — no execution). Four findings. The first two matter most: between them, the default path reintroduces #792 and the refusal degrades into a silent drop. 1.
|
Banded measures took each band's upper edge from the lower edges of sibling specs in the live registry, so excluding a band silently widened its lower neighbour to the next surviving edge (or to infinity at the top) — the #792 movers: 12 of 361 shared measures shifted by up to +858% when the exclusion register grew 5->47, each by exactly the mass of the absorbed excluded siblings. materialize_target_bindings and resolve_target_measures now accept a band_edge_registry (default: the materialized registry, bit-identical for existing callers), and _band_bounds refuses a spec whose own lower edge is absent from the supplied edge set rather than guessing. Regression fence: survivors must be bit-identical under sibling exclusion, for numeric edges and published range labels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The seam prunes measure exclusions before materialization, so the stage only ever saw the pruned registry. run_uk_calibration now takes the compiled pre-exclusion register as band_edge_registry and refuses, before any artifact is written, a non-empty exclusion receipt without it — and a register whose extra names do not exactly reconstitute the receipt (the applier already fails exclusions that match zero specs, so pruned + receipt == compiled is exact). The registry content hash is recorded as run_config.band_edge_register_sha256, flowing into the build record and identity digest. The seam tool passes compilation.registry; census counts are unchanged — only survivors' measured values return to published band widths. The stale prose in test_uk_measure_simulation that read the widening artifact as the resolution mechanism is corrected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…verage fence Findings 1 and 3 (vahid-ahmadi): band_edge_registry is now REQUIRED at UKNationalCalibrationStage and run_uk_calibration - never defaulted. The stage cannot tell a pruned registry from a full one, so the old fallback to self.registry restated #792 for any direct caller holding a pruned roster, and gating the run-level check on receipt truthiness let an empty-but-present receipt skip reconciliation entirely. The reconciliation now always runs: an empty receipt is a claim that nothing was pruned, so the rosters must be name-identical. Receipt keys are spec names by the applier's construction (it raises on zero-match exclusions and builds the receipt from matched spec names), documented at the check (finding 4's key-space concern). Finding 2: the coverage fence in _band_bounds now raises BandEdgeCoverageError, a RuntimeError the per-spec skip catch does not swallow - a register that cannot bound a spec is a wrong-register problem for the whole run, and the target must never quietly drop out of the solve. The refusal test asserts propagation instead of a skip, and new tests pin the TypeError on omission and the empty-receipt reconciliation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4429487 to
2a445b8
Compare
|
All four dispositioned in 2a445b8 (the branch is also now rebased onto main's head, 1225a27 — clean, zero file overlap with #755). 1 — fixed as proposed (required, no default). 2 — fixed (refusal, not skip). The coverage fence now raises 3 — fixed, subsumed by 1. With the parameter required at 4 — documented; the key spaces are aligned by construction. Receipt keys are spec names: Verification: ruff, |
…nion (second application) Both merged UK lanes re-pinned the UK spec digest on main; the union with this train computes a fresh UK value (71054f53), keeps BE at the train's 7062e38f, and leaves US unchanged (835b4d61) — all verified against the live loader on the merged tree. Loader, gate-battery-pin, UK content-identity, and country-bundle suites green on the union. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erialize The rule-1 scorer refused a frozen register it could not materialize on both arms, and 119 of the v20 register's 638 targets bind admin-basis inputs the enhanced FRS never carried (the UC family measures, the CGT asset type, the ONS household type), so no candidate built on the current surface could be scored, and therefore certified, in this repository; the v20 head-to-head ran on a pruning scorer outside the tree (María's direction, 2026-09-21). The score core moves to microcosm.build.uk_runtime.candidate_score, shared by the command, the rowwise driver's national role and the certifier. A target whose measure the incumbent cannot materialize is pruned from both arms and reported (a stderr warning naming every absent measure and the rows it removed; incumbent_unresolvable_pruned in the receipt); a measure the candidate cannot materialize still refuses; band edges come from the full register (#803). The receipt's evaluation block decides rule 1 (#578) on that surface with a verdict, and the receipt is the score block itself (the certifier reads artifacts.candidate.sha256 at the top level, #823 B0). The certifier refuses a receipt whose verdict is not passed, whose surface does not close over its pruned rows, or which carries no evaluation, and summarises the verdict into the certification. --no-prune-incumbent- unresolvable restores the refusal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he build The national role takes --incumbent-h5 / --incumbent-sha256 (and a label) and, after the bundle is staged and before the telemetry completes, scores the finished candidate against it on the surface both can materialize (microcosm#967's common-surface scorer, loaded lazily so this branch stands without it): score_vs_incumbent.json beside the outputs and as a reviewed telemetry artifact, rows the incumbent cannot materialize pruned from both arms and named on stderr, and a manifest evaluation block with the rule-1 verdict the release-cut certifier requires. The evaluation never blocks the build: an error is recorded and warned, staging and the exit code stand. Without the flags the manifest says not_requested. The full compiled register is frozen beside the solve register as national_contract_registry.json, the band edges a pruned scoring surface must never redraw (#803). The dense role refuses the flags. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
prepare_uk_target_frame had no test, and the defect it fixed (every score of a candidate export refused since the region fan-out, because one injected input name at two entities met the prepared frame's column-uniqueness rule) went unseen. The fixture injects one input at the person and the household entity: both targets materialize, the injected inputs are dropped from both tables, the same injection without the drop is refused, and a spy asserts the band-edge register reaches materialization (#803). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d-edge register prepare_uk_target_frame (the scoring route) missed two things the calibration stage does: dropping the injected measure-input columns after materialization, and materializing with a caller-supplied band-edge register. Since the region fan-out, cross-grain measures (region, country, esa_contrib, esa_income) inject at two entities, so the prepared frame's global-column-uniqueness rule refused every score of a candidate export. The band-edge register matters whenever the scored registry is a pruned surface: per #803 a pruned roster must never redraw its own band edges. The scorer tool grows --band-edge-registry-json and threads both fixes. Debt: no unit fixture exists for CalibrationFrameAdapter or this route; the functional evidence is the v20 head-to-head this commit unblocks. A proper synthetic-frame test belongs in the upstream PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erialize The rule-1 scorer refused a frozen register it could not materialize on both arms, and 119 of the v20 register's 638 targets bind admin-basis inputs the enhanced FRS never carried (the UC family measures, the CGT asset type, the ONS household type), so no candidate built on the current surface could be scored, and therefore certified, in this repository; the v20 head-to-head ran on a pruning scorer outside the tree (María's direction, 2026-09-21). The score core moves to microcosm.build.uk_runtime.candidate_score, shared by the command, the rowwise driver's national role and the certifier. A target whose measure the incumbent cannot materialize is pruned from both arms and reported (a stderr warning naming every absent measure and the rows it removed; incumbent_unresolvable_pruned in the receipt); a measure the candidate cannot materialize still refuses; band edges come from the full register (#803). The receipt's evaluation block decides rule 1 (#578) on that surface with a verdict, and the receipt is the score block itself (the certifier reads artifacts.candidate.sha256 at the top level, #823 B0). The certifier refuses a receipt whose verdict is not passed, whose surface does not close over its pruned rows, or which carries no evaluation, and summarises the verdict into the certification. --no-prune-incumbent- unresolvable restores the refusal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
prepare_uk_target_frame had no test, and the defect it fixed (every score of a candidate export refused since the region fan-out, because one injected input name at two entities met the prepared frame's column-uniqueness rule) went unseen. The fixture injects one input at the person and the household entity: both targets materialize, the injected inputs are dropped from both tables, the same injection without the drop is refused, and a spy asserts the band-edge register reaches materialization (#803). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Closes #792 (code side). Based on main's head (#793 merged as 58a09bd; rebased onto it, trees byte-identical to the verified state).
The defect — deterministic, not RNG
Banded measures took each band's upper edge from the lower edges of the sibling specs surviving in the pruned registry (
_band_edges_by_groupiteratesregistry.specs; the seam applies the measure-exclusion register before materialization). Excluding a band therefore silently widened its lower neighbour to the next surviving edge — or to infinity at the top.This is the whole of the 5→47 register movement: every one of the 12 movers' 361-roster initial estimates equals its 403-roster value plus the 403-roster values of exactly the excluded sibling bands its widened band absorbed (max residual 1.1e-5 on values up to 2.6e10). Replaying the edge logic predicts 21 widened bands; the 12 with model mass in the absorbed region moved, the other 9 are bit-stable. The issue's RNG hypothesis is refuted three ways: formula-time randomness is statically banned in policyengine-core; every UK draw is minted spine-side, identity-keyed, and frozen into the H5; and the operator's own
rng_controlreceipts show 0/403 rows differing across code pins. The 4 blockeduk_target_fitcells attributed to this class are artifacts — those bands pass (or nearly) at published widths.The fix
target_materialization.py):materialize_target_bindingsandresolve_target_measuresaccept a keyword-onlyband_edge_registry(defaultNone= derive from the materialized registry, bit-identical for every existing caller). Band edges come from it; the materialization loop stays on the pruned set._band_boundsnow refuses a spec whose own lower edge is absent from the supplied edge set, rather than guessing — the same posture the function already took for unreadable edges.materialize_uk_ledger_targets→UKNationalCalibrationStage→run_uk_calibration; the seam tool passescompilation.registry.run_uk_calibrationrefuses, before any artifact is written, (a) a non-empty exclusion receipt without a band-edge register, and (b) a band-edge register whose extra names do not exactly reconstitute the receipt (the applier already fails exclusions matching zero specs, sopruned + receipt == compiledis exact). Provenance:run_config.band_edge_register_sha256(the registry content hash) flows into the build record and identity digest.band_period_factor), plus refusal and threading tests at every seam layer. The middle-band prune flips[0,20)to[0,40)on pre-fix semantics, so the fence fails on the old code by construction.Deliberately untouched:
certify_uk_release_cut.py's pruned reference registry (aname@periodroster-parity instrument — it never materializes values, which is exactly why its parity arm could not catch this drift), and the scoring path (prepare_uk_target_frame— the frozen scoring register is pinned to the compiled registry before the prune, so its self-derived edges are already the published ones). Register census counts are invariant: compiled/excluded/calibrated do not change, only survivors' measured values return to published band widths.Verification
Ruff,
ci_test_groups.py --verify, the six touched test files, and the fullshared-specandukCI group file lists all pass locally under the locked engine env (run while the PR was stacked on the pre-merge #793 branch; now that the base is main, PR CI runs the same lanes). The data claim — the 12 movers reverting to 403-roster values and the 4 artifact cells reading at published widths — is a licensed seam-run fact, verified on the next seam run outside PR CI, per the PR-CI/certification boundary.🤖 Generated with Claude Code