Skip to content

UK national build onto the shared gate battery (#611 A2) - #662

Draft
juaristi22 wants to merge 5 commits into
mainfrom
uk-battery-consumer-a2
Draft

UK national build onto the shared gate battery (#611 A2)#662
juaristi22 wants to merge 5 commits into
mainfrom
uk-battery-consumer-a2

Conversation

@juaristi22

Copy link
Copy Markdown
Collaborator

The UK consumer flip of microcosm#611 increment 1 (A2 of the plan on #611): build_uk_national_dataset stops running the hand-rolled uk_terminal_gate_report batch and becomes the shared GateBatteryRun executor's first production caller. Preflight runs before the frame loads, terminal immediately before the staging writer, both under BLOCKS_ARTIFACT; one schema-4 report lands at the existing terminal_gates.json path 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_absent gap with the missing keys spelled out. Blocking is two-axis: FAILED blocks every build exactly as today, evidence_absent blocks only under the new --release-candidate posture (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-candidate is 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)

  1. Error type/message: RuntimeError("Release gates failed: ...") → typed GateBatteryBlockedError with 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.)
  2. Preflight failures now persist a report (blocked_at_phase: "preflight", terminal entries unreached) where the legacy assertions raised bare. (test_national_build_manifest_failure_blocks_before_stages_with_a_report.)
  3. Same path, schema 3 → 4 (passedshippable + 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.)
  4. Signing: env var becomes 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 honest shippable: 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.)
  5. calibration_diagnostics_sha256 moves from the legacy schema-3 attestation into the battery's new signed release_evidence slot (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.)
  6. One exclusion clock: the three exclusion-consuming bindings require an exclusions_evaluated_on artifact — one date computed once per build — instead of per-gate today() 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.)
  7. --degenerate-exclusions under the battery is a loud override: the spec-derived policy_sha256 cannot move at runtime, so the degenerate binding's new evidence hook digests the resolved register records into evidence_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.)
  8. Half-armed input-mass (reference without policy or vice versa) is refused with an early ValueError before 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' _UKGateSurface is 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, the uk_release_input_coverage_gate module alias, and every monkeypatch.setattr(national_build, ...) seam — replaced by an injectable gate_registry parameter.

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 their uk_runtime/__init__ re-exports); the schema-1 --input-coverage-json alias (byte-compatible via the in-memory GateOutcome.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_phaseenforce, and national_build.py is ~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

juaristi22 and others added 5 commits August 11, 2026 18:19
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 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.

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:18export 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-byte POPULACE_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_failure and 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-exclusions ordering 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_phaseenforce is a good outcome for a change this size.

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