UK national build onto the shared gate battery (#611 A2) - #662
UK national build onto the shared gate battery (#611 A2)#662juaristi22 wants to merge 5 commits into
Conversation
Digests of release inputs the gates themselves do not consume (for the UK, the calibration-diagnostics digest) need a signed home once the legacy schema-3 attestation retires. The slot rides in the report body and the signed attestation, defaults to an empty mapping so the envelope key set is stable, and refuses non-string entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Exclusion receipts carry approval and expiry dates, and the release contract requires every gate in one report to evaluate them on the same date. The three exclusion-consuming bindings (degenerate surface, input mass, tail concentration) now require an exclusions_evaluated_on artifact — one date, computed once by the caller — instead of each gate defaulting its own clock across a possible midnight. The degenerate binding also gains the review-time override the driver offers (--degenerate-exclusions): a supplied artifact replaces the committed register for that run, and a new evidence hook digests whichever records actually ran into the signed report's evidence_sha256, so an overridden run self-describes. The committed register stays the policy of record; the spec pin test now covers its declared resource name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The consumer flip the tooling was built for: build_uk_national_dataset constructs one GateBatteryRun over the declared uk/gates.json spec and runs both phases under BLOCKS_ARTIFACT — preflight before the frame loads (the two raising assertions become declared, persisted verdicts), terminal after the last stage and immediately before the staging writer. Every declared entry now appears in the schema-4 report at the existing diagnostic path; evidence the build cannot supply is a named evidence_absent gap that blocks only release candidates, which is the chartered semantic change: omission stops vanishing. release_candidate (default False, refused on a sampled rung — the #627 non-publishability coupling), a shared now clock, and a gate_registry test seam replace the module-attribute monkeypatch seams. A full-scale build still refuses to stage unsigned (report on disk first); a rung may proceed with an honest shippable: false. The schema-1 input-coverage alias keeps its byte-compatible last-write order through a try/finally around enforce. Deleted: _UKGateEvidence/_uk_gate_evidence (battery_bindings' _UKGateSurface is the one surviving copy) and terminal_gates' verbatim _evaluate_gate (the shared executor's copy is now imported by the retained uk_terminal_gate_report oracle). GateBatteryRun gains a public phase_report accessor so the build result carries its phase reports without reaching into private state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The driver catches GateBatteryBlockedError instead of string-matching the "Release gates failed:" prefix (stage reports are written only for a terminal block — a preflight block ran no stage), gains --release-candidate with a parser refusal on any sampled rung, and forwards the flag to the build. The stdout payload (schema 4 -> 5) and the build record (schema 2 -> 3) embed the schema-4 gate report, and the record mirrors calibration_diagnostics_sha256 top-level from the report's signed release_evidence slot. _is_final_release_gate_failure is deleted; the canonical-release-id fence stays as defense in depth over the id namespace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Reviewing this as a draft, since the body reads as review-ready. The delete-or-justify section is the thing I'd want in every migration PR of this shape — naming each retained artifact with its retirement trigger is what stops a migration window becoming permanent. Keeping uk_terminal_gate_report explicitly as the differential oracle, rather than as "we didn't get to it", is the right call.
Enumerating the eight behaviour deltas with a named test each also made this reviewable at a fraction of the cost it would otherwise have been. Two of them I probed; one has a real gap.
1. The signing env-var rename is code-complete but leaves the operator docs pointing at the old variable.
gate_signing_key_env("uk") moves the variable to MICROCOSM_UK_TERMINAL_GATE_SIGNING_KEY, but the two places a human is told what to export still name the old one, and neither is in this PR's file list:
tools/release.env.example:18—export POPULACE_UK_TERMINAL_GATE_SIGNING_KEY=, under a comment saying to source it "before both the UK build and publish steps"README.md:144— "Canonical UK exact-k builds also require a stable, base64-encoded 32-bytePOPULACE_UK_TERMINAL_GATE_SIGNING_KEY"
So after this lands, an operator who follows either exports a variable nothing reads, and per delta 4 the full-scale build then refuses to stage — report on disk, H5 never written. That fails safe, which is right, but the diagnostic points away from the cause because the checked-in instructions are wrong.
Worth noting the retention in contract.py / test_contract.py is correct — the POPULACE variable stays with the schema-3 vintage path, as #663 says. So this isn't "rename everywhere"; it's specifically the two operator-facing docs, and during the migration window they probably need to name both and say which path uses which.
No CI or workflow file is touched either. If the build runs anywhere automated, that environment needs the new variable before this merges, not after.
2. "The A3 verifier must land before any exact-k release assembly" is a note, not a guard.
Delta 3 changes the report at the existing path from schema 3 to schema 4, and the safety argument is that the schema-3 checker fires only for exact-k release ids and no published release exercises it. #663 says merge order between the two is free because there's no file overlap. Both are true, and together they leave a window where #662 is on main, #663 isn't, and an exact-k assembly would produce a schema-4 report that the contract has no dispatch for.
The window is probably short and the team small enough that it holds. But the protection is currently a sentence in a PR body, and PR bodies stop being read the moment the PR merges. If there's a cheap mechanical guard — the assembly path refusing an unknown schema_version by name, which #663 adds anyway — it might be worth having it land with this PR rather than the other one, so the ordering constraint can't be violated silently.
3. Smaller
- Delta 1 deletes
_is_final_release_gate_failureand its string-match. You say no out-of-repo consumers exist; agreed nothing in-repo matches the old"Release gates failed: "prefix. Worth a changelog line flagging the typed-exception change for anyone catching it downstream, since a string-match break is silent at import time. - Delta 6 (one exclusion clock computed once per build, rather than per-gate
today()defaults that could straddle midnight) directly closes the cross-gate same-date rule #658 added. Good that the coupling was noticed rather than discovered in a midnight build. - Delta 8 moving configuration refusals ahead of the sidecar unlinks is the same class of fix as the
--degenerate-exclusionsordering bug from #658's adversarial pass. Might be worth a single invariant somewhere — "no destructive step precedes argument validation" — since that's now twice.
Net ~40 lines shorter in national_build.py with the orchestration collapsing to construct → run_phase → enforce is a good outcome for a change this size.
The UK consumer flip of microcosm#611 increment 1 (A2 of the plan on #611):
build_uk_national_datasetstops running the hand-rolleduk_terminal_gate_reportbatch and becomes the sharedGateBatteryRunexecutor's first production caller. Preflight runs before the frame loads, terminal immediately before the staging writer, both underBLOCKS_ARTIFACT; one schema-4 report lands at the existingterminal_gates.jsonpath with every declared entry present.The chartered semantic change
Omission stops vanishing. The legacy report silently omitted gates whose evidence was absent; the battery records each as a named
evidence_absentgap with the missing keys spelled out. Blocking is two-axis:FAILEDblocks every build exactly as today,evidence_absentblocks only under the new--release-candidateposture (default off — the staging build has no calibration, so its parity trio is legitimately absent and the default build's blocking behavior is verdict-identical to the legacy path).--release-candidateis refused on a sampled rung: a rung is structurally non-releasable, the durable #627 coupling the #656 fence comment deferred to this increment. The id-namespace fence stays as defense in depth.Behavior deltas a reviewer should probe (each pinned by a named test)
RuntimeError("Release gates failed: ...")→ typedGateBatteryBlockedErrorwith entry-id-prefixed failure lines; the driver's string-match (_is_final_release_gate_failure) is deleted. Out-of-repo consumers matching the old prefix would break — none exist in-repo. (test_national_build_real_terminal_batch_writes_all_findings_before_raise, driver re-raise tests.)blocked_at_phase: "preflight", terminal entriesunreached) where the legacy assertions raised bare. (test_national_build_manifest_failure_blocks_before_stages_with_a_report.)passed→shippable+ five-state statuses, gates keyed by spec entry id). Safe today because the schema-3 release checker fires only for exact-k release ids and no published release exercises it; the schema-4 verifier (A3, concurrent PR) must land before any exact-k release assembly. (test_national_build_real_terminal_batch_passes_before_staging.)MICROCOSM_UK_TERMINAL_GATE_SIGNING_KEY(gate_signing_key_env("uk")). A full-scale build still refuses to stage unsigned — report on disk first, H5 never written; a rung proceeds with an honestshippable: false(reviewed decision: rung operability during the env-var migration). CI/local build environments need the new variable exported. (test_full_scale_build_refuses_to_stage_unsigned,test_rung_build_proceeds_unsigned_with_an_honest_report.)calibration_diagnostics_sha256moves from the legacy schema-3 attestation into the battery's new signedrelease_evidenceslot (small additive executor extension) and is mirrored top-level in the build record (schema 2 → 3); the stdout payload bumps 4 → 5. (TestReleaseEvidence, driver seam test.)exclusions_evaluated_onartifact — one date computed once per build — instead of per-gatetoday()defaults that could straddle midnight and violate the cross-gate same-date rule source_year reviewed exclusion + schema-2 approval receipts with expiry (#630, #610) #658 added to the contract. (TestExclusionDiscipline.)--degenerate-exclusionsunder the battery is a loud override: the spec-derivedpolicy_sha256cannot move at runtime, so the degenerate binding's new evidence hook digests the resolved register records intoevidence_sha256— an overridden run self-describes in the signed report, and the committed register stays the policy of record. (test_review_override_is_loud_in_the_evidence_payload.)ValueErrorbefore any report exists, where the legacy path produced a FAILED gate inside the report — deliberate: it is a configuration error, not gate evidence. Configuration refusals now all precede the sidecar unlinks, so a misconfigured run cannot destroy a previous report. (test_release_candidate_is_refused_on_a_rung_before_any_unlink.)Delete-or-justify (the legacy sweep)
Deleted, zero references remain:
_UKGateEvidence/_uk_gate_evidence(battery_bindings'_UKGateSurfaceis the one surviving copy of the duck-attr surface), terminal_gates' verbatim_evaluate_gate(the shared executor's copy is imported by the retained oracle), the driver's_is_final_release_gate_failure, theuk_release_input_coverage_gatemodule alias, and everymonkeypatch.setattr(national_build, ...)seam — replaced by an injectablegate_registryparameter.Retained deliberately, each with its retirement trigger:
uk_terminal_gate_report+ writer +_AttestedUKTerminalGateReport+ the POPULACE env var (the differential-test oracle — retires after S3's 10% old-vs-new real-data comparison, together with theiruk_runtime/__init__re-exports); the schema-1--input-coverage-jsonalias (byte-compatible via the in-memoryGateOutcome.result, kept through a try/finally so the blocked path preserves the legacy last-write order); the driver's canonical-id fence (defense in depth behind--release-candidate).Net orchestration complexity shrinks: the report/write/raise sequence and its attestation plumbing collapse into construct →
run_phase→enforce, andnational_build.pyis ~40 lines shorter.Verification
The differential suite (battery ≡ legacy oracle, gate for gate, over identical synthetic evidence — now including a shared expiry clock and an expired-register case) stays green through the whole change; the executor-level write-then-block, alias byte-compat in pass and block paths, candidate blocking, and both unsigned behaviors are pinned by new orchestration tests. Fixtures remain synthetic throughout (UKDS licence).
Part of #611. The A3 verifier PR lands the schema-4 verification path in microcosm-data; whichever merges second flips the round-trip xfail noted there.
🤖 Generated with Claude Code