diff --git a/changelog.d/757-certification-contract.added.md b/changelog.d/757-certification-contract.added.md new file mode 100644 index 00000000..805de4aa --- /dev/null +++ b/changelog.d/757-certification-contract.added.md @@ -0,0 +1,8 @@ +The publication contract verifies the multi-part release certification +(`release_certification.json`): exact field set, the three mirrored part +scopes and their committed scoped-manifest digests, full-manifest spec +pins, union/no-gap/no-overlap over the declared entry set, per-part +fully-passed status censuses (shippability recomputed, never read off the +flag), the diagnostics digest join, and the release-key signature over the +whole document. The mirrored constants are held in lockstep with the build +shard by the contract-pins sync tests. diff --git a/changelog.d/757-compile-parity-registers-current.fixed.md b/changelog.d/757-compile-parity-registers-current.fixed.md new file mode 100644 index 00000000..1ddf4798 --- /dev/null +++ b/changelog.d/757-compile-parity-registers-current.fixed.md @@ -0,0 +1,11 @@ +The two ledger compile-parity signed-difference registers are regenerated +against the pinned chronicle feed before their runner ships: 13 stale +entries pruned (scotgov council-tax stock and SCP spending, three SLC +recipient rows - the live compilation now matches the fixture) and 13 SLC +entries re-kinded `fixture_only` -> `calibration_drift` with measured +values (the SLC chronicle waves completed after the June fixtures froze). +Zero entries added: the live diff carried no unsigned differences, so the +regeneration is exactly the correction the gate's own anti-rot refusals +demanded. Both release-cut preflight gates now pass against the pinned +feed - verified live, so the producer's first real invocation does not +open on a known-stale register. diff --git a/changelog.d/757-national-release-id.added.md b/changelog.d/757-national-release-id.added.md new file mode 100644 index 00000000..2649a6a9 --- /dev/null +++ b/changelog.d/757-national-release-id.added.md @@ -0,0 +1,10 @@ +The certified UK national line gets its release id (ruling 2026-08-27): +`microcosm-uk-2024-25-national`, one constant name across cuts - run +identity and ordering live in the Logbook chain and artifact versioning, +not in the id. The `2024-25` segment follows the uk-data survey-vintage +convention and the `-national` segment keeps the id disjoint from the +exact-k ladder's `-k` shape. The seam refuses it (a shippable name +belongs to the release-cut producer), and `required_release_files()` now +demands `release_certification.json` for it - completing the review +round's finding 1 at the id-keyed layer, with the content-keyed refusal +staying as the belt for part-carrying directories under other ids. diff --git a/changelog.d/757-phase3-acceptance.changed.md b/changelog.d/757-phase3-acceptance.changed.md new file mode 100644 index 00000000..397b4859 --- /dev/null +++ b/changelog.d/757-phase3-acceptance.changed.md @@ -0,0 +1,12 @@ +The committed spine acceptance receipt moves to spine-i, the candidate +rebuilt at the follow-up tip: payload-identical to spine-g (the +certification machinery, exclusion pass, QRF re-arm, and lever revert are +all payload-inert), 14/14 battery at release-candidate strictness with a +signed report, fit-weight records in the sidecar, identity ladder e4-e8 +green, strict parity `signed_parity` with 0 unsigned. The first certified +cut itself is blocked at `uk_target_fit` on 13 characterized cells - 8 UC +caseload/two-child cells (the U8 lever is measured and exhausted; the +binding constraint is capital-test support, microcosm#750), 4 +exclusion-set-dependence artifacts (microcosm#792), and 1 sparse-band +sibling - with the signed blocked-run receipts in the 757-swap acceptance +evidence and the adjudication queue in the PR. diff --git a/changelog.d/757-qrf-tail-rearm.changed.md b/changelog.d/757-qrf-tail-rearm.changed.md new file mode 100644 index 00000000..385d5ba8 --- /dev/null +++ b/changelog.d/757-qrf-tail-rearm.changed.md @@ -0,0 +1,12 @@ +`uk_qrf_tail_concentration` is re-armed from the #686 L3 baselines (#757 +B4): top_k 100 stays the measurement grid anchor, max_top_share moves to +the exact measured maximum over the checked surface (0.9994670564654868, +hmrc_spi_other_social_security_income at 104 carriers on spine-a), and +min_nonzero_records to the thinnest measured column above the grid anchor +(104). The three saturated sub-anchor columns (taxable termination pay, +charitable investment gifts, SDA; 12-24 carriers, top-100 share 1.0) go +thin visibly on every run. The gate notes record the measuring run - the +baselines file digest, the measured artifact, and the defining column per +threshold. The 12 household-surface grids are measured but not yet armed +(declared follow-up). Gate policy/manifest/fingerprint digests re-cut from +the live producer payload. diff --git a/changelog.d/757-release-cut-certification.added.md b/changelog.d/757-release-cut-certification.added.md new file mode 100644 index 00000000..af3fc706 --- /dev/null +++ b/changelog.d/757-release-cut-certification.added.md @@ -0,0 +1,16 @@ +The release-cut certification producer (#757 B5): the 16 declared national +preflight/terminal gates get their executable home back +(`tools/certify_uk_release_cut.py` over +`uk_runtime/release_certification.py`), running as a scoped release-candidate +battery against the calibrated candidate with evidence reconstructed from +the persisted artifacts - the spine sidecar (which now carries each fitting +stage's `FitWeightRecord`s across the run boundary), the seam's diagnostics +and build record, and the per-run licensed input-mass reference. The +multi-part certification the 5413502559 audit specified composes over the +spine, seam, and release-cut reports: union to the full declared entry set, +no gap, no overlap beyond `uk_aggregate_admin`, per-part signatures and +committed-spec scoped digests, full phase coverage, a closed identity join +(spine report -> sidecar -> build record -> diagnostics -> candidate +bytes), the doctrine and its receipted overrides recorded verbatim, and the +rule-1 score receipt cross-pinned. A candidate's shippability verdict comes +only from the certification. diff --git a/changelog.d/757-review-dispositions-793.fixed.md b/changelog.d/757-review-dispositions-793.fixed.md new file mode 100644 index 00000000..a0254009 --- /dev/null +++ b/changelog.d/757-review-dispositions-793.fixed.md @@ -0,0 +1,18 @@ +The #793 review round (four findings, all the reports-success-without- +testing class): a release shipping national-line gate artifacts without +`release_certification.json` now refuses instead of validating clean by +omission (the id-keyed required-files rule waits on the canonical national +release-id); the rule-1 cross-pin reads `artifacts.candidate.sha256` - the +field that names what the scorer measured - instead of a substring scan; a +malformed sidecar fit-weight block raises as corruption instead of +degrading to the empty tuple (a genuinely empty fitting stage still fails +the audit, never vacuously passes); and the parity evidence's shared +name@period grain is now a loud refusal rather than an implicit +convention. Alongside, the duplication the round pointed at is +consolidated: one scope-filtering helper (`uk_scoped_gate_manifest`, with +a source parameter serving the spine driver's stub point) replaces three +copies and a dead zero-caller variant, one `finalize_uk_scoped_gate_report` +grafts and re-signs every scoped report, the certification's private +cross-module imports become public names, and `GateBatteryRun` exposes its +attested digests as properties so derivation stops routing through a +signed payload. diff --git a/changelog.d/757-seam-refuses-release-candidate.changed.md b/changelog.d/757-seam-refuses-release-candidate.changed.md new file mode 100644 index 00000000..0280c38d --- /dev/null +++ b/changelog.d/757-seam-refuses-release-candidate.changed.md @@ -0,0 +1,7 @@ +The calibration seam refuses `--release-candidate` outright and refuses +canonical UK release ids (the #757 release-cut audit, issue comment +5413502559): its scoped battery covers 6 of the declared gate entries and +must never sign a shippability claim. The hand-written build-record +`shippable` literal retires with it, replaced by a pointer to the +release-cut certification artifact +(`.release_certification.json`) whose verdict is authoritative. diff --git a/changelog.d/757-target-fit-exclusion-pass.changed.md b/changelog.d/757-target-fit-exclusion-pass.changed.md new file mode 100644 index 00000000..8a7d4066 --- /dev/null +++ b/changelog.d/757-target-fit-exclusion-pass.changed.md @@ -0,0 +1,12 @@ +The calibration measure-exclusion register carries the adjudicated +`uk_target_fit` disposition pass (microcosm#757, issue comment 5427936411): +42 windowed exclusions covering the eleven 1m+ top-income channel cells, +the three SLC zero/structural-support channels, the sixteen UC +payment-distribution cells, the OBR welfare-cap pair, the three ONS +household-composition cells pending a relationship-to-head frame column +(microcosm#791), the six sparse HMRC band cells, and `obr.fuel_duties` +(universe scope; ledger retarget pending chronicle-side adjudication). +The six UC caseload/two-child-limit cells are deliberately not excluded — +they ride the `would_claim_uc` lever run — and neither are +`couple_no_children` nor `state_pension_income_band_40_000_to_50_000`, +which the exclusion re-run is expected to pull inside the band. diff --git a/changelog.d/757-would-claim-uc-lever.changed.md b/changelog.d/757-would-claim-uc-lever.changed.md new file mode 100644 index 00000000..c658aa11 --- /dev/null +++ b/changelog.d/757-would-claim-uc-lever.changed.md @@ -0,0 +1,11 @@ +The pre-registered `would_claim_uc` U8 lever (uk-data#452) was run and +measured per the #757 `uk_target_fit` dispositions (issue comment +5427936411), and reverted on the receipts: a raise to 0.85 leaves every +failing UC caseload cell unchanged (`dwp.uc.households` -44.9% at both +rates) while destroying the legacy housing-benefit surface +(`obr.housing_benefit` -0.0% at 0.55 vs -52.4% at 0.85 - the raise moves +claimants off legacy benefits) and perturbing the QRF predictor surface +through engine-computed `household_net_income`. The contract entry stays +frozen at 0.55 and now records the run; the receipts live in the 757-swap +acceptance evidence. The binding constraint on UC caseload is capital-test +support (microcosm#750), not take-up support. diff --git a/packages/microcosm-build/src/microcosm/build/gate_battery.py b/packages/microcosm-build/src/microcosm/build/gate_battery.py index d3cc8561..58de3281 100644 --- a/packages/microcosm-build/src/microcosm/build/gate_battery.py +++ b/packages/microcosm-build/src/microcosm/build/gate_battery.py @@ -835,6 +835,24 @@ def _next_phase(self) -> str | None: return phase return None + @property + def gates_manifest_sha256(self) -> str: + """Canonical digest of the scoped manifest this run attests.""" + + return self._gates_manifest_sha256 + + @property + def spec_fingerprint(self) -> str: + """Composition fingerprint derived from the manifest digest.""" + + return self._spec_fingerprint + + @property + def policy_sha256(self) -> str: + """Canonical digest of the declared gate policy entries.""" + + return self._policy_sha256() + def run_phase(self, phase: str, context: EvidenceContext) -> GatePhaseReport: """Evaluate one phase and persist the full report before returning. diff --git a/packages/microcosm-build/src/microcosm/build/uk/calibration_measure_exclusions.json b/packages/microcosm-build/src/microcosm/build/uk/calibration_measure_exclusions.json index 21d0ac05..0a12cf65 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/calibration_measure_exclusions.json +++ b/packages/microcosm-build/src/microcosm/build/uk/calibration_measure_exclusions.json @@ -45,6 +45,384 @@ "adjudication": "microcosm#757 (the #743-audit adjudication, issue comment 5413502559)", "approved_on": "2026-08-25", "expires_on": "2026-11-25" + }, + { + "name": "hmrc/employment_income_income_band_1_000_000_to_inf", + "reason": "Zero-support channel: the SPI-derived spine carries 2 records above 1m total income (measured on candidate and incumbent alike, the #757 A2 support-channel measurement), so the cell cannot be reached by reweighting; nine of the eleven cells sit at -100% with final estimate 0.0 on the rebind packet.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/employment_income_count_income_band_1_000_000_to_inf", + "reason": "Zero-support channel: the SPI-derived spine carries 2 records above 1m total income (measured on candidate and incumbent alike, the #757 A2 support-channel measurement), so the cell cannot be reached by reweighting; nine of the eleven cells sit at -100% with final estimate 0.0 on the rebind packet.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/dividend_income_income_band_1_000_000_to_inf", + "reason": "Zero-support channel: the SPI-derived spine carries 2 records above 1m total income (measured on candidate and incumbent alike, the #757 A2 support-channel measurement), so the cell cannot be reached by reweighting; nine of the eleven cells sit at -100% with final estimate 0.0 on the rebind packet.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/dividend_income_count_income_band_1_000_000_to_inf", + "reason": "Zero-support channel: the SPI-derived spine carries 2 records above 1m total income (measured on candidate and incumbent alike, the #757 A2 support-channel measurement), so the cell cannot be reached by reweighting; nine of the eleven cells sit at -100% with final estimate 0.0 on the rebind packet.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/property_income_count_income_band_1_000_000_to_inf", + "reason": "Zero-support channel: the SPI-derived spine carries 2 records above 1m total income (measured on candidate and incumbent alike, the #757 A2 support-channel measurement), so the cell cannot be reached by reweighting; nine of the eleven cells sit at -100% with final estimate 0.0 on the rebind packet.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/private_pension_income_income_band_1_000_000_to_inf", + "reason": "Zero-support channel: the SPI-derived spine carries 2 records above 1m total income (measured on candidate and incumbent alike, the #757 A2 support-channel measurement), so the cell cannot be reached by reweighting; nine of the eleven cells sit at -100% with final estimate 0.0 on the rebind packet.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/private_pension_income_count_income_band_1_000_000_to_inf", + "reason": "Zero-support channel: the SPI-derived spine carries 2 records above 1m total income (measured on candidate and incumbent alike, the #757 A2 support-channel measurement), so the cell cannot be reached by reweighting; nine of the eleven cells sit at -100% with final estimate 0.0 on the rebind packet.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/state_pension_income_band_1_000_000_to_inf", + "reason": "Zero-support channel: the SPI-derived spine carries 2 records above 1m total income (measured on candidate and incumbent alike, the #757 A2 support-channel measurement), so the cell cannot be reached by reweighting; nine of the eleven cells sit at -100% with final estimate 0.0 on the rebind packet.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/state_pension_count_income_band_1_000_000_to_inf", + "reason": "Zero-support channel: the SPI-derived spine carries 2 records above 1m total income (measured on candidate and incumbent alike, the #757 A2 support-channel measurement), so the cell cannot be reached by reweighting; nine of the eleven cells sit at -100% with final estimate 0.0 on the rebind packet.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/self_employment_income_income_band_1_000_000_to_inf", + "reason": "Zero-support channel: the SPI-derived spine carries 2 records above 1m total income (measured on candidate and incumbent alike, the #757 A2 support-channel measurement), so the cell cannot be reached by reweighting; nine of the eleven cells sit at -100% with final estimate 0.0 on the rebind packet.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/self_employment_income_count_income_band_1_000_000_to_inf", + "reason": "Zero-support channel: the SPI-derived spine carries 2 records above 1m total income (measured on candidate and incumbent alike, the #757 A2 support-channel measurement), so the cell cannot be reached by reweighting; nine of the eleven cells sit at -100% with final estimate 0.0 on the rebind packet.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "slc.repayments.england_postgraduate", + "reason": "Zero-support channel: no spine record carries postgraduate-plan repayments (-100%, final estimate 0.0 on the rebind packet); the student-loans stage assigns plans from the SLC liable-stocks resource and the postgraduate stock finds no eligible carriers at spine grain.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "slc.repayments.england_plan_5", + "reason": "Structural under-support: the E8 PLAN_5 top-up saturates the eligible pool (rate 1.0), so the plan-5 repayment mass is bound by frame composition, not by weighting (-84.4% on the rebind packet).", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "slc.borrowers.plan_5_liable", + "reason": "Structural under-support: the E8 PLAN_5 top-up saturates the eligible pool (rate 1.0, final England 43,055 vs 230,000 stock); the liable count is bound by frame composition, not by weighting (-81.3% on the rebind packet).", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/SINGLE_annual_payment_27_600_to_28_800", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/COUPLE_NO_CHILDREN_annual_payment_26_400_to_27_600", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/LONE_PARENT_annual_payment_13_200_to_14_400", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/LONE_PARENT_annual_payment_15_600_to_16_800", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/SINGLE_annual_payment_9_600_to_10_800", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/LONE_PARENT_annual_payment_18_000_to_19_200", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/LONE_PARENT_annual_payment_16_800_to_18_000", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/LONE_PARENT_annual_payment_19_200_to_20_400", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/LONE_PARENT_annual_payment_14_400_to_15_600", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/LONE_PARENT_annual_payment_12_000_to_13_200", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/SINGLE_annual_payment_14_400_to_15_600", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/LONE_PARENT_annual_payment_10_800_to_12_000", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/SINGLE_annual_payment_13_200_to_14_400", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/SINGLE_annual_payment_21_600_to_22_800", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/SINGLE_annual_payment_8_400_to_9_600", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "dwp/uc_payment_dist/LONE_PARENT_annual_payment_21_600_to_22_800", + "reason": "One-sided undershoot: the annual-payment band composition inside the UC caseload is unreachable under the spine's current UC support (the #623-carried catalogue); every cell in this class undershoots on the rebind packet and two sit at -100% with final estimate 0.0. The payment distribution needs element/taper composition support beyond the would_claim_uc take-up lever.", + "tracking": "uk-data#452", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "obr.universal_credit_in_cap", + "reason": "The OBR welfare-cap boundary (UC in-cap vs outside-cap) is not measurable on the frame: the model cannot attribute UC spend to the cap concept, so the pair splits one well-measured total into two mis-attributed halves (-96.8% / +286.3% on the rebind packet while total UC spend is within band of the OBR anchor).", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "obr.universal_credit_outside_cap", + "reason": "The OBR welfare-cap boundary (UC in-cap vs outside-cap) is not measurable on the frame: the model cannot attribute UC spend to the cap concept, so the pair splits one well-measured total into two mis-attributed halves (-96.8% / +286.3% on the rebind packet while total UC spend is within band of the OBR anchor).", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "ons.household_composition.multi_family_households", + "reason": "The ONS household-composition split requires a relationship-to-head frame column the spine does not carry; without it multi-family, unrelated-adult, and lone-parent-with-non-dependent-children households are indistinguishable and the categories bleed into each other (multi_family +1618% on the rebind packet).", + "tracking": "microcosm#791", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "ons.household_composition.unrelated_adult_households", + "reason": "The ONS household-composition split requires a relationship-to-head frame column the spine does not carry; without it multi-family, unrelated-adult, and lone-parent-with-non-dependent-children households are indistinguishable and the categories bleed into each other (multi_family +1618% on the rebind packet).", + "tracking": "microcosm#791", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "ons.household_composition.lone_parent_non_dependent_children_households", + "reason": "The ONS household-composition split requires a relationship-to-head frame column the spine does not carry; this cell fits numerically (-0.03% on the rebind packet) but measures the wrong concept - the solver holds it by stuffing misclassified households into the sibling categories, so it must unbind together with them.", + "tracking": "microcosm#791", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/dividend_income_income_band_500_000_to_1_000_000", + "reason": "Sparse cell support: the frame carries too few records in this income-band cell for the solver to hit the published value without distorting neighbouring cells; a member of the stable six-cell sparse-band set present on both the live-proof and rebind packets.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/private_pension_income_count_income_band_100_000_to_150_000", + "reason": "Sparse cell support: the frame carries too few records in this income-band cell for the solver to hit the published value without distorting neighbouring cells; a member of the stable six-cell sparse-band set present on both the live-proof and rebind packets.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/property_income_count_income_band_500_000_to_1_000_000", + "reason": "Sparse cell support: the frame carries too few records in this income-band cell for the solver to hit the published value without distorting neighbouring cells; a member of the stable six-cell sparse-band set present on both the live-proof and rebind packets.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/self_employment_income_count_income_band_500_000_to_1_000_000", + "reason": "Sparse cell support: the frame carries too few records in this income-band cell for the solver to hit the published value without distorting neighbouring cells; a member of the stable six-cell sparse-band set present on both the live-proof and rebind packets.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/self_employment_income_income_band_50_000_to_70_000", + "reason": "Sparse cell support: the frame carries too few records in this income-band cell for the solver to hit the published value without distorting neighbouring cells; a member of the stable six-cell sparse-band set present on both the live-proof and rebind packets.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "hmrc/state_pension_income_band_50_000_to_70_000", + "reason": "Sparse cell support: the frame carries too few records in this income-band cell for the solver to hit the published value without distorting neighbouring cells; a member of the stable six-cell sparse-band set present on both the live-proof and rebind packets.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" + }, + { + "name": "obr.fuel_duties", + "reason": "Universe scope: the OBR fact is all-road-users receipts (including freight and business mileage) against a household frame, compounded by LCFS diary under-capture at 37% of implied litres; the binding is verified correct and the gap is scope, not measurement. The household-incidence-vs-total-receipts ledger retarget question is pending chronicle-side adjudication.", + "tracking": "microcosm#757", + "approved_by": "juaristi22", + "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", + "approved_on": "2026-08-26", + "expires_on": "2026-11-26" } ] } diff --git a/packages/microcosm-build/src/microcosm/build/uk/gates.json b/packages/microcosm-build/src/microcosm/build/uk/gates.json index 099d2142..ed3447da 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/gates.json +++ b/packages/microcosm-build/src/microcosm/build/uk/gates.json @@ -659,10 +659,10 @@ "parameters": { "reviewed_exclusions_resource": "qrf_tail_reviewed_exclusions.json", "top_k": 100, - "max_top_share": 0.9970712395200448, - "min_nonzero_records": 274 + "max_top_share": 0.9994670564654868, + "min_nonzero_records": 104 }, - "notes": "Weighted tail-concentration audit of the QRF-imputed output columns derived from the HMRC source manifest. Thresholds sit at the measured edges from the microcosm#630 measurement pass: top_k 100 as the grid anchor, max_top_share at the exact measured maximum, and min_nonzero_records at the thinnest measured column, with no headroom. A future thinner column goes thin visibly in the signed details, never a silent pass. Reviewed exclusions live in the named register resource of this package." + "notes": "Weighted tail-concentration audit of the QRF-imputed output columns derived from the HMRC source manifest. Armed from the #686 L3 baselines per #757 B4, each threshold recording the run that measured it: the 47-column qrf_tail grids of uk_weighted_integrity_baselines_686.json (sha256 8b3f6c4e9c522445bf3e05d3451ee8557f4e14d25524ceac3af8bfe05401a146, licensed acceptance dir 686-spine-swap), measured on spine-a.h5 (sha256 a65f2132736d1dcecb91709a513a0d54a5887635ea03760629cc888d188ee306, 113,649 person rows, design weights) for the #609/#578 threshold adjudication. top_k 100 is the measurement grid anchor [10, 100, 500, 1000]; max_top_share is the exact measured maximum over the checked surface (hmrc_spi_other_social_security_income, 104 carriers), no headroom; min_nonzero_records is the thinnest measured column above the grid anchor (the same column) - the policy domain requires min_nonzero_records > top_k, so the three saturated sub-anchor columns (hmrc_spi_taxable_termination_pay 12, charitable_investment_gifts 22, sda_reported 24 carriers, each top-100 share 1.0) sit below it and go thin visibly in the signed details on every run, never a silent pass. The baselines are design-weight measurements and the terminal gate runs on the calibrated release frame; a calibrated-weight breach names its column and is a finding, not noise. The 12 household_qrf_tail grids (was_wealth surface) are measured in the same baselines file and not yet armed - a household-surface gate entry is a declared follow-up. Reviewed exclusions live in the named register resource of this package." } ] } diff --git a/packages/microcosm-build/src/microcosm/build/uk/ledger_compile_parity_incumbent_2025_signed_differences.json b/packages/microcosm-build/src/microcosm/build/uk/ledger_compile_parity_incumbent_2025_signed_differences.json index 399a3c6f..ab237d33 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/ledger_compile_parity_incumbent_2025_signed_differences.json +++ b/packages/microcosm-build/src/microcosm/build/uk/ledger_compile_parity_incumbent_2025_signed_differences.json @@ -1,11 +1,11 @@ { - "compiled_count": 388, + "compiled_count": 408, "counts_by_kind": { - "calibration_drift": 163, - "fixture_only": 287, + "calibration_drift": 170, + "fixture_only": 267, "ledger_only": 38 }, - "difference_count": 488, + "difference_count": 475, "differences": [ { "fixture_value": 1608000.0, @@ -1225,146 +1225,6 @@ "period": 2025, "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." }, - { - "fixture_value": 498707.0, - "kind": "fixture_only", - "name": "scotgov.council_tax_stock.band_a", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 583705.0, - "kind": "fixture_only", - "name": "scotgov.council_tax_stock.band_b", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 426388.0, - "kind": "fixture_only", - "name": "scotgov.council_tax_stock.band_c", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 369621.0, - "kind": "fixture_only", - "name": "scotgov.council_tax_stock.band_d", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 366881.0, - "kind": "fixture_only", - "name": "scotgov.council_tax_stock.band_e", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 221282.0, - "kind": "fixture_only", - "name": "scotgov.council_tax_stock.band_f", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 142084.0, - "kind": "fixture_only", - "name": "scotgov.council_tax_stock.band_g", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 14481.0, - "kind": "fixture_only", - "name": "scotgov.council_tax_stock.band_h", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 2623149.0, - "kind": "fixture_only", - "name": "scotgov.council_tax_stock.total", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 471000000.0, - "kind": "fixture_only", - "name": "scotgov.scottish_child_payment_spending", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 3985000.0, - "kind": "fixture_only", - "name": "slc.borrowers.plan_2_above_threshold", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 8940000.0, - "kind": "fixture_only", - "name": "slc.borrowers.plan_2_liable", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 0.0, - "kind": "fixture_only", - "name": "slc.borrowers.plan_5_above_threshold", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 10000.0, - "kind": "fixture_only", - "name": "slc.borrowers.plan_5_liable", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 18611.0, - "kind": "fixture_only", - "name": "slc.support.adult_dependants_grant_recipients", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 55364917.0, - "kind": "fixture_only", - "name": "slc.support.adult_dependants_grant_spend", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 1159761.0, - "kind": "fixture_only", - "name": "slc.support.maintenance_loan_recipients", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 8591659718.0, - "kind": "fixture_only", - "name": "slc.support.maintenance_loan_spend", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 99645.0, - "kind": "fixture_only", - "name": "slc.support.parents_learning_allowance_recipients", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 181421659.0, - "kind": "fixture_only", - "name": "slc.support.parents_learning_allowance_spend", - "period": 2025, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, { "fixture_value": 181700000.0, "kind": "fixture_only", @@ -3570,6 +3430,38 @@ "period": 2025, "reason": "Ledger-compiled value differs from the fixture value at this comparison period." }, + { + "fixture_value": 3985000.0, + "kind": "calibration_drift", + "ledger_value": 4460000.0, + "name": "slc.borrowers.plan_2_above_threshold", + "period": 2025, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 8940000.0, + "kind": "calibration_drift", + "ledger_value": 9710000.0, + "name": "slc.borrowers.plan_2_liable", + "period": 2025, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 0.0, + "kind": "calibration_drift", + "ledger_value": 35000.0, + "name": "slc.borrowers.plan_5_above_threshold", + "period": 2025, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 10000.0, + "kind": "calibration_drift", + "ledger_value": 230000.0, + "name": "slc.borrowers.plan_5_liable", + "period": 2025, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, { "fixture_value": 346409713.95, "kind": "calibration_drift", @@ -3585,6 +3477,30 @@ "name": "slc.repayments.england_total_higher_education", "period": 2025, "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 55364917.0, + "kind": "calibration_drift", + "ledger_value": 55364916.81, + "name": "slc.support.adult_dependants_grant_spend", + "period": 2025, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 8591659718.0, + "kind": "calibration_drift", + "ledger_value": 8591659718.080004, + "name": "slc.support.maintenance_loan_spend", + "period": 2025, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 181421659.0, + "kind": "calibration_drift", + "ledger_value": 181421659.32, + "name": "slc.support.parents_learning_allowance_spend", + "period": 2025, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." } ], "fixture_count": 637 diff --git a/packages/microcosm-build/src/microcosm/build/uk/ledger_compile_parity_production_2023_signed_differences.json b/packages/microcosm-build/src/microcosm/build/uk/ledger_compile_parity_production_2023_signed_differences.json index b796fe2d..efe5675c 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/ledger_compile_parity_production_2023_signed_differences.json +++ b/packages/microcosm-build/src/microcosm/build/uk/ledger_compile_parity_production_2023_signed_differences.json @@ -1,8 +1,8 @@ { - "compiled_count": 211, + "compiled_count": 217, "counts_by_kind": { - "calibration_drift": 6, - "fixture_only": 131, + "calibration_drift": 12, + "fixture_only": 125, "ledger_only": 193 }, "difference_count": 330, @@ -882,48 +882,6 @@ "period": 2023, "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." }, - { - "fixture_value": 20226.0, - "kind": "fixture_only", - "name": "slc.support.adult_dependants_grant_recipients", - "period": 2023, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 54638997.0, - "kind": "fixture_only", - "name": "slc.support.adult_dependants_grant_spend", - "period": 2023, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 1151607.0, - "kind": "fixture_only", - "name": "slc.support.maintenance_loan_recipients", - "period": 2023, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 8594103415.0, - "kind": "fixture_only", - "name": "slc.support.maintenance_loan_spend", - "period": 2023, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 89283.0, - "kind": "fixture_only", - "name": "slc.support.parents_learning_allowance_recipients", - "period": 2023, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, - { - "fixture_value": 153266251.0, - "kind": "fixture_only", - "name": "slc.support.parents_learning_allowance_spend", - "period": 2023, - "reason": "Fixture row has no Ledger-compiled counterpart at this comparison period." - }, { "kind": "ledger_only", "ledger_value": 65937000000.0, @@ -2322,6 +2280,54 @@ "name": "ons.public_sector_employment", "period": 2023, "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 20226.0, + "kind": "calibration_drift", + "ledger_value": 17960.0, + "name": "slc.support.adult_dependants_grant_recipients", + "period": 2023, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 54638997.0, + "kind": "calibration_drift", + "ledger_value": 51719575.64, + "name": "slc.support.adult_dependants_grant_spend", + "period": 2023, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 1151607.0, + "kind": "calibration_drift", + "ledger_value": 1154427.0, + "name": "slc.support.maintenance_loan_recipients", + "period": 2023, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 8594103415.0, + "kind": "calibration_drift", + "ledger_value": 8881701386.559977, + "name": "slc.support.maintenance_loan_spend", + "period": 2023, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 89283.0, + "kind": "calibration_drift", + "ledger_value": 95287.0, + "name": "slc.support.parents_learning_allowance_recipients", + "period": 2023, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." + }, + { + "fixture_value": 153266251.0, + "kind": "calibration_drift", + "ledger_value": 168349636.59, + "name": "slc.support.parents_learning_allowance_spend", + "period": 2023, + "reason": "Ledger-compiled value differs from the fixture value at this comparison period." } ], "fixture_count": 149 diff --git a/packages/microcosm-build/src/microcosm/build/uk/spine_candidate_acceptance.json b/packages/microcosm-build/src/microcosm/build/uk/spine_candidate_acceptance.json index 3c91f935..3c1c7b60 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/spine_candidate_acceptance.json +++ b/packages/microcosm-build/src/microcosm/build/uk/spine_candidate_acceptance.json @@ -6,9 +6,9 @@ "household": 52846, "person": 113649 }, - "name": "spine-g", - "sha256": "02900abeec29d52596f16c9e29b2a8991e0acd5f5118c3be28a50ca77403787b", - "sidecar_sha256": "d5f4dc645c130484b05d8d7ef00f3f81ffb9c3633eff305bc9e6754c52b1ec0e", + "name": "spine-i", + "sha256": "0cce03992207b4e16d96483642e2edd943fd25112a6425208d748b31c7742416", + "sidecar_sha256": "615de0a428538b0005c050cdd18982d32903b75ab7fd95f55edcc60958d92fee", "stage_count": 25, "stage_roster": [ "frs_spine", @@ -66,13 +66,15 @@ "schema_version": 1, "spine_battery": { "blocked_at_phase": null, - "report_sha256": "57c4a44d9627b1e47b7a94438a71bc0d96de06073931e37ff096f1f988c3c58b", + "release_candidate": true, + "report_sha256": "0ddd22c15fab204c5ca9b7992f736795107f4fc804c811cfb7e16f197065e83a", + "signed": true, "statuses": { "passed": 14 } }, "strict_parity": { - "receipt_sha256": "8fff9bf6952f6985eb71b8e81d6adad8dfb862810a7c8e4c5da7d2c278d13bdd", + "receipt_sha256": "f020df07cc6bf8c535671475c0655b49f99d75e19cc014d69ba8de3cb1579756", "share_band": { "contract": 0.02, "effective": 0.02 @@ -82,9 +84,9 @@ "verdict": "signed_parity" }, "twin": { - "name": "spine-e (and spine-d before it)", - "note": "Three code vintages, one payload: spine-d (pre-evidence-layer), spine-e (pre-battery), spine-g (full battery armed) are pairwise payload_identical across all tables, keys and root attrs \u2014 the twin-determinism receipt and the proof that receipts and gates never moved a byte of the artifact.", + "name": "spine-g (and spine-d/e before it)", + "note": "spine-i rebuilds the candidate at the #757 follow-up tip (release-cut certification machinery, exclusion pass, QRF tail re-arm, lever run reverted to 0.55): payload-identical to spine-g, whose own d/e/g twins were pairwise payload-identical. The battery ran at release-candidate strictness and the report is signed. The would_claim_uc lever build (spine-h, 0.85) is a measured-and-reverted receipt in the acceptance evidence, not a candidate.", "payload_identical": true, - "sha256": "3c8799970851c409e4cb8578d33a180acb30ae600f4bd99ca3a190f9c5eb870a" + "sha256": "02900abeec29d52596f16c9e29b2a8991e0acd5f5118c3be28a50ca77403787b" } } diff --git a/packages/microcosm-build/src/microcosm/build/uk/take_up_contract.json b/packages/microcosm-build/src/microcosm/build/uk/take_up_contract.json index 72c6f4d6..a48ce8ec 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/take_up_contract.json +++ b/packages/microcosm-build/src/microcosm/build/uk/take_up_contract.json @@ -67,12 +67,12 @@ "2015-01-01": 0.55 }, "source": { - "source": "U8 adjudication and upstream issue uk-data#452", + "source": "U8 adjudication and upstream issue uk-data#452; lever run receipts in data/ukds/acceptance/757-swap (2026-08-26)", "agency": "PolicyEngine", - "citation": "Frozen at 0.55 by adjudication for parity with the incumbent UK pipeline.", + "citation": "Frozen at 0.55 by adjudication for parity with the incumbent UK pipeline. The pre-registered U8 lever (a raise to 0.85 at build year 2024) was run and measured on 2026-08-26 per the #757 uk_target_fit dispositions and REVERTED on the receipts: the raise left every failing UC caseload cell unchanged (dwp.uc.households -44.9% at both rates) while destroying the legacy housing-benefit surface (obr.housing_benefit -0.0% at 0.55, -52.4% at 0.85 - the raise moves claimants off legacy benefits) and polluting the QRF predictor surface through engine-computed household_net_income. The binding constraint on UC caseload is capital-test support (uk-data#452 mechanism 2, microcosm#750), not take-up support.", "status": "frozen_by_adjudication", "freeze": { - "decision": "U8", + "decision": "U8 (re-opened and re-frozen by the 2026-08-26 lever run)", "followup": "uk-data#452" } } diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py index 416e1ad6..5bb27c48 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py @@ -204,7 +204,6 @@ def run_uk_calibration( measure_resolver: object | None, source_pins: Mapping[str, Mapping[str, object]], run_config_extra: Mapping[str, object], - release_candidate: bool, release_id: str, logbook_prev_row_digest: str | None = None, ) -> UKCalibrationRunResult: @@ -253,7 +252,6 @@ def run_uk_calibration( doctrine_overrides=doctrine_overrides, measure_resolver=measure_resolver, source_pins=source_pins, - release_candidate=release_candidate, release_id=release_id, state=state, run_config=run_config, @@ -344,7 +342,6 @@ def _run_uk_calibration_attempt( doctrine_overrides: Mapping[str, Mapping[str, object]], measure_resolver: object | None, source_pins: Mapping[str, Mapping[str, object]], - release_candidate: bool, release_id: str, state: AttemptState, run_config: Mapping[str, object], @@ -421,7 +418,6 @@ def _run_uk_calibration_attempt( calibrated, stage, paths.terminal_gate_json, - release_candidate=release_candidate, release_id=release_id, diagnostics_sha256=diagnostics_sha, ) @@ -448,11 +444,15 @@ def _run_uk_calibration_attempt( "register": build_block["register"], "calibration": stage.manifest, "gate_summary": _gate_summary(gate_report), - "shippable": False, - "shippable_reason": ( - "calibration-scoped battery; release certification is the " - "release-cut producer's job" - ), + # No shippability claim lives here: the calibration-scoped battery + # covers 6 of the declared gate entries. The release verdict is the + # release-cut certification's, produced over this record. + "certification": { + "expected_artifact": str( + paths.staging_h5.with_suffix(".release_certification.json") + ), + "producer": "tools/certify_uk_release_cut.py", + }, "artifacts": { "staging_h5": {"path": str(paths.staging_h5), "sha256": staging_sha}, "diagnostics_json": { @@ -500,12 +500,11 @@ def _run_calibration_gate_battery( stage: UKNationalCalibrationStage, path: Path, *, - release_candidate: bool, release_id: str, diagnostics_sha256: str, ) -> dict[str, object]: manifest = _calibration_gate_manifest() - admin_totals, admin_receipt = _aggregate_admin_totals(frame, manifest) + admin_totals, admin_receipt = uk_aggregate_admin_totals(frame, manifest) artifacts = { "national_calibration": stage.manifest, "parity_evidence": SimpleNamespace( @@ -519,18 +518,24 @@ def _run_calibration_gate_battery( battery = GateBatteryRun( manifest, release_id=release_id, + # The seam never runs release-candidate posture: its scoped battery + # covers 6 of the declared entries and must never sign a + # shippability claim (the #757 release-cut audit). Shippability + # comes only from the release-cut certification. report_path=path, - release_candidate=release_candidate, + release_candidate=False, registry=UK_GATE_REGISTRY, release_evidence={"calibration_diagnostics_sha256": diagnostics_sha256}, ) battery.run_phase("terminal", EvidenceContext(frame=frame, artifacts=artifacts)) battery.enforce("terminal", mode=BlockingMode.BLOCKS_ARTIFACT) payload = battery.report_payload() - payload["posture"] = "calibration_seam" - payload["scope_exclusions"] = dict(UK_CALIBRATION_GATE_SCOPE_EXCLUSIONS) - payload["aggregate_admin_measurement"] = admin_receipt - _resign_gate_report(payload) + finalize_uk_scoped_gate_report( + payload, + posture="calibration_seam", + scope_exclusions=dict(UK_CALIBRATION_GATE_SCOPE_EXCLUSIONS), + aggregate_admin_measurement=admin_receipt, + ) _write_json(path, payload) return payload @@ -667,28 +672,30 @@ def _spine_provenance_from_sidecar( def _calibration_gate_manifest() -> GatesManifest: - return _scoped_gate_manifest( + return uk_scoped_gate_manifest( UK_CALIBRATION_GATE_SCOPE, phases=("terminal",), policy_suffix="calibration_seam_scope", ) -def _spine_gate_manifest() -> GatesManifest: - return _scoped_gate_manifest( - UK_SPINE_GATE_SCOPE, - phases=("assembled", "transferred"), - policy_suffix="spine_build_scope", - ) - - -def _scoped_gate_manifest( - scope: tuple[str, ...], +def uk_scoped_gate_manifest( + scope: tuple[str, ...] | frozenset[str], *, phases: tuple[str, ...], policy_suffix: str, + source: GatesManifest | None = None, ) -> GatesManifest: - source = load_country_spec("uk").gates + """Filter the declared UK gate spec to one battery's scope. + + The one scope-filtering implementation every scoped producer shares + (spine build, calibration seam, release cut). ``source`` lets a caller + that already holds a loaded spec — or a hermetic test that stubs one — + supply it; the default is the committed package spec. + """ + + if source is None: + source = load_country_spec("uk").gates entries = tuple(entry for entry in source.gates if entry.id in scope) missing = sorted(set(scope) - {entry.id for entry in entries}) if missing: @@ -712,7 +719,7 @@ def _scoped_gate_manifest( } -def _aggregate_admin_totals( +def uk_aggregate_admin_totals( frame: Frame, manifest: GatesManifest ) -> tuple[dict[str, float], list[dict[str, object]]]: """Measure every declared admin anchor, fail-loud on absent evidence. @@ -873,7 +880,29 @@ def _gate_summary(report: Mapping[str, object]) -> dict[str, object]: } -def _resign_gate_report(payload: dict[str, object]) -> None: +def finalize_uk_scoped_gate_report( + payload: dict[str, object], + *, + posture: str, + scope_exclusions: Mapping[str, str], + aggregate_admin_measurement: object, +) -> None: + """Graft the scoped-report trio onto a battery payload and re-sign it. + + Every scoped UK producer (the calibration seam, the release-cut + certification) declares its posture, the rationale for each gate it + does not run, and its admin-anchor measurement receipt, then signs the + augmented bytes. One implementation, shared, so the parts the + certification composes over cannot drift apart in shape. + """ + + payload["posture"] = posture + payload["scope_exclusions"] = dict(scope_exclusions) + payload["aggregate_admin_measurement"] = aggregate_admin_measurement + resign_uk_gate_report(payload) + + +def resign_uk_gate_report(payload: dict[str, object]) -> None: attestation = payload.get("attestation") if not isinstance(attestation, dict): raise RuntimeError("gate report has no attestation block.") diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/release_certification.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/release_certification.py new file mode 100644 index 00000000..1e996488 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/release_certification.py @@ -0,0 +1,785 @@ +"""UK release-cut certification: the national battery's runner and composer. + +The June national driver retired with microcosm#757 and took the only +executor of the 16 declared national preflight/terminal gates with it. +This module is their executable home (issue #757 item B5): a scoped +``GateBatteryRun`` over ``UK_NATIONAL_GATE_SCOPE`` evaluated against the +calibrated candidate, plus the **multi-part release certification** the +2026-08-25 audit (issue comment 5413502559) specified — the spine build's +battery report, the calibration seam's battery report, and the release-cut +battery report must union to the full declared gate-entry set with no gap +and no overlap beyond ``UK_SHARED_GATE_IDS``, each part signed by its +producer, with the phase and digest checks moving from per-report to +per-certification. A candidate's shippability verdict comes only from the +certification, never from a single scoped report. + +Evidence adaptation only, never verdict re-implementation: every gate in +the release-cut battery runs the same ``UK_GATE_REGISTRY`` binding the June +runner used; this module reconstructs the evidence the retired runner drew +from live stage objects out of the artifacts the split pipeline persists +(the spine build sidecar, the seam's diagnostics and build record, the +per-run licensed input-mass reference). +""" + +from __future__ import annotations + +import base64 +import hashlib +import hmac +import json +import os +from collections.abc import Mapping, Sequence +from datetime import date +from functools import lru_cache +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +from microcosm.build.country_spec import GatesManifest, load_country_spec +from microcosm.build.gate_battery import ( + BlockingMode, + EvidenceContext, + GateBatteryRun, + gate_signing_key_env, +) +from microcosm.build.gates import FitWeightRecord +from microcosm.build.logbook import canonical_json_bytes +from microcosm.build.uk_runtime.battery_bindings import UK_GATE_REGISTRY +from microcosm.build.uk_runtime.calibration_run import ( + UK_CALIBRATION_GATE_SCOPE, + UK_NATIONAL_GATE_SCOPE, + UK_SHARED_GATE_IDS, + UK_SPINE_GATE_SCOPE, + finalize_uk_scoped_gate_report, + uk_aggregate_admin_totals, + uk_scoped_gate_manifest, +) + +__all__ = [ + "UK_RELEASE_CERTIFICATION_KIND", + "UK_RELEASE_CERTIFICATION_SCHEMA_VERSION", + "UK_RELEASE_CUT_POSTURE", + "UKReleaseCertificationError", + "compose_uk_release_certification", + "rehydrate_uk_fit_weight_records", + "run_uk_release_cut_battery", + "uk_national_gate_manifest", + "uk_release_cut_scope_exclusions", + "uk_release_parity_evidence", +] + +UK_RELEASE_CERTIFICATION_SCHEMA_VERSION = 1 +UK_RELEASE_CERTIFICATION_KIND = "uk_release_certification" +UK_RELEASE_CUT_POSTURE = "release_cut" + +#: Each certification part's declared scope, phases, and manifest policy +#: suffix. The suffixes are load-bearing: they are what the producers bake +#: into their scoped manifests, so the re-derived digests only match a part +#: that really ran the committed spec under its declared scope. +_PART_SCOPES: Mapping[str, Mapping[str, object]] = { + "spine": { + "scope": UK_SPINE_GATE_SCOPE, + "phases": ("assembled", "transferred"), + "policy_suffix": "spine_build_scope", + "posture": None, + }, + "calibration_seam": { + "scope": UK_CALIBRATION_GATE_SCOPE, + "phases": ("terminal",), + "policy_suffix": "calibration_seam_scope", + "posture": "calibration_seam", + }, + "release_cut": { + "scope": UK_NATIONAL_GATE_SCOPE, + "phases": ("preflight", "terminal"), + "policy_suffix": "release_cut_scope", + "posture": UK_RELEASE_CUT_POSTURE, + }, +} + + +class UKReleaseCertificationError(ValueError): + """A certification refusal: the parts do not certify the candidate.""" + + +@lru_cache(maxsize=1) +def _uk_gates_spec() -> GatesManifest: + # The committed spec is immutable within a process; the composer derives + # digests for three scopes per certification, so one validated load + # serves them all. + return load_country_spec("uk").gates + + +@lru_cache(maxsize=1) +def uk_national_gate_manifest() -> GatesManifest: + """The release-cut battery's scoped manifest (preflight + terminal).""" + + return uk_scoped_gate_manifest( + UK_NATIONAL_GATE_SCOPE, + phases=("preflight", "terminal"), + policy_suffix="release_cut_scope", + ) + + +def uk_release_cut_scope_exclusions() -> dict[str, str]: + """Why each declared gate outside the national scope is not run here.""" + + spec = _uk_gates_spec() + exclusions: dict[str, str] = {} + for entry in spec.gates: + if entry.id in UK_NATIONAL_GATE_SCOPE: + continue + if entry.id in UK_SPINE_GATE_SCOPE: + exclusions[entry.id] = ( + "spine-construction gate; owned by the spine build's scoped battery." + ) + elif entry.id in UK_CALIBRATION_GATE_SCOPE: + exclusions[entry.id] = ( + "calibration-seam gate; owned by the seam's scoped battery." + ) + else: # pragma: no cover - the three-way partition is import-enforced + raise RuntimeError( + f"UK gate {entry.id!r} belongs to no declared battery scope." + ) + return exclusions + + +def rehydrate_uk_fit_weight_records( + sidecar: Mapping[str, Any], +) -> tuple[FitWeightRecord, ...] | None: + """The weights-audit evidence, rehydrated from the spine build sidecar. + + ``None`` (sidecar carries no ``fit_weight_records`` block — a pre-#757 + spine) leaves the artifact unsupplied so the audit records a named + ``evidence_absent`` gap, which blocks at release-candidate strictness. A + present block with any empty per-stage list coerces to ``()``: a fitting + stage that emitted nothing is a failed audit, never a vacuous pass. + """ + + block = sidecar.get("fit_weight_records") + if block is None: + return None + if not isinstance(block, Mapping): + raise UKReleaseCertificationError( + "spine sidecar fit_weight_records must map stage names to record lists." + ) + collected: list[FitWeightRecord] = [] + empty_stages: list[str] = [] + for stage_name, records in block.items(): + if not isinstance(records, Sequence) or isinstance(records, (str, bytes)): + raise UKReleaseCertificationError( + f"spine sidecar fit_weight_records[{stage_name!r}] must be a " + "list of records; an unreadable block is corruption, not an " + "empty audit." + ) + if not records: + empty_stages.append(str(stage_name)) + continue + for record in records: + if not isinstance(record, Mapping) or not ( + {"fit_name", "weight_kind"} <= set(record) + ): + raise UKReleaseCertificationError( + f"spine sidecar fit_weight_records[{stage_name!r}] carries " + "a malformed record; an unreadable record is corruption, " + "not an empty audit." + ) + collected.append( + FitWeightRecord( + fit_name=str(record["fit_name"]), + weight_kind=str(record["weight_kind"]), + ) + ) + if empty_stages: + # A fitting stage that recorded nothing is a failed audit: hand the + # binding the empty tuple its refusal path exists for. + return () + return tuple(collected) + + +def uk_release_parity_evidence( + frame: Any, + *, + diagnostics_targets: Sequence[Mapping[str, Any]], + reference_registry: Any, + parity_reference: Any, +) -> SimpleNamespace: + """The parity-trio evidence over persisted inputs, sides never aliased. + + Candidate columns come from the staged frame; reference columns from the + frozen parity instrument's declared input entities. Candidate targets + come from the solve's realized diagnostics rows; reference targets from + the independently recompiled (and exclusion-pruned) register at + ``name@period`` grain — the same two-source rule the retired June + runner's ``_stage_parity_evidence`` enforced. + """ + + target_relative_errors: dict[str, float] = {} + for row in diagnostics_targets: + name = str(row["name"]) + if "@" not in name: + raise UKReleaseCertificationError( + f"diagnostics target {name!r} is not labeled at name@period " + "grain; the reference side is keyed name@period, so a " + "bare-name row would silently fall out of the comparison." + ) + target_relative_errors[name] = float(row["relative_error"]) + return SimpleNamespace( + candidate_columns={ + f"{entity}.{column}" + for entity in frame.entities + for column in frame.table(entity).columns + }, + reference_columns={ + f"{entity}.{name}" + for name, entity in parity_reference.input_entities.items() + }, + candidate_targets=set(target_relative_errors), + reference_targets={ + f"{spec.name}@{spec.period}" for spec in reference_registry.specs + }, + target_relative_errors=target_relative_errors, + ) + + +def run_uk_release_cut_battery( + frame: Any, + *, + report_path: Path, + release_id: str, + diagnostics_sha256: str, + coverage_engine: Any, + build_stage_names: Sequence[str], + ledger_registries: Mapping[object, Any], + parity_evidence: Any, + fit_weight_records: tuple[FitWeightRecord, ...] | None, + input_mass_reference: Mapping[str, Any], + exclusions_evaluated_on: date, + gate_registry: Mapping[str, Any] | None = None, +) -> dict[str, Any]: + """Run the 16 national gates over the calibrated candidate, signed. + + Always release-candidate strict: this battery exists to certify a cut, + so an ``evidence_absent`` gap blocks rather than being tolerated, and a + blocked phase persists its report and raises before any composition. + """ + + battery = GateBatteryRun( + uk_national_gate_manifest(), + release_id=release_id, + report_path=report_path, + release_candidate=True, + registry=UK_GATE_REGISTRY if gate_registry is None else gate_registry, + release_evidence={"calibration_diagnostics_sha256": diagnostics_sha256}, + ) + preflight_artifacts: dict[str, Any] = { + "coverage_engine": coverage_engine, + "build_stage_names": tuple(str(name) for name in build_stage_names), + "uk_ledger_compiled_registries": dict(ledger_registries), + } + battery.run_phase("preflight", EvidenceContext(artifacts=preflight_artifacts)) + battery.enforce("preflight", mode=BlockingMode.BLOCKS_ARTIFACT) + + admin_totals, admin_receipt = uk_aggregate_admin_totals( + frame, uk_national_gate_manifest() + ) + terminal_artifacts: dict[str, Any] = { + "coverage_engine": coverage_engine, + "rules_engine": coverage_engine, + "build_stage_names": tuple(str(name) for name in build_stage_names), + "exclusions_evaluated_on": exclusions_evaluated_on, + "parity_evidence": parity_evidence, + "aggregate_admin": admin_totals, + "input_mass_reference": input_mass_reference, + } + if fit_weight_records is not None: + terminal_artifacts["fit_weight_records"] = fit_weight_records + battery.run_phase( + "terminal", EvidenceContext(frame=frame, artifacts=terminal_artifacts) + ) + battery.enforce("terminal", mode=BlockingMode.BLOCKS_ARTIFACT) + payload = battery.report_payload() + finalize_uk_scoped_gate_report( + payload, + posture=UK_RELEASE_CUT_POSTURE, + scope_exclusions=uk_release_cut_scope_exclusions(), + aggregate_admin_measurement=admin_receipt, + ) + _write_json(report_path, payload) + return payload + + +# --------------------------------------------------------------------------- +# The multi-part certification composer +# --------------------------------------------------------------------------- + + +def compose_uk_release_certification( + *, + release_id: str, + candidate_name: str, + candidate_path: Path, + candidate_sha256: str, + spine_report_path: Path, + seam_report_path: Path, + release_cut_report_path: Path, + spine_sidecar: Mapping[str, Any], + build_record: Mapping[str, Any], + score_receipt_path: Path, + exclusions_evaluated_on: date, + certification_path: Path, +) -> dict[str, Any]: + """Verify the three scoped parts and compose the signed certification. + + Every check is a refusal: a certification only exists when the parts + union to the full declared entry set with no gap, no overlap beyond + ``UK_SHARED_GATE_IDS``, full phase coverage, verified signatures, the + committed spec's scoped digests, green release-blocking verdicts, and a + closed identity join from the spine report through the sidecar, the + build record, the diagnostics digest, and the candidate bytes. + """ + + parts_raw = { + "spine": _load_part(spine_report_path), + "calibration_seam": _load_part(seam_report_path), + "release_cut": _load_part(release_cut_report_path), + } + signing_key = _require_signing_key() + declared = _uk_gates_spec() + declared_ids = {entry.id for entry in declared.gates} + declared_phases = tuple(declared.phases) + + part_summaries: dict[str, dict[str, Any]] = {} + for part_name, (payload, raw_bytes) in parts_raw.items(): + spec = _PART_SCOPES[part_name] + _verify_part( + part_name, + payload, + scope=frozenset(spec["scope"]), + phases=tuple(spec["phases"]), + policy_suffix=str(spec["policy_suffix"]), + posture=spec["posture"], + signing_key=signing_key, + ) + part_summaries[part_name] = { + "path": str( + _part_path( + part_name, + spine_report_path, + seam_report_path, + release_cut_report_path, + ) + ), + "sha256": hashlib.sha256(raw_bytes).hexdigest(), + "release_id": str(payload["release_id"]), + "phases": list(payload["phases"]), + "entry_ids": sorted(payload["gates"]), + "gates_manifest_sha256": str(payload["gates_manifest_sha256"]), + "policy_sha256": str(payload["policy_sha256"]), + "statuses": _status_census(payload), + } + + _verify_union( + {name: (payload, raw) for name, (payload, raw) in parts_raw.items()}, + declared_ids=declared_ids, + declared_phases=declared_phases, + ) + _verify_identity_join( + spine_report_bytes=parts_raw["spine"][1], + seam_report_bytes=parts_raw["calibration_seam"][1], + seam_payload=parts_raw["calibration_seam"][0], + release_cut_payload=parts_raw["release_cut"][0], + spine_sidecar=spine_sidecar, + build_record=build_record, + candidate_path=candidate_path, + candidate_sha256=candidate_sha256, + release_id=release_id, + ) + score_receipt_bytes = score_receipt_path.read_bytes() + score_receipt = json.loads(score_receipt_bytes) + _verify_score_receipt(score_receipt, candidate_sha256=candidate_sha256) + + full_digests = _full_manifest_digests() + run_config = build_record.get("run_config", {}) + certification: dict[str, Any] = { + "schema_version": UK_RELEASE_CERTIFICATION_SCHEMA_VERSION, + "kind": UK_RELEASE_CERTIFICATION_KIND, + "country": "uk", + "release_id": release_id, + "candidate": { + "name": candidate_name, + "filename": candidate_path.name, + "sha256": candidate_sha256, + "size_bytes": candidate_path.stat().st_size, + }, + "parts": part_summaries, + "spec": { + "gates_manifest_sha256": full_digests["gates_manifest_sha256"], + "policy_sha256": full_digests["policy_sha256"], + "spec_fingerprint": full_digests["spec_fingerprint"], + "declared_entry_count": len(declared_ids), + "declared_phases": list(declared_phases), + "shared_gate_ids": sorted(UK_SHARED_GATE_IDS), + }, + "doctrine": { + "payload": dict(run_config.get("doctrine", {})), + "overrides": dict(run_config.get("doctrine_overrides", {})), + }, + "diagnostics_sha256": str( + parts_raw["calibration_seam"][0]["release_evidence"][ + "calibration_diagnostics_sha256" + ] + ), + "score_receipt": { + "filename": score_receipt_path.name, + "sha256": hashlib.sha256(score_receipt_bytes).hexdigest(), + }, + "exclusions_evaluated_on": exclusions_evaluated_on.isoformat(), + "shippable": True, + } + _sign_certification(certification, signing_key) + _write_json(certification_path, certification) + return certification + + +# --------------------------------------------------------------------------- +# Refusal helpers +# --------------------------------------------------------------------------- + + +def _part_path( + part_name: str, + spine_report_path: Path, + seam_report_path: Path, + release_cut_report_path: Path, +) -> Path: + return { + "spine": spine_report_path, + "calibration_seam": seam_report_path, + "release_cut": release_cut_report_path, + }[part_name] + + +def _load_part(path: Path) -> tuple[dict[str, Any], bytes]: + if not path.is_file(): + raise UKReleaseCertificationError(f"certification part absent: {path}") + raw = path.read_bytes() + payload = json.loads(raw) + if not isinstance(payload, Mapping): + raise UKReleaseCertificationError( + f"certification part {path} is not a JSON object." + ) + return dict(payload), raw + + +def _require_signing_key() -> bytes: + env_name = gate_signing_key_env("uk") + encoded = os.environ.get(env_name) + if not encoded: + raise UKReleaseCertificationError( + f"{env_name} must be set: a certification and every part it " + "verifies are signed artifacts." + ) + key = base64.b64decode(encoded) + if len(key) != 32: + raise UKReleaseCertificationError( + f"{env_name} must decode to exactly 32 bytes." + ) + return key + + +def _verify_part( + part_name: str, + payload: Mapping[str, Any], + *, + scope: frozenset[str], + phases: tuple[str, ...], + policy_suffix: str, + posture: str | None, + signing_key: bytes, +) -> None: + if payload.get("schema_version") != 4: + raise UKReleaseCertificationError( + f"{part_name}: schema_version must be 4, got " + f"{payload.get('schema_version')!r}." + ) + if payload.get("country") != "uk": + raise UKReleaseCertificationError(f"{part_name}: country must be 'uk'.") + if payload.get("blocked_at_phase") is not None: + raise UKReleaseCertificationError( + f"{part_name}: blocked at phase {payload['blocked_at_phase']!r}; " + "a blocked part cannot certify." + ) + if list(payload.get("phases", ())) != list(phases): + raise UKReleaseCertificationError( + f"{part_name}: phases must be {list(phases)}, got " + f"{payload.get('phases')!r}." + ) + if posture is not None and payload.get("posture") != posture: + raise UKReleaseCertificationError( + f"{part_name}: posture must be {posture!r}, got " + f"{payload.get('posture')!r}." + ) + gates = payload.get("gates") + if not isinstance(gates, Mapping) or set(gates) != set(scope): + missing = sorted(set(scope) - set(gates or ())) + extra = sorted(set(gates or ()) - set(scope)) + raise UKReleaseCertificationError( + f"{part_name}: entry ids must equal the declared scope; " + f"missing {missing}, extra {extra}." + ) + failing = sorted( + gate_id + for gate_id, entry in gates.items() + if entry.get("criticality") == "release_blocking" + and entry.get("status") != "passed" + ) + if failing: + raise UKReleaseCertificationError( + f"{part_name}: release-blocking entries not passed: {failing}." + ) + expected = _scoped_digests(scope, phases=phases, policy_suffix=policy_suffix) + for field in ("gates_manifest_sha256", "policy_sha256"): + if payload.get(field) != expected[field]: + raise UKReleaseCertificationError( + f"{part_name}: {field} does not match the committed spec's " + f"scoped manifest ({payload.get(field)!r} != " + f"{expected[field]!r}); the part did not run the declared " + "gate spec." + ) + _verify_part_signature(part_name, payload, signing_key) + + +def _verify_part_signature( + part_name: str, payload: Mapping[str, Any], signing_key: bytes +) -> None: + attestation = payload.get("attestation") + if not isinstance(attestation, Mapping): + raise UKReleaseCertificationError(f"{part_name}: attestation absent.") + if attestation.get("signing_error") is not None: + raise UKReleaseCertificationError( + f"{part_name}: unsigned report ({attestation['signing_error']}); " + "every certification part must be signed by its producer." + ) + signature = attestation.get("signature") + if not isinstance(signature, str) or not signature: + raise UKReleaseCertificationError(f"{part_name}: signature absent.") + unsigned = json.loads(json.dumps(payload)) + unsigned["attestation"]["signature"] = None + recomputed = hmac.new( + signing_key, canonical_json_bytes(unsigned), hashlib.sha256 + ).hexdigest() + if not hmac.compare_digest(recomputed, signature): + raise UKReleaseCertificationError( + f"{part_name}: signature does not authenticate under the " + "release signing key." + ) + + +def _verify_union( + parts: Mapping[str, tuple[Mapping[str, Any], bytes]], + *, + declared_ids: set[str], + declared_phases: tuple[str, ...], +) -> None: + seen: dict[str, list[str]] = {} + phases_covered: set[str] = set() + for part_name, (payload, _raw) in parts.items(): + for gate_id in payload["gates"]: + seen.setdefault(gate_id, []).append(part_name) + phases_covered.update(str(phase) for phase in payload["phases"]) + union = set(seen) + gap = sorted(declared_ids - union) + if gap: + raise UKReleaseCertificationError( + f"certification gap: declared gate ids evaluated by no part: {gap}." + ) + undeclared = sorted(union - declared_ids) + if undeclared: + raise UKReleaseCertificationError( + f"certification parts evaluate undeclared gate ids: {undeclared}." + ) + overlap = sorted( + gate_id + for gate_id, owners in seen.items() + if len(owners) > 1 and gate_id not in UK_SHARED_GATE_IDS + ) + if overlap: + raise UKReleaseCertificationError( + "certification overlap beyond the declared shared ids: " + f"{overlap}." + ) + for shared in sorted(UK_SHARED_GATE_IDS): + if len(seen.get(shared, [])) < 2: + raise UKReleaseCertificationError( + f"declared shared gate id {shared!r} was evaluated by " + f"{seen.get(shared, [])}; a shared id must be measured on " + "both of its frames." + ) + if phases_covered != set(declared_phases): + raise UKReleaseCertificationError( + f"certification phase coverage {sorted(phases_covered)} does not " + f"equal the declared phase order {list(declared_phases)}." + ) + + +def _verify_identity_join( + *, + spine_report_bytes: bytes, + seam_report_bytes: bytes, + seam_payload: Mapping[str, Any], + release_cut_payload: Mapping[str, Any], + spine_sidecar: Mapping[str, Any], + build_record: Mapping[str, Any], + candidate_path: Path, + candidate_sha256: str, + release_id: str, +) -> None: + sidecar_binding = spine_sidecar.get("spine_gate_report") + if not isinstance(sidecar_binding, Mapping): + raise UKReleaseCertificationError( + "spine sidecar carries no spine_gate_report binding." + ) + spine_report_sha = hashlib.sha256(spine_report_bytes).hexdigest() + if sidecar_binding.get("sha256") != spine_report_sha: + raise UKReleaseCertificationError( + "spine battery report bytes do not match the sidecar's binding; " + "the report does not describe this spine build." + ) + provenance = build_record.get("spine_provenance", {}) + recorded = provenance.get("spine_gate_report", {}) + if recorded.get("sha256") != spine_report_sha: + raise UKReleaseCertificationError( + "the seam's build record binds a different spine battery report " + "than the one supplied; the calibration did not consume this " + "spine build." + ) + artifacts = build_record.get("artifacts", {}) + staged = artifacts.get("staging_h5", {}) + if staged.get("sha256") != candidate_sha256: + raise UKReleaseCertificationError( + "the seam's build record staged a different candidate than the " + "one under certification." + ) + measured_candidate = hashlib.sha256(candidate_path.read_bytes()).hexdigest() + if measured_candidate != candidate_sha256: + raise UKReleaseCertificationError( + f"candidate bytes measure {measured_candidate}, not the pinned " + f"{candidate_sha256}." + ) + seam_report_sha = hashlib.sha256(seam_report_bytes).hexdigest() + recorded_seam = artifacts.get("terminal_gate_json", {}) + if recorded_seam.get("sha256") != seam_report_sha: + raise UKReleaseCertificationError( + "the seam battery report bytes do not match the build record's " + "binding." + ) + diagnostics_sha = artifacts.get("diagnostics_json", {}).get("sha256") + for part_name, payload in ( + ("calibration_seam", seam_payload), + ("release_cut", release_cut_payload), + ): + evidence = payload.get("release_evidence", {}) + if evidence.get("calibration_diagnostics_sha256") != diagnostics_sha: + raise UKReleaseCertificationError( + f"{part_name}: release evidence pins a different diagnostics " + "digest than the build record; the parts were not measured " + "on one calibration." + ) + if release_cut_payload.get("release_id") != release_id: + raise UKReleaseCertificationError( + "the release-cut battery ran under release id " + f"{release_cut_payload.get('release_id')!r}, not the " + f"certification's {release_id!r}." + ) + if release_cut_payload.get("release_candidate") is not True: + raise UKReleaseCertificationError( + "the release-cut battery must run at release-candidate " + "strictness." + ) + if release_cut_payload.get("shippable") is not True: + raise UKReleaseCertificationError( + "the release-cut battery's own report is not shippable." + ) + + +def _verify_score_receipt( + receipt: Mapping[str, Any], *, candidate_sha256: str +) -> None: + artifacts = receipt.get("artifacts") + scored = ( + artifacts.get("candidate", {}).get("sha256") + if isinstance(artifacts, Mapping) + else None + ) + if scored != candidate_sha256: + raise UKReleaseCertificationError( + "the score receipt's artifacts.candidate.sha256 is " + f"{scored!r}, not the candidate under certification " + f"({candidate_sha256!r}); the rule-1 score must be measured on " + "this candidate's bytes." + ) + + +def _status_census(payload: Mapping[str, Any]) -> dict[str, int]: + census: dict[str, int] = {} + for entry in payload["gates"].values(): + status = str(entry.get("status")) + census[status] = census.get(status, 0) + 1 + return census + + +@lru_cache(maxsize=8) +def _scoped_digests( + scope: frozenset[str], + *, + phases: tuple[str, ...], + policy_suffix: str, +) -> dict[str, str]: + manifest = uk_scoped_gate_manifest( + scope, phases=phases, policy_suffix=policy_suffix + ) + return _manifest_digests(manifest) + + +@lru_cache(maxsize=1) +def _full_manifest_digests() -> dict[str, str]: + return _manifest_digests(_uk_gates_spec()) + + +def _manifest_digests(manifest: GatesManifest) -> dict[str, str]: + run = GateBatteryRun( + manifest, + release_id="uk-certification-digest-derivation", + report_path=Path(os.devnull), + release_candidate=False, + registry=UK_GATE_REGISTRY, + ) + return { + "gates_manifest_sha256": run.gates_manifest_sha256, + "policy_sha256": run.policy_sha256, + "spec_fingerprint": run.spec_fingerprint, + } + + +def _sign_certification(payload: dict[str, Any], signing_key: bytes) -> None: + attestation = { + "producer": "microcosm.build.uk_runtime.release_certification", + "signature_algorithm": "hmac-sha256", + "signing_key_sha256": hashlib.sha256(signing_key).hexdigest(), + "signature": None, + } + payload["attestation"] = attestation + attestation["signature"] = hmac.new( + signing_key, canonical_json_bytes(payload), hashlib.sha256 + ).hexdigest() + + +def _write_json(path: Path, payload: Mapping[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(path.name + ".tmp") + temporary.write_text( + json.dumps(payload, indent=2, sort_keys=True, allow_nan=False) + "\n", + encoding="utf-8", + ) + os.replace(temporary, path) diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/release_identity.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/release_identity.py index 5a691fcc..7d7bca2d 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/release_identity.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/release_identity.py @@ -12,6 +12,7 @@ from dataclasses import dataclass __all__ = [ + "UK_NATIONAL_RELEASE_ID", "UK_RELEASE_TIERS", "UK_RELEASE_TIER_CPS_TRANSFER", "UK_RELEASE_TIER_FRS", @@ -31,6 +32,14 @@ ) +#: The certified UK national line's release id (ruling 2026-08-27): one +#: constant name across cuts — run identity and ordering live in the +#: Logbook chain and artifact versioning, not in the id. The "2024-25" +#: vintage segment follows the uk-data survey-year convention; the +#: "-national" segment keeps the id disjoint from the exact-k ladder's +#: "-k" shape and may retire as the migration moves on. +UK_NATIONAL_RELEASE_ID = "microcosm-uk-2024-25-national" + def validate_uk_release_tier(tier: object) -> str: """Return a ratified UK source tier, rejecting every other token.""" diff --git a/packages/microcosm-build/tests/test_country_spec.py b/packages/microcosm-build/tests/test_country_spec.py index 12eb7fa7..e278f482 100644 --- a/packages/microcosm-build/tests/test_country_spec.py +++ b/packages/microcosm-build/tests/test_country_spec.py @@ -727,9 +727,9 @@ def test_thresholds_match_the_schema4_manifest(self, manifest) -> None: assert params["uk_input_mass_parity"]["minimum_reference_total"] == 0.0 assert params["uk_qrf_tail_concentration"]["top_k"] == 100 assert ( - params["uk_qrf_tail_concentration"]["max_top_share"] == 0.9970712395200448 + params["uk_qrf_tail_concentration"]["max_top_share"] == 0.9994670564654868 ) - assert params["uk_qrf_tail_concentration"]["min_nonzero_records"] == 274 + assert params["uk_qrf_tail_concentration"]["min_nonzero_records"] == 104 assert ( params["uk_target_fit"]["max_abs_relative_error"] == terminal_gates.UK_MAX_TARGET_ABS_RELATIVE_ERROR diff --git a/packages/microcosm-build/tests/test_gate_battery_contract_pins.py b/packages/microcosm-build/tests/test_gate_battery_contract_pins.py index 7997cef9..bc1d37e7 100644 --- a/packages/microcosm-build/tests/test_gate_battery_contract_pins.py +++ b/packages/microcosm-build/tests/test_gate_battery_contract_pins.py @@ -311,3 +311,66 @@ def test_a_real_report_survives_every_mirror_check( if any(needle in line for needle in self.MIRROR_DRIFT_NEEDLES) ] assert drifted == [], drifted + + +class TestCertificationMirrors: + """The data shard's certification mirrors track the build shard.""" + + def test_part_scopes_mirror_the_ownership_partition(self) -> None: + from microcosm.build.uk_runtime.calibration_run import ( + UK_CALIBRATION_GATE_SCOPE, + UK_NATIONAL_GATE_SCOPE, + UK_SHARED_GATE_IDS, + UK_SPINE_GATE_SCOPE, + ) + + assert data_contract._UK_CERTIFICATION_PART_SCOPES["spine"] == frozenset( + UK_SPINE_GATE_SCOPE + ) + assert data_contract._UK_CERTIFICATION_PART_SCOPES[ + "calibration_seam" + ] == frozenset(UK_CALIBRATION_GATE_SCOPE) + assert data_contract._UK_CERTIFICATION_PART_SCOPES[ + "release_cut" + ] == frozenset(UK_NATIONAL_GATE_SCOPE) + assert data_contract._UK_CERTIFICATION_SHARED_GATE_IDS == frozenset( + UK_SHARED_GATE_IDS + ) + + def test_part_digests_mirror_the_live_scoped_manifests(self) -> None: + from microcosm.build.uk_runtime.release_certification import ( + _PART_SCOPES, + _scoped_digests, + ) + + for part_name, spec in _PART_SCOPES.items(): + live = _scoped_digests( + frozenset(spec["scope"]), + phases=tuple(spec["phases"]), + policy_suffix=str(spec["policy_suffix"]), + ) + mirrored = data_contract._UK_CERTIFICATION_PART_DIGESTS[part_name] + assert mirrored["gates_manifest_sha256"] == ( + live["gates_manifest_sha256"] + ), part_name + assert mirrored["policy_sha256"] == live["policy_sha256"], part_name + assert list( + data_contract._UK_CERTIFICATION_PART_PHASES[part_name] + ) == list(spec["phases"]) + + def test_certification_identity_mirrors(self) -> None: + from microcosm.build.uk_runtime import release_certification + + assert data_contract._UK_RELEASE_CERTIFICATION_SCHEMA_VERSION == ( + release_certification.UK_RELEASE_CERTIFICATION_SCHEMA_VERSION + ) + assert data_contract._UK_RELEASE_CERTIFICATION_KIND == ( + release_certification.UK_RELEASE_CERTIFICATION_KIND + ) + + def test_national_release_id_mirrors_the_build_shard(self) -> None: + from microcosm.build.uk_runtime.release_identity import ( + UK_NATIONAL_RELEASE_ID, + ) + + assert data_contract._UK_NATIONAL_RELEASE_ID == UK_NATIONAL_RELEASE_ID diff --git a/packages/microcosm-build/tests/test_uk_calibration_run.py b/packages/microcosm-build/tests/test_uk_calibration_run.py index f21b1aa4..42420d26 100644 --- a/packages/microcosm-build/tests/test_uk_calibration_run.py +++ b/packages/microcosm-build/tests/test_uk_calibration_run.py @@ -184,7 +184,7 @@ def test_run_uk_calibration_writes_cross_pinned_outputs(monkeypatch, tmp_path: P pytest.importorskip("tables") # pandas HDF backend monkeypatch.setattr( calibration_run, - "_aggregate_admin_totals", + "uk_aggregate_admin_totals", lambda frame, manifest: (_admin_anchor_values(), []), ) input_h5 = tmp_path / "input.h5" @@ -215,7 +215,6 @@ def test_run_uk_calibration_writes_cross_pinned_outputs(monkeypatch, tmp_path: P measure_resolver=None, source_pins=source_pins, run_config_extra={"calibration_year": 2025}, - release_candidate=False, release_id="test-run", ) @@ -226,6 +225,16 @@ def test_run_uk_calibration_writes_cross_pinned_outputs(monkeypatch, tmp_path: P assert result.build_record["artifacts"]["staging_h5"]["sha256"] == _sha(paths.staging_h5) assert result.build_record["artifacts"]["diagnostics_json"]["sha256"] == _sha(paths.diagnostics_json) assert result.build_record["artifacts"]["terminal_gate_json"]["sha256"] == _sha(paths.terminal_gate_json) + # The record makes no shippability claim of its own — the hand-written + # literal retired with the #757 release-cut audit — and instead points + # at the certification artifact whose verdict is authoritative. + assert "shippable" not in result.build_record + assert "shippable_reason" not in result.build_record + certification = result.build_record["certification"] + assert certification["producer"] == "tools/certify_uk_release_cut.py" + assert certification["expected_artifact"] == str( + paths.staging_h5.with_suffix(".release_certification.json") + ) spine_provenance = result.build_record["spine_provenance"] assert spine_provenance["stages"] == spine_sidecar["stages"] assert spine_provenance["stage_records"] == spine_sidecar["stage_records"] @@ -282,8 +291,7 @@ def test_run_uk_calibration_refuses_input_sha_before_outputs(tmp_path: Path): "input_h5": {"sha256": _sha(input_h5), "size_bytes": input_h5.stat().st_size} }, run_config_extra={"calibration_year": 2025}, - release_candidate=False, - release_id="bad-sha", + release_id="bad-sha", ) assert not paths.staging_h5.exists() assert not paths.diagnostics_json.exists() @@ -319,8 +327,7 @@ def test_run_uk_calibration_refuses_absent_input_sidecar(tmp_path: Path): } }, run_config_extra={"calibration_year": 2025}, - release_candidate=False, - release_id="missing-sidecar", + release_id="missing-sidecar", ) assert not paths.staging_h5.exists() @@ -372,8 +379,7 @@ def test_run_uk_calibration_refuses_unbound_input_sidecar( } }, run_config_extra={"calibration_year": 2025}, - release_candidate=False, - release_id="unbound-sidecar", + release_id="unbound-sidecar", ) assert not paths.staging_h5.exists() @@ -393,7 +399,7 @@ def test_seam_never_modifies_data_variables(monkeypatch, tmp_path: Path): monkeypatch.setattr( calibration_run, - "_aggregate_admin_totals", + "uk_aggregate_admin_totals", lambda frame, manifest: (_admin_anchor_values(), []), ) input_h5 = tmp_path / "input.h5" @@ -438,7 +444,6 @@ def test_seam_never_modifies_data_variables(monkeypatch, tmp_path: Path): "input_h5": {"sha256": _sha(input_h5), "size_bytes": input_h5.stat().st_size} }, run_config_extra={}, - release_candidate=False, release_id="invariant-run", ) @@ -462,7 +467,7 @@ def test_aggregate_admin_measurement_convention_and_refusals(): frame = _frame() manifest = calibration_run._calibration_gate_manifest() - totals, receipt = calibration_run._aggregate_admin_totals(frame, manifest) + totals, receipt = calibration_run.uk_aggregate_admin_totals(frame, manifest) # Small anchors (NEED means) measure as the weighted mean over carriers; # the NHS total measures as the person total under mapped household @@ -480,7 +485,7 @@ def test_aggregate_admin_measurement_convention_and_refusals(): stripped = _frame() stripped.table("household").drop(columns=["electricity_consumption"], inplace=True) with pytest.raises(ValueError, match="household.electricity_consumption"): - calibration_run._aggregate_admin_totals(stripped, manifest) + calibration_run.uk_aggregate_admin_totals(stripped, manifest) def test_nhs_anchor_composes_from_the_columns_the_spine_actually_carries(): @@ -499,7 +504,7 @@ def test_nhs_anchor_composes_from_the_columns_the_spine_actually_carries(): person["nhs_outpatient_spending"] = [5.0, 5.0, 5.0, 5.0] manifest = calibration_run._calibration_gate_manifest() - totals, receipt = calibration_run._aggregate_admin_totals(frame, manifest) + totals, receipt = calibration_run.uk_aggregate_admin_totals(frame, manifest) # Same 4 persons x 50.0 x weight 10.0 as the single-column fixture. assert totals["nhs_spending_total"] == pytest.approx(2000.0) @@ -518,7 +523,7 @@ def test_partly_carried_derived_anchor_refuses_and_names_the_missing_part(): manifest = calibration_run._calibration_gate_manifest() with pytest.raises(ValueError, match="nhs_admitted_patient_spending"): - calibration_run._aggregate_admin_totals(frame, manifest) + calibration_run.uk_aggregate_admin_totals(frame, manifest) def test_seam_pipeline_derives_a_ratified_logbook_scope(): @@ -572,8 +577,7 @@ def test_refusal_records_a_failed_attempt_and_stages_nothing(tmp_path: Path): } }, run_config_extra={"calibration_year": 2025}, - release_candidate=False, - release_id="refused-run", + release_id="refused-run", ) # Every terminal disposition is a row; a refusal that left the chain @@ -597,7 +601,7 @@ def test_attempt_ids_are_unique_across_reruns_of_one_release( pytest.importorskip("tables") # pandas HDF backend monkeypatch.setattr( calibration_run, - "_aggregate_admin_totals", + "uk_aggregate_admin_totals", lambda frame, manifest: (_admin_anchor_values(), []), ) input_h5 = tmp_path / "input.h5" @@ -629,8 +633,7 @@ def test_attempt_ids_are_unique_across_reruns_of_one_release( measure_resolver=None, source_pins=source_pins, run_config_extra={"calibration_year": 2025}, - release_candidate=False, - release_id="one-release-id", + release_id="one-release-id", ) build_ids.append(result.build_record["build_id"]) @@ -644,7 +647,7 @@ def test_verified_ledger_identity_reaches_the_run_evidence(monkeypatch, tmp_path pytest.importorskip("tables") # pandas HDF backend monkeypatch.setattr( calibration_run, - "_aggregate_admin_totals", + "uk_aggregate_admin_totals", lambda frame, manifest: (_admin_anchor_values(), []), ) input_h5 = tmp_path / "input.h5" @@ -683,7 +686,6 @@ def test_verified_ledger_identity_reaches_the_run_evidence(monkeypatch, tmp_path "input_h5": {"sha256": _sha(input_h5), "size_bytes": input_h5.stat().st_size} }, run_config_extra={"calibration_year": 2025}, - release_candidate=False, release_id="ledger-identity", ) diff --git a/packages/microcosm-build/tests/test_uk_calibration_seam_driver.py b/packages/microcosm-build/tests/test_uk_calibration_seam_driver.py index 78deb778..8ed5850e 100644 --- a/packages/microcosm-build/tests/test_uk_calibration_seam_driver.py +++ b/packages/microcosm-build/tests/test_uk_calibration_seam_driver.py @@ -70,34 +70,42 @@ def _args(tmp_path: Path) -> list[str]: ] -def test_driver_refuses_release_candidate_with_each_override(tmp_path: Path): +def test_driver_refuses_release_candidate_outright(tmp_path: Path): + # The seam's scoped battery covers 6 of the declared entries and must + # never sign a shippability claim (the #757 release-cut audit); the + # release verdict belongs to the release-cut certification producer. driver = _load_driver_module() - override_flags = [ - ["--epochs", "128"], - ["--target-weight-rule", "family_equal"], - ["--learning-rate", "0.01"], - ["--target-loss-cap", "5"], - ] - for flag in override_flags: + with pytest.raises(SystemExit): + driver._parse_args(_args(tmp_path) + ["--release-candidate"]) + # The refusal is unconditional — an otherwise doctrine-clean invocation + # is refused too, not just ones with override flags. + with pytest.raises(SystemExit): + driver._parse_args( + _args(tmp_path) + ["--release-candidate", "--epochs", "128"] + ) + + +def test_driver_refuses_canonical_release_ids(tmp_path: Path): + # Canonical release ids name shippable candidates; the seam runs under + # staging or dev ids only, and redirects canonical ids to the + # release-cut producer. + driver = _load_driver_module() + base = _args(tmp_path) + release_index = base.index("--release-id") + for canonical in ( + "populace-uk-2024-frs-k100", + "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + ): + args = [*base] + args[release_index + 1] = canonical with pytest.raises(SystemExit): - driver._parse_args(_args(tmp_path) + ["--release-candidate", *flag]) + driver._parse_args(args) -def test_driver_refuses_release_candidate_with_operator_exclusions(tmp_path: Path): - # The scoped battery carries no target-surface gate, so an operator - # register could narrow what a release candidate was measured against - # without anything noticing. +def test_driver_accepts_operator_exclusions_on_staging_posture(tmp_path: Path): driver = _load_driver_module() exclusions = tmp_path / "operator.json" exclusions.write_text("{}", encoding="utf-8") - - with pytest.raises(SystemExit): - driver._parse_args( - _args(tmp_path) - + ["--release-candidate", "--measure-exclusions", str(exclusions)] - ) - - # Without the release-candidate posture the same register is accepted. parsed = driver._parse_args( _args(tmp_path) + ["--measure-exclusions", str(exclusions)] ) @@ -184,3 +192,14 @@ def fake_run(**kwargs): assert call["measure_resolver"].kwargs["simulation_source"] == call["paths"].input_h5 assert call["source_pins"]["ledger_facts"] == {"sha256": "b" * 64, "size_bytes": 2} assert "uk_target_fit" in capsys.readouterr().out + + +def test_driver_refuses_the_national_release_id(tmp_path: Path): + # The constant national id names a shippable release (ruling 2026-08-27); + # the seam runs under staging or dev ids only. + driver = _load_driver_module() + base = _args(tmp_path) + args = [*base] + args[base.index("--release-id") + 1] = "microcosm-uk-2024-25-national" + with pytest.raises(SystemExit): + driver._parse_args(args) diff --git a/packages/microcosm-build/tests/test_uk_frs_spine.py b/packages/microcosm-build/tests/test_uk_frs_spine.py index 8919b6b4..4d684ab4 100644 --- a/packages/microcosm-build/tests/test_uk_frs_spine.py +++ b/packages/microcosm-build/tests/test_uk_frs_spine.py @@ -1741,6 +1741,44 @@ def checkpoint_metadata(self) -> dict[str, object]: assert "frs_spine" not in evidence +def test_collect_fit_weight_records_is_duck_typed_and_fail_visible(): + tool = _load_tool() + + class _Record: + def __init__(self, fit_name, weight_kind): + self.fit_name = fit_name + self.weight_kind = weight_kind + + class _Broken: + @property + def fit_weight_records(self): + raise RuntimeError("records unreadable") + + implementations = { + "frs_spine": SimpleNamespace(), + "was_wealth": SimpleNamespace( + fit_weight_records=(_Record("uk_was_2018_20_wealth:savings", "design"),) + ), + "etb_vat": SimpleNamespace(fit_weight_records=()), + "lcfs_consumption": _Broken(), + } + records = tool._collect_fit_weight_records( + stage_names=("frs_spine", "was_wealth", "etb_vat", "lcfs_consumption"), + implementations=implementations, + ) + # Stages without the hook contribute nothing; a fitting stage with no or + # unreadable records persists an empty list, so the release-cut weights + # audit fails visibly instead of the gap vanishing from the sidecar. + assert records == { + "was_wealth": [ + {"fit_name": "uk_was_2018_20_wealth:savings", "weight_kind": "design"} + ], + "etb_vat": [], + "lcfs_consumption": [], + } + assert "frs_spine" not in records + + class TestScottishWaterAndSewerage: """The FRS 2024-25 cell retirement, at the three shapes the tab presents. diff --git a/packages/microcosm-build/tests/test_uk_measure_simulation.py b/packages/microcosm-build/tests/test_uk_measure_simulation.py index 9054d78d..4e380007 100644 --- a/packages/microcosm-build/tests/test_uk_measure_simulation.py +++ b/packages/microcosm-build/tests/test_uk_measure_simulation.py @@ -243,15 +243,63 @@ def test_exclusion_applier_returns_pruned_registry_and_receipt(): ) +#: The adjudicated register census: one entry per class of the #757 +#: ``uk_target_fit`` dispositions (issue comment 5427936411) plus the +#: standing salary-sacrifice adjudication. The counts are the record of +#: what was signed; a drifting count is a register change that must be +#: re-adjudicated, never absorbed. +_PACKAGED_EXCLUSION_CENSUS = { + "hmrc.salary_sacrifice.": 5, + "_1_000_000_to_inf": 11, + "slc.": 3, + "dwp/uc_payment_dist/": 16, + "obr.universal_credit_": 2, + "ons.household_composition.": 3, + "obr.fuel_duties": 1, +} + + def test_packaged_exclusions_load(): exclusions = load_uk_calibration_measure_exclusions() - assert {entry["name"] for entry in exclusions} == { - "hmrc.salary_sacrifice.it_relief_basic_rate", - "hmrc.salary_sacrifice.it_relief_higher_rate", - "hmrc.salary_sacrifice.it_relief_additional_rate", - "hmrc.salary_sacrifice.nics_relief_employee", - "hmrc.salary_sacrifice.nics_relief_employer", - } + names = [entry["name"] for entry in exclusions] + assert len(names) == len(set(names)) == 47 + + for marker, expected in _PACKAGED_EXCLUSION_CENSUS.items(): + matched = [name for name in names if marker in name] + assert len(matched) == expected, (marker, matched) + # The six sparse HMRC band cells are whatever remains: hmrc/ band + # cells that are not the eleven 1m+ channel cells. + sparse = [ + name + for name in names + if name.startswith("hmrc/") and "_1_000_000_to_inf" not in name + ] + assert len(sparse) == 6, sparse + + # The 2026-08-26 tranche carries the uk_target_fit disposition + # adjudication and a uniform three-month window; the ONS composition + # cells track the relationship-to-head successor issue. + tranche = [e for e in exclusions if e["approved_on"] == "2026-08-26"] + assert len(tranche) == 42 + for entry in tranche: + assert "5427936411" in entry["adjudication"], entry["name"] + assert entry["expires_on"] == "2026-11-26", entry["name"] + for entry in exclusions: + if entry["name"].startswith("ons.household_composition."): + assert entry["tracking"] == "microcosm#791", entry["name"] + + # The lever targets are deliberately NOT excluded: the six UC + # caseload / two-child-limit cells ride the would_claim_uc lever run, + # and the two expected-to-resolve cells ride the exclusion re-run. + excluded = set(names) + for riding in ( + "dwp.uc.households", + "dwp.uc.households_single_no_children", + "dwp.uc.two_child_limit.children_disabled_child_element", + "ons.household_composition.couple_no_children_households", + "hmrc/state_pension_income_band_40_000_to_50_000", + ): + assert riding not in excluded, riding def test_measure_resolver_direct_and_scratch_receipts(monkeypatch, tmp_path: Path): diff --git a/packages/microcosm-build/tests/test_uk_release_certification.py b/packages/microcosm-build/tests/test_uk_release_certification.py new file mode 100644 index 00000000..20c5d7e5 --- /dev/null +++ b/packages/microcosm-build/tests/test_uk_release_certification.py @@ -0,0 +1,473 @@ +"""The release-cut certification producer and its composer refusals.""" + +from __future__ import annotations + +import base64 +import hashlib +import json +from datetime import date +from pathlib import Path + +import pytest + +from microcosm.build.country_spec import load_country_spec +from microcosm.build.gate_battery import ( + BlockingMode, + EvidenceContext, + FunctionBinding, + GateBatteryRun, + gate_signing_key_env, +) +from microcosm.build.gates import GateResult +from microcosm.build.uk_runtime import release_certification +from microcosm.build.uk_runtime.calibration_run import ( + UK_CALIBRATION_GATE_SCOPE, + UK_NATIONAL_GATE_SCOPE, + UK_SHARED_GATE_IDS, + UK_SPINE_GATE_SCOPE, + resign_uk_gate_report, + uk_scoped_gate_manifest, +) +from microcosm.build.uk_runtime.release_certification import ( + UKReleaseCertificationError, + compose_uk_release_certification, + rehydrate_uk_fit_weight_records, + run_uk_release_cut_battery, + uk_release_cut_scope_exclusions, +) + +_TEST_KEY = base64.b64encode(bytes(range(32))).decode("ascii") + + +@pytest.fixture(autouse=True) +def _signing_key(monkeypatch): + monkeypatch.setenv(gate_signing_key_env("uk"), _TEST_KEY) + + +def _stub_registry(): + """A registry that passes every declared gate, over the real spec. + + Digests derive from the committed manifest alone, so parts built with + this registry carry the same ``gates_manifest_sha256`` / ``policy_sha256`` + the production registry would produce — which is exactly what the + composer verifies. + """ + + spec = load_country_spec("uk").gates + parameter_keys: dict[str, set[str]] = {} + for entry in spec.gates: + parameter_keys.setdefault(entry.gate, set()).update(entry.parameters) + + def _passing(name): + def _gate(**_kwargs): + return GateResult(name=name, passed=True) + + return _gate + + return { + gate: FunctionBinding( + name=gate, + gate=_passing(gate), + parameter_keys=frozenset(keys), + ) + for gate, keys in parameter_keys.items() + } + + +def _write_part(path: Path, scope, phases, *, release_id, release_candidate, + release_evidence=None, augment=None, block_phase=None): + registry = _stub_registry() + manifest = uk_scoped_gate_manifest( + frozenset(scope), + phases=tuple(phases), + policy_suffix={ + frozenset(UK_SPINE_GATE_SCOPE): "spine_build_scope", + frozenset(UK_CALIBRATION_GATE_SCOPE): "calibration_seam_scope", + frozenset(UK_NATIONAL_GATE_SCOPE): "release_cut_scope", + }[frozenset(scope)], + ) + battery = GateBatteryRun( + manifest, + release_id=release_id, + report_path=path, + release_candidate=release_candidate, + registry=registry, + release_evidence=release_evidence or {}, + ) + for phase in phases: + battery.run_phase(phase, EvidenceContext(artifacts={})) + battery.enforce(phase, mode=BlockingMode.MARKS_ARTIFACT) + payload = battery.report_payload() + if augment: + payload.update(augment) + resign_uk_gate_report(payload) + path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") + return payload + + +def _sha(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +@pytest.fixture +def green_certification_inputs(tmp_path: Path): + """Three green signed parts plus a closed identity join.""" + + candidate = tmp_path / "microcosm_uk_2024.h5" + candidate.write_bytes(b"candidate-bytes") + candidate_sha = _sha(candidate) + diagnostics = tmp_path / "calibration_diagnostics.json" + diagnostics.write_text('{"targets": []}', encoding="utf-8") + diagnostics_sha = _sha(diagnostics) + + spine_report = tmp_path / "spine.spine_gates.json" + _write_part( + spine_report, + UK_SPINE_GATE_SCOPE, + ("assembled", "transferred"), + release_id="uk-frs-spine-test", + release_candidate=True, + ) + seam_report = tmp_path / "terminal_gates.json" + _write_part( + seam_report, + UK_CALIBRATION_GATE_SCOPE, + ("terminal",), + release_id="dev-seam-test", + release_candidate=False, + release_evidence={"calibration_diagnostics_sha256": diagnostics_sha}, + augment={ + "posture": "calibration_seam", + "scope_exclusions": {}, + "aggregate_admin_measurement": {}, + }, + ) + release_cut_report = tmp_path / "release_cut_gates.json" + _write_part( + release_cut_report, + UK_NATIONAL_GATE_SCOPE, + ("preflight", "terminal"), + release_id="uk-757-first-certified-cut", + release_candidate=True, + release_evidence={"calibration_diagnostics_sha256": diagnostics_sha}, + augment={ + "posture": "release_cut", + "scope_exclusions": uk_release_cut_scope_exclusions(), + "aggregate_admin_measurement": {}, + }, + ) + seam_report_sha = _sha(seam_report) + sidecar = { + "stages": ["frs_spine"], + "spine_gate_report": { + "path": str(spine_report), + "sha256": _sha(spine_report), + }, + } + build_record = { + "run_config": { + "doctrine": {"epochs": 1500}, + "doctrine_overrides": {"epochs": {"default": 256, "effective": 1500}}, + }, + "spine_provenance": { + "spine_gate_report": {"sha256": _sha(spine_report)}, + }, + "artifacts": { + "staging_h5": {"sha256": candidate_sha}, + "diagnostics_json": {"sha256": diagnostics_sha}, + "terminal_gate_json": {"sha256": seam_report_sha}, + }, + } + score_receipt = tmp_path / "score_vs_enhanced_frs.json" + score_receipt.write_text( + json.dumps( + { + "artifacts": { + "candidate": {"sha256": candidate_sha, "size_bytes": 15}, + "incumbent": {"sha256": "9" * 64, "size_bytes": 1}, + }, + "candidate_target_wins": 293, + } + ), + encoding="utf-8", + ) + return { + "release_id": "uk-757-first-certified-cut", + "candidate_name": "microcosm_uk_2024", + "candidate_path": candidate, + "candidate_sha256": candidate_sha, + "spine_report_path": spine_report, + "seam_report_path": seam_report, + "release_cut_report_path": release_cut_report, + "spine_sidecar": sidecar, + "build_record": build_record, + "score_receipt_path": score_receipt, + "exclusions_evaluated_on": date(2026, 8, 27), + "certification_path": tmp_path / "release_certification.json", + } + + +def test_scope_exclusions_cover_every_non_national_gate(): + exclusions = uk_release_cut_scope_exclusions() + declared = {entry.id for entry in load_country_spec("uk").gates.gates} + assert set(exclusions) | set(UK_NATIONAL_GATE_SCOPE) == declared + assert not set(exclusions) & set(UK_NATIONAL_GATE_SCOPE) + assert all(exclusions.values()) + + +def test_rehydrate_fit_weight_records(): + assert rehydrate_uk_fit_weight_records({}) is None + records = rehydrate_uk_fit_weight_records( + { + "fit_weight_records": { + "was_wealth": [ + {"fit_name": "uk_was_2018_20_wealth:savings", "weight_kind": "design"} + ], + } + } + ) + assert [(r.fit_name, r.weight_kind) for r in records] == [ + ("uk_was_2018_20_wealth:savings", "design") + ] + # A fitting stage that recorded nothing coerces the whole artifact to + # (), which the weights-audit binding fails — never a vacuous pass. + assert ( + rehydrate_uk_fit_weight_records( + {"fit_weight_records": {"was_wealth": []}} + ) + == () + ) + # A malformed block is corruption, not an empty audit: it raises rather + # than degrading to (), keeping "no weights" and "weights we could not + # read" distinguishable. + with pytest.raises(UKReleaseCertificationError, match="unreadable block"): + rehydrate_uk_fit_weight_records( + {"fit_weight_records": {"was_wealth": "not-a-list"}} + ) + with pytest.raises(UKReleaseCertificationError, match="malformed record"): + rehydrate_uk_fit_weight_records( + {"fit_weight_records": {"was_wealth": [{"fit_name": "x"}]}} + ) + with pytest.raises(UKReleaseCertificationError, match="malformed record"): + rehydrate_uk_fit_weight_records( + {"fit_weight_records": {"was_wealth": ["not-a-mapping"]}} + ) + + +def test_parity_evidence_refuses_bare_name_grain(): + # Both parity sides share the name@period grain; a diagnostics row + # missing the period label refuses loudly instead of silently falling + # out of the comparison. + from microcosm.build.uk_runtime.release_certification import ( + uk_release_parity_evidence, + ) + + class _Frame: + entities = () + + def table(self, entity): # pragma: no cover - never reached + raise AssertionError + + class _Registry: + specs = () + + class _Reference: + input_entities = {} + + with pytest.raises(UKReleaseCertificationError, match="name@period"): + uk_release_parity_evidence( + _Frame(), + diagnostics_targets=[{"name": "dwp.uc.households", "relative_error": 0.1}], + reference_registry=_Registry(), + parity_reference=_Reference(), + ) + evidence = uk_release_parity_evidence( + _Frame(), + diagnostics_targets=[ + {"name": "dwp.uc.households@2025", "relative_error": 0.1} + ], + reference_registry=_Registry(), + parity_reference=_Reference(), + ) + assert evidence.candidate_targets == {"dwp.uc.households@2025"} + + +def test_compose_green_certification(green_certification_inputs): + certification = compose_uk_release_certification(**green_certification_inputs) + assert certification["shippable"] is True + assert certification["kind"] == "uk_release_certification" + assert set(certification["parts"]) == {"spine", "calibration_seam", "release_cut"} + declared = {entry.id for entry in load_country_spec("uk").gates.gates} + union = set() + for part in certification["parts"].values(): + union.update(part["entry_ids"]) + assert union == declared + assert certification["spec"]["shared_gate_ids"] == sorted(UK_SHARED_GATE_IDS) + assert certification["doctrine"]["overrides"] == { + "epochs": {"default": 256, "effective": 1500} + } + written = json.loads( + green_certification_inputs["certification_path"].read_text(encoding="utf-8") + ) + assert written["attestation"]["signature"] + # The certification's own signature verifies under the release key. + import hmac as hmac_module + + from microcosm.build.logbook import canonical_json_bytes + + unsigned = json.loads(json.dumps(written)) + unsigned["attestation"]["signature"] = None + recomputed = hmac_module.new( + base64.b64decode(_TEST_KEY), canonical_json_bytes(unsigned), hashlib.sha256 + ).hexdigest() + assert recomputed == written["attestation"]["signature"] + + +def test_compose_refuses_tampered_part_signature(green_certification_inputs): + seam_path = green_certification_inputs["seam_report_path"] + payload = json.loads(seam_path.read_text(encoding="utf-8")) + payload["release_id"] = "dev-seam-tampered" + seam_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") + # Keep the build record's byte pin in step so the signature check is + # the refusal that fires, not the identity join. + green_certification_inputs["build_record"]["artifacts"]["terminal_gate_json"][ + "sha256" + ] = _sha(seam_path) + with pytest.raises(UKReleaseCertificationError, match="signature"): + compose_uk_release_certification(**green_certification_inputs) + + +def test_compose_refuses_entry_id_gap(green_certification_inputs): + cut_path = green_certification_inputs["release_cut_report_path"] + payload = json.loads(cut_path.read_text(encoding="utf-8")) + payload["gates"].pop("uk_qrf_tail_concentration") + cut_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") + with pytest.raises(UKReleaseCertificationError, match="entry ids"): + compose_uk_release_certification(**green_certification_inputs) + + +def test_compose_refuses_blocked_part(green_certification_inputs): + spine_path = green_certification_inputs["spine_report_path"] + payload = json.loads(spine_path.read_text(encoding="utf-8")) + payload["blocked_at_phase"] = "transferred" + spine_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") + green_certification_inputs["spine_sidecar"]["spine_gate_report"]["sha256"] = _sha( + spine_path + ) + green_certification_inputs["build_record"]["spine_provenance"][ + "spine_gate_report" + ]["sha256"] = _sha(spine_path) + with pytest.raises(UKReleaseCertificationError, match="blocked"): + compose_uk_release_certification(**green_certification_inputs) + + +def test_compose_refuses_failing_release_blocking_entry(green_certification_inputs): + cut_path = green_certification_inputs["release_cut_report_path"] + payload = json.loads(cut_path.read_text(encoding="utf-8")) + payload["gates"]["uk_support"]["status"] = "failed" + payload["gates"]["uk_support"]["failures"] = ["synthetic failure"] + cut_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") + with pytest.raises(UKReleaseCertificationError, match="release-blocking"): + compose_uk_release_certification(**green_certification_inputs) + + +def test_compose_refuses_sidecar_report_mismatch(green_certification_inputs): + green_certification_inputs["spine_sidecar"]["spine_gate_report"]["sha256"] = ( + "0" * 64 + ) + with pytest.raises(UKReleaseCertificationError, match="sidecar"): + compose_uk_release_certification(**green_certification_inputs) + + +def test_compose_refuses_candidate_mismatch(green_certification_inputs): + green_certification_inputs["build_record"]["artifacts"]["staging_h5"][ + "sha256" + ] = "0" * 64 + with pytest.raises(UKReleaseCertificationError, match="staged a different"): + compose_uk_release_certification(**green_certification_inputs) + + +def test_compose_refuses_diagnostics_divergence(green_certification_inputs): + green_certification_inputs["build_record"]["artifacts"]["diagnostics_json"][ + "sha256" + ] = "0" * 64 + with pytest.raises(UKReleaseCertificationError, match="diagnostics"): + compose_uk_release_certification(**green_certification_inputs) + + +def test_compose_refuses_foreign_release_id(green_certification_inputs): + green_certification_inputs["release_id"] = "uk-some-other-cut" + with pytest.raises(UKReleaseCertificationError, match="release id"): + compose_uk_release_certification(**green_certification_inputs) + + +def test_compose_refuses_unpinned_score_receipt(green_certification_inputs): + green_certification_inputs["score_receipt_path"].write_text( + '{"verdict": "scored"}', encoding="utf-8" + ) + with pytest.raises(UKReleaseCertificationError, match="score receipt"): + compose_uk_release_certification(**green_certification_inputs) + + +def test_compose_refuses_score_receipt_scored_on_another_artifact( + green_certification_inputs, +): + # The candidate digest appearing elsewhere in the document (an inputs + # list, a provenance pin) must not satisfy the cross-pin: only + # artifacts.candidate.sha256 names what was scored. + candidate_sha = green_certification_inputs["candidate_sha256"] + green_certification_inputs["score_receipt_path"].write_text( + json.dumps( + { + "artifacts": { + "candidate": {"sha256": "8" * 64}, + "incumbent": {"sha256": candidate_sha}, + }, + "provenance": {"inputs": [candidate_sha]}, + } + ), + encoding="utf-8", + ) + with pytest.raises( + UKReleaseCertificationError, match="artifacts.candidate.sha256" + ): + compose_uk_release_certification(**green_certification_inputs) + + +def test_compose_refuses_absent_signing_key(green_certification_inputs, monkeypatch): + monkeypatch.delenv(gate_signing_key_env("uk")) + with pytest.raises(UKReleaseCertificationError, match="must be set"): + compose_uk_release_certification(**green_certification_inputs) + + +def test_release_cut_battery_runs_and_signs(tmp_path: Path, monkeypatch): + monkeypatch.setattr( + release_certification, + "uk_aggregate_admin_totals", + lambda frame, manifest: ({}, {"stub": True}), + ) + report_path = tmp_path / "release_cut_gates.json" + payload = run_uk_release_cut_battery( + object(), + report_path=report_path, + release_id="uk-757-first-certified-cut", + diagnostics_sha256="a" * 64, + coverage_engine=object(), + build_stage_names=("frs_spine",), + ledger_registries={2023: object(), 2025: object()}, + parity_evidence=object(), + fit_weight_records=None, + input_mass_reference={}, + exclusions_evaluated_on=date(2026, 8, 27), + gate_registry=_stub_registry(), + ) + assert payload["posture"] == "release_cut" + assert payload["release_candidate"] is True + assert payload["shippable"] is True + assert set(payload["gates"]) == set(UK_NATIONAL_GATE_SCOPE) + assert payload["blocked_at_phase"] is None + assert set(payload["scope_exclusions"]) == ( + set(UK_SPINE_GATE_SCOPE) | set(UK_CALIBRATION_GATE_SCOPE) + ) - set(UK_NATIONAL_GATE_SCOPE) + on_disk = json.loads(report_path.read_text(encoding="utf-8")) + assert on_disk["attestation"]["signature"] == payload["attestation"]["signature"] diff --git a/packages/microcosm-data/src/microcosm/data/contract.py b/packages/microcosm-data/src/microcosm/data/contract.py index d6b295e8..c423461c 100644 --- a/packages/microcosm-data/src/microcosm/data/contract.py +++ b/packages/microcosm-data/src/microcosm/data/contract.py @@ -202,15 +202,17 @@ _UK_MIN_ESS_FRACTION = 0.01 _UK_MAX_TO_MEDIAN_WEIGHT_RATIO = 1_151.2542195939373 _UK_MAX_TARGET_ABS_RELATIVE_ERROR = 0.25 -# Spec-armed weighted-integrity thresholds (uk/gates.json parameters, -# microcosm#630): passing reports must carry exactly the committed values, +# Spec-armed weighted-integrity thresholds (uk/gates.json parameters; +# input-mass pair from microcosm#630, QRF tail pair re-armed from the #686 +# L3 baselines by microcosm#757 B4): passing reports must carry exactly the +# committed values, # so a re-signed report cannot loosen a fence the spec armed. Held in # lockstep with the committed spec by the build-shard sync tests. _UK_INPUT_MASS_RELATIVE_TOLERANCE = 4.521811483823806 _UK_INPUT_MASS_MINIMUM_REFERENCE_TOTAL = 0.0 _UK_QRF_TAIL_TOP_K = 100 -_UK_QRF_TAIL_MAX_TOP_SHARE = 0.9970712395200448 -_UK_QRF_TAIL_MIN_NONZERO_RECORDS = 274 +_UK_QRF_TAIL_MAX_TOP_SHARE = 0.9994670564654868 +_UK_QRF_TAIL_MIN_NONZERO_RECORDS = 104 # Independent publication pin for the active reviewed reference source. The data # shard cannot import the build shard, so keep this in lockstep with # uk/gates.json reference_registry["efrs-post-calibration"].identity. @@ -373,13 +375,13 @@ # fingerprint derives from the manifest digest. Editing the spec moves all # three here in the same reviewed change. _UK_GATE_BATTERY_POLICY_SHA256 = ( - "0b215cad96263fc8ee937facd189212b0f60639bb317ecdf6d19d7c7004689d9" + "5459347c9077b2acd5970a62d818e3ddd063d86d6c3dbce4d32dcacec3bdc414" ) _UK_GATE_BATTERY_GATES_MANIFEST_SHA256 = ( - "fe580e1f39924c40f22c9826c21df8a0d02273cf0660dccf13039d173fadee85" + "f68e10d8ff6654b7fc707da0508ea063b0f3c96b8a813b7098298727d43b9d6d" ) _UK_GATE_BATTERY_SPEC_FINGERPRINT = ( - "c6b43744bdc2ac3187f503d719aea12d764a521d24382f0e0390bf7b92a2bd5f" + "2a4a8b18d024f80782b375539c87d57006592d64470553a4da5a378791254faa" ) #: Spec entry id -> the legacy gate name whose observable detail checks #: apply unchanged (the battery re-keys the report by entry id; the gate @@ -510,12 +512,139 @@ ) +# --- UK release certification (microcosm#757 item B5) ---------------------- +# The multi-part certification the release-cut producer composes: the spine +# build's battery report, the calibration seam's battery report, and the +# release-cut battery report union to the full declared gate-entry set with +# no gap and no overlap beyond the declared shared id. The data shard cannot +# import the build shard, so the part scopes, phases, and scoped-manifest +# digests are hand-mirrored here and held in lockstep by the build-shard +# sync tests (test_gate_battery_contract_pins). The phase and digest checks +# that _check_uk_gate_battery_report applies to one unfiltered report apply +# here per-certification (the 5413502559 audit's nine refusal points). +_UK_RELEASE_CERTIFICATION_FILE = "release_certification.json" +_UK_RELEASE_CUT_GATE_REPORT_FILE = "release_cut_gates.json" +# The certified national line's constant release id (ruling 2026-08-27): +# ordering and run identity live in the Logbook and versioning, so the id +# stays fixed across cuts. Mirrored from +# microcosm.build.uk_runtime.release_identity.UK_NATIONAL_RELEASE_ID (the +# data shard cannot import the build shard); lockstep-tested. +_UK_NATIONAL_RELEASE_ID = "microcosm-uk-2024-25-national" +_UK_RELEASE_CERTIFICATION_SCHEMA_VERSION = 1 +_UK_RELEASE_CERTIFICATION_KIND = "uk_release_certification" +_UK_CERTIFICATION_SHARED_GATE_IDS = frozenset({"uk_aggregate_admin"}) +_UK_CERTIFICATION_PART_PHASES: Mapping[str, tuple[str, ...]] = { + "spine": ("assembled", "transferred"), + "calibration_seam": ("terminal",), + "release_cut": ("preflight", "terminal"), +} +_UK_CERTIFICATION_PART_SCOPES: Mapping[str, frozenset[str]] = { + "spine": frozenset( + { + "uk_brma_enum_domain", + "uk_stage_age_tail_targets", + "uk_stage_cgt_band_donors_support", + "uk_stage_cgt_incidence_clone_mass", + "uk_stage_etb_services_support", + "uk_stage_etb_vat_support", + "uk_stage_frs_hmrc_spine_leaves_signal", + "uk_stage_hmrc_cgt_gains_spine_summary", + "uk_stage_hmrc_spi_income_spine_identity", + "uk_stage_lcfs_consumption_support", + "uk_stage_salary_sacrifice_realization", + "uk_stage_spi_support_channel_mass", + "uk_stage_student_loans_realization", + "uk_stage_was_wealth_support", + } + ), + "calibration_seam": frozenset( + { + "uk_aggregate_admin", + "uk_calibration_reference_coverage", + "uk_target_fit", + "uk_weight_ess", + "uk_weight_ratio", + "uk_zero_weight_strata", + } + ), + "release_cut": frozenset( + { + "uk_aggregate_admin", + "uk_degenerate_release_surface", + "uk_export_surface", + "uk_input_mass_parity", + "uk_ledger_compile_parity_incumbent_2025", + "uk_ledger_compile_parity_production_2023", + "uk_nonnegative_columns", + "uk_qrf_tail_concentration", + "uk_release_family_build_stages", + "uk_release_input_coverage", + "uk_release_input_coverage_manifest_current", + "uk_student_loan_plan_enum_domain", + "uk_support", + "uk_take_up_signal", + "uk_target_surface", + "uk_weights_audit", + } + ), +} +_UK_CERTIFICATION_PART_DIGESTS: Mapping[str, Mapping[str, str]] = { + "spine": { + "gates_manifest_sha256": ( + "1605cf3fe1be4983cfb4ed806a34d69375cdc3e4e0c8883cc49481ac5870399a" + ), + "policy_sha256": ( + "3d14ad24eff7f5afd343164560db24095d27fafb36c619ddf725c32e00b35a69" + ), + }, + "calibration_seam": { + "gates_manifest_sha256": ( + "7bc1fab5aa0c035b664684f93195c7f18cb6f48a5ff0e29fbd557bda979ba83b" + ), + "policy_sha256": ( + "59a5e70053626439a848fd77c58064bf64c62914829667e62ef66a7408dd40f3" + ), + }, + "release_cut": { + "gates_manifest_sha256": ( + "18f07f40eead198a4436de7a43d4c0b13b9f2a9335bc84d4ab6b6a2ed75e597e" + ), + "policy_sha256": ( + "b2a6446da0ffc53c0a894618795d918163368a266ef01a2a3609587085162115" + ), + }, +} +_UK_CERTIFICATION_REQUIRED_FIELDS = frozenset( + { + "schema_version", + "kind", + "country", + "release_id", + "candidate", + "parts", + "spec", + "doctrine", + "diagnostics_sha256", + "score_receipt", + "exclusions_evaluated_on", + "shippable", + "attestation", + } +) + + def required_release_files(release_id: str) -> tuple[str, ...]: """Files required for a release id's country-specific contract.""" if release_id.startswith("populace-us-"): return (*REQUIRED_RELEASE_FILES, US_SOURCE_COVERAGE_DIAGNOSTICS_FILE) if _is_uk_exact_k_release_id(release_id): return (*REQUIRED_RELEASE_FILES, _UK_TERMINAL_GATE_REPORT_FILE) + if release_id == _UK_NATIONAL_RELEASE_ID: + # The certified national line ships its composed verdict: the + # shippability claim lives only in the certification, so a national + # release without one is refused at the required-files layer, not + # just when part artifacts happen to be present. + return (*REQUIRED_RELEASE_FILES, _UK_RELEASE_CERTIFICATION_FILE) return REQUIRED_RELEASE_FILES @@ -2646,6 +2775,209 @@ def _check_uk_gate_battery_report( ) +def _check_uk_release_certification( + certification: Mapping, + *, + release_id: str, + calibration_diagnostics_sha256: str | None, + failures: list[str], +) -> None: + """Validate the multi-part release certification (microcosm#757 B5). + + The certification is the only artifact that may carry a UK shippability + verdict: its parts must union to the full declared gate-entry set with + no gap and no overlap beyond the declared shared id, each part pinned to + the committed spec's scoped digests, with every release-blocking entry + passed and the whole document signed by the release key. Shippability is + recomputed from the parts, never read off the flag. + """ + + file = _UK_RELEASE_CERTIFICATION_FILE + actual_fields = set(certification) + if actual_fields != _UK_CERTIFICATION_REQUIRED_FIELDS: + missing = sorted(_UK_CERTIFICATION_REQUIRED_FIELDS - actual_fields) + unexpected = sorted(actual_fields - _UK_CERTIFICATION_REQUIRED_FIELDS) + failures.append( + f"{file} must carry exactly the certification fields; " + f"missing {missing}, unexpected {unexpected}." + ) + return + schema = certification.get("schema_version") + if type(schema) is not int or schema != _UK_RELEASE_CERTIFICATION_SCHEMA_VERSION: + failures.append( + f"{file} schema_version must be the integer " + f"{_UK_RELEASE_CERTIFICATION_SCHEMA_VERSION}, got {schema!r}." + ) + return + if certification.get("kind") != _UK_RELEASE_CERTIFICATION_KIND: + failures.append( + f"{file} kind must be {_UK_RELEASE_CERTIFICATION_KIND!r}, got " + f"{certification.get('kind')!r}." + ) + if certification.get("country") != "uk": + failures.append(f"{file} country must be 'uk'.") + if certification.get("release_id") != release_id: + failures.append( + f"{file} release_id {certification.get('release_id')!r} does not " + f"match the release under validation ({release_id!r})." + ) + + parts = certification.get("parts") + if not isinstance(parts, Mapping) or set(parts) != set( + _UK_CERTIFICATION_PART_SCOPES + ): + failures.append( + f"{file} parts must be exactly " + f"{sorted(_UK_CERTIFICATION_PART_SCOPES)}, got " + f"{sorted(parts) if isinstance(parts, Mapping) else parts!r}." + ) + return + all_passed = True + for part_name, expected_scope in _UK_CERTIFICATION_PART_SCOPES.items(): + part = parts[part_name] + if not isinstance(part, Mapping): + failures.append(f"{file} parts.{part_name} must be an object.") + all_passed = False + continue + if sorted(part.get("entry_ids", ())) != sorted(expected_scope): + failures.append( + f"{file} parts.{part_name}.entry_ids must equal the declared " + f"{part_name} scope." + ) + all_passed = False + expected_phases = list(_UK_CERTIFICATION_PART_PHASES[part_name]) + if list(part.get("phases", ())) != expected_phases: + failures.append( + f"{file} parts.{part_name}.phases must be {expected_phases}, " + f"got {part.get('phases')!r}." + ) + for field, expected_digest in _UK_CERTIFICATION_PART_DIGESTS[ + part_name + ].items(): + if part.get(field) != expected_digest: + failures.append( + f"{file} parts.{part_name}.{field} does not match the " + "committed spec's scoped manifest digest." + ) + statuses = part.get("statuses") + expected_statuses = {"passed": len(expected_scope)} + if statuses != expected_statuses: + failures.append( + f"{file} parts.{part_name}.statuses must be " + f"{expected_statuses}, got {statuses!r}: shippability is " + "recomputed from the parts, and only a fully-passed part " + "certifies." + ) + all_passed = False + part_sha = part.get("sha256") + if not isinstance(part_sha, str) or not _SHA256_RE.fullmatch(part_sha): + failures.append( + f"{file} parts.{part_name}.sha256 must be a sha256 hex digest." + ) + + # The union and overlap are properties of the mirrored scopes; assert + # them against the full entry-id mirror so the three constants cannot + # drift apart silently. + union: dict[str, int] = {} + for scope in _UK_CERTIFICATION_PART_SCOPES.values(): + for gate_id in scope: + union[gate_id] = union.get(gate_id, 0) + 1 + if set(union) != _UK_GATE_BATTERY_ENTRY_IDS: + failures.append( + f"{file} mirrored part scopes do not union to the declared " + "gate-entry set." + ) + overlap = sorted( + gate_id + for gate_id, count in union.items() + if count > 1 and gate_id not in _UK_CERTIFICATION_SHARED_GATE_IDS + ) + if overlap: + failures.append( + f"{file} mirrored part scopes overlap beyond the declared shared " + f"ids: {overlap}." + ) + + spec = certification.get("spec") + if not isinstance(spec, Mapping): + failures.append(f"{file} spec must be an object.") + else: + for field, expected in ( + ("gates_manifest_sha256", _UK_GATE_BATTERY_GATES_MANIFEST_SHA256), + ("policy_sha256", _UK_GATE_BATTERY_POLICY_SHA256), + ("spec_fingerprint", _UK_GATE_BATTERY_SPEC_FINGERPRINT), + ): + if spec.get(field) != expected: + failures.append( + f"{file} spec.{field} does not match the committed " + "full-manifest pin." + ) + if spec.get("declared_entry_count") != len(_UK_GATE_BATTERY_ENTRY_IDS): + failures.append( + f"{file} spec.declared_entry_count must be " + f"{len(_UK_GATE_BATTERY_ENTRY_IDS)}." + ) + if list(spec.get("declared_phases", ())) != list(_UK_GATE_BATTERY_PHASES): + failures.append( + f"{file} spec.declared_phases must be " + f"{list(_UK_GATE_BATTERY_PHASES)}." + ) + if list(spec.get("shared_gate_ids", ())) != sorted( + _UK_CERTIFICATION_SHARED_GATE_IDS + ): + failures.append( + f"{file} spec.shared_gate_ids must be " + f"{sorted(_UK_CERTIFICATION_SHARED_GATE_IDS)}." + ) + + if ( + calibration_diagnostics_sha256 is not None + and certification.get("diagnostics_sha256") != calibration_diagnostics_sha256 + ): + failures.append( + f"{file} diagnostics_sha256 does not match the release's " + "calibration_diagnostics.json bytes." + ) + + if certification.get("shippable") is not True or not all_passed: + failures.append( + f"{file} does not certify a shippable candidate: shippable must " + "be true and every part fully passed." + ) + + attestation = certification.get("attestation") + if not isinstance(attestation, Mapping): + failures.append(f"{file} attestation must be an object.") + return + verification_key = _uk_gate_battery_verification_key(failures) + if verification_key is None: + return + expected_key_sha256 = hashlib.sha256(verification_key).hexdigest() + if attestation.get("signing_key_sha256") != expected_key_sha256: + failures.append( + f"{file} attestation.signing_key_sha256 does not identify the " + "trusted release key." + ) + unsigned = dict(certification) + unsigned["attestation"] = { + **{str(key): value for key, value in attestation.items()}, + "signature": None, + } + expected_signature = hmac.new( + verification_key, + _canonical_json_bytes(unsigned), + hashlib.sha256, + ).hexdigest() + signature = attestation.get("signature") + if not isinstance(signature, str) or not hmac.compare_digest( + signature, expected_signature + ): + failures.append( + f"{file} attestation.signature does not authenticate the " + "complete certification with the trusted release key." + ) + + def _check_calibration_diagnostics( diagnostics: Mapping, failures: list[str], @@ -4037,6 +4369,44 @@ def validate_release_dir(release_dir: Path | str) -> None: f"got {report_schema!r}." ) + certification_path = release_dir / _UK_RELEASE_CERTIFICATION_FILE + # A release that ships any national-line part must ship the composed + # verdict: the shippability claim lives only in the certification, so a + # directory carrying a release-cut report, or a calibration-seam-scoped + # terminal report, without release_certification.json is refused rather + # than validating clean by omission. (The id-keyed required-files rule + # lands with the national publication integration, once the canonical + # national release-id form exists.) + national_line_parts = [] + if (release_dir / _UK_RELEASE_CUT_GATE_REPORT_FILE).is_file(): + national_line_parts.append(_UK_RELEASE_CUT_GATE_REPORT_FILE) + seam_terminal_path = release_dir / _UK_TERMINAL_GATE_REPORT_FILE + if seam_terminal_path.is_file(): + try: + probe = json.loads(seam_terminal_path.read_text(encoding="utf-8")) + except (OSError, ValueError): + probe = None + if isinstance(probe, Mapping) and probe.get("posture") == "calibration_seam": + national_line_parts.append( + f"{_UK_TERMINAL_GATE_REPORT_FILE} (posture calibration_seam)" + ) + if national_line_parts and not certification_path.is_file(): + failures.append( + f"{_UK_RELEASE_CERTIFICATION_FILE} is missing while national-line " + f"gate artifacts are present ({', '.join(national_line_parts)}); " + "a candidate's shippability verdict comes only from the " + "certification, so its omission cannot validate clean." + ) + if certification_path.is_file(): + certification = _load_json(certification_path, failures) + if certification is not None: + _check_uk_release_certification( + certification, + release_id=release_id, + calibration_diagnostics_sha256=calibration_diagnostics_sha256, + failures=failures, + ) + _check_cross_manifest_consistency( build_manifest, release_manifest, diff --git a/packages/microcosm-data/tests/test_contract.py b/packages/microcosm-data/tests/test_contract.py index c25f3148..4f4910eb 100644 --- a/packages/microcosm-data/tests/test_contract.py +++ b/packages/microcosm-data/tests/test_contract.py @@ -22,6 +22,7 @@ RELEASE_MANIFEST_SCHEMA_VERSION, US_SOURCE_COVERAGE_DIAGNOSTICS_FILE, ReleaseContractError, + contract, required_release_files, validate_evidence_release_dir, validate_release_dir, @@ -134,13 +135,13 @@ def _trusted_terminal_gate_signing_key(monkeypatch) -> None: UK_GATE_BATTERY_PRODUCER = "microcosm.build.gate_battery" UK_GATE_BATTERY_SIGNING_KEY_ENV = "MICROCOSM_UK_TERMINAL_GATE_SIGNING_KEY" UK_GATE_BATTERY_POLICY_SHA256 = ( - "0b215cad96263fc8ee937facd189212b0f60639bb317ecdf6d19d7c7004689d9" + "5459347c9077b2acd5970a62d818e3ddd063d86d6c3dbce4d32dcacec3bdc414" ) UK_GATE_BATTERY_GATES_MANIFEST_SHA256 = ( - "fe580e1f39924c40f22c9826c21df8a0d02273cf0660dccf13039d173fadee85" + "f68e10d8ff6654b7fc707da0508ea063b0f3c96b8a813b7098298727d43b9d6d" ) UK_GATE_BATTERY_SPEC_FINGERPRINT = ( - "c6b43744bdc2ac3187f503d719aea12d764a521d24382f0e0390bf7b92a2bd5f" + "2a4a8b18d024f80782b375539c87d57006592d64470553a4da5a378791254faa" ) UK_GATE_BATTERY_DEGENERATE_EVIDENCE_SHA256 = ( "d0d024043132fa07c378c393dbe2b24fe99bf19e876bcc39997d2c80cc9bd4f6" @@ -876,8 +877,8 @@ def _terminal_gate_details(name: str) -> dict: return { "columns_checked": 1, "top_k": 100, - "max_top_share": 0.9970712395200448, - "min_nonzero_records": 274, + "max_top_share": 0.9994670564654868, + "min_nonzero_records": 104, "top_share": {"self_employment_income": 0.5}, "carrier_counts": {"self_employment_income": 274}, "thin_columns": {}, @@ -4893,3 +4894,191 @@ def test_breach_acknowledgment_matching_is_name_delimited() -> None: assert not contract_module._token_appears_delimited( "b.c@2024", "this names a.b.c@2024" ) + + +# --- UK release certification (microcosm#757 B5) --------------------------- + + +def _green_uk_certification(key: bytes) -> dict: + parts = {} + for part_name, scope in contract._UK_CERTIFICATION_PART_SCOPES.items(): + parts[part_name] = { + "path": f"{part_name}.json", + "sha256": "a" * 64, + "release_id": "uk-757-first-certified-cut", + "phases": list(contract._UK_CERTIFICATION_PART_PHASES[part_name]), + "entry_ids": sorted(scope), + "gates_manifest_sha256": contract._UK_CERTIFICATION_PART_DIGESTS[ + part_name + ]["gates_manifest_sha256"], + "policy_sha256": contract._UK_CERTIFICATION_PART_DIGESTS[part_name][ + "policy_sha256" + ], + "statuses": {"passed": len(scope)}, + } + certification = { + "schema_version": 1, + "kind": "uk_release_certification", + "country": "uk", + "release_id": "uk-757-first-certified-cut", + "candidate": { + "name": "microcosm_uk_2024", + "filename": "microcosm_uk_2024.h5", + "sha256": "b" * 64, + "size_bytes": 1, + }, + "parts": parts, + "spec": { + "gates_manifest_sha256": contract._UK_GATE_BATTERY_GATES_MANIFEST_SHA256, + "policy_sha256": contract._UK_GATE_BATTERY_POLICY_SHA256, + "spec_fingerprint": contract._UK_GATE_BATTERY_SPEC_FINGERPRINT, + "declared_entry_count": len(contract._UK_GATE_BATTERY_ENTRY_IDS), + "declared_phases": list(contract._UK_GATE_BATTERY_PHASES), + "shared_gate_ids": sorted(contract._UK_CERTIFICATION_SHARED_GATE_IDS), + }, + "doctrine": {"payload": {"epochs": 1500}, "overrides": {}}, + "diagnostics_sha256": "c" * 64, + "score_receipt": {"filename": "score_vs_enhanced_frs.json", "sha256": "d" * 64}, + "exclusions_evaluated_on": "2026-08-27", + "shippable": True, + } + attestation = { + "producer": "microcosm.build.uk_runtime.release_certification", + "signature_algorithm": "hmac-sha256", + "signing_key_sha256": hashlib.sha256(key).hexdigest(), + "signature": None, + } + certification["attestation"] = attestation + attestation["signature"] = hmac.new( + key, contract._canonical_json_bytes(certification), hashlib.sha256 + ).hexdigest() + return certification + + +def _certification_failures(certification, monkeypatch, key: bytes) -> list[str]: + monkeypatch.setenv( + UK_GATE_BATTERY_SIGNING_KEY_ENV, base64.b64encode(key).decode("ascii") + ) + failures: list[str] = [] + contract._check_uk_release_certification( + certification, + release_id="uk-757-first-certified-cut", + calibration_diagnostics_sha256="c" * 64, + failures=failures, + ) + return failures + + +def test_uk_release_certification_green(monkeypatch) -> None: + key = bytes(range(32)) + certification = _green_uk_certification(key) + assert _certification_failures(certification, monkeypatch, key) == [] + + +def test_uk_release_certification_refusals(monkeypatch) -> None: + key = bytes(range(32)) + + certification = _green_uk_certification(key) + certification["shippable"] = False + assert any( + "does not certify a shippable candidate" in line + for line in _certification_failures(certification, monkeypatch, key) + ) + + certification = _green_uk_certification(key) + certification["parts"]["release_cut"]["statuses"] = {"passed": 15, "failed": 1} + failures = _certification_failures(certification, monkeypatch, key) + assert any("statuses" in line for line in failures) + assert any("does not certify a shippable" in line for line in failures) + + certification = _green_uk_certification(key) + certification["parts"]["spine"]["entry_ids"] = sorted( + set(certification["parts"]["spine"]["entry_ids"]) - {"uk_brma_enum_domain"} + ) + assert any( + "entry_ids" in line + for line in _certification_failures(certification, monkeypatch, key) + ) + + certification = _green_uk_certification(key) + certification["parts"]["calibration_seam"]["gates_manifest_sha256"] = "e" * 64 + assert any( + "scoped manifest digest" in line + for line in _certification_failures(certification, monkeypatch, key) + ) + + certification = _green_uk_certification(key) + certification["diagnostics_sha256"] = "f" * 64 + assert any( + "diagnostics_sha256" in line + for line in _certification_failures(certification, monkeypatch, key) + ) + + # A tampered field breaks the signature: the flag flip is caught both as + # a verdict refusal and as a signature failure. + certification = _green_uk_certification(key) + certification["release_id"] = "uk-757-first-certified-cut" + certification["doctrine"] = {"payload": {}, "overrides": {}} + assert any( + "signature does not authenticate" in line + for line in _certification_failures(certification, monkeypatch, key) + ) + + certification = _green_uk_certification(key) + del certification["score_receipt"] + assert any( + "exactly the certification fields" in line + for line in _certification_failures(certification, monkeypatch, key) + ) + + +def test_national_line_artifacts_require_the_certification(tmp_path) -> None: + # A release that ships any national-line gate part without the composed + # certification must refuse: the shippability verdict lives only in the + # certification, so its omission cannot validate clean (green-by-absence). + release_dir = tmp_path / "uk-757-first-certified-cut" + release_dir.mkdir() + (release_dir / "release_cut_gates.json").write_text("{}", encoding="utf-8") + + with pytest.raises(ReleaseContractError) as caught: + validate_release_dir(release_dir) + assert any( + "release_certification.json is missing while national-line" in line + for line in caught.value.failures + ) + + # A calibration-seam-scoped terminal report is a national-line part too. + seam_dir = tmp_path / "uk-757-seam-only" + seam_dir.mkdir() + (seam_dir / "terminal_gates.json").write_text( + '{"posture": "calibration_seam"}', encoding="utf-8" + ) + with pytest.raises(ReleaseContractError) as caught: + validate_release_dir(seam_dir) + assert any( + "release_certification.json is missing while national-line" in line + for line in caught.value.failures + ) + + # With the certification present the omission failure clears (the file's + # own validation and the base required-files failures still apply). + (release_dir / "release_certification.json").write_text("{}", encoding="utf-8") + with pytest.raises(ReleaseContractError) as caught: + validate_release_dir(release_dir) + assert not any( + "is missing while national-line" in line for line in caught.value.failures + ) + assert any( + "must carry exactly the certification fields" in line + for line in caught.value.failures + ) + + +def test_national_release_id_requires_the_certification() -> None: + from microcosm.data.contract import required_release_files + + required = required_release_files("microcosm-uk-2024-25-national") + assert "release_certification.json" in required + assert "release_certification.json" not in required_release_files( + "dev-757-rebind-proof" + ) diff --git a/tools/build_uk_frs_spine.py b/tools/build_uk_frs_spine.py index b739c57e..bb3fccbf 100644 --- a/tools/build_uk_frs_spine.py +++ b/tools/build_uk_frs_spine.py @@ -40,7 +40,10 @@ ) from microcosm.build.uk_runtime.age_tail import UKAgeTailStageTransform from microcosm.build.uk_runtime.battery_bindings import UK_GATE_REGISTRY -from microcosm.build.uk_runtime.calibration_run import UK_SPINE_GATE_SCOPE +from microcosm.build.uk_runtime.calibration_run import ( + UK_SPINE_GATE_SCOPE, + uk_scoped_gate_manifest, +) from microcosm.build.uk_runtime.cgt_imputation import uk_cgt_spine_stage_transform from microcosm.build.uk_runtime.cgt_structure import ( UKCGTBandDonorStageTransform, @@ -504,6 +507,52 @@ def _collect_stage_evidence( return evidence_by_stage +def _collect_fit_weight_records( + *, + stage_names: Sequence[str], + implementations: Mapping[str, object], +) -> dict[str, list[dict[str, str]]]: + """Persist each fitting stage's resolved weight kinds into the sidecar. + + The terminal weights audit (``uk_weights_audit``) consumes + :class:`FitWeightRecord` evidence that only exists on live stage + objects; the release-cut certification producer runs in a later + process, so the sidecar carries the records across the run boundary. + Duck-typed like ``stage_evidence``: every stage whose transform exposes + ``fit_weight_records`` contributes, in stage order. A fitting stage + whose records are missing, unreadable, or empty records an empty list — + the audit binding fails an empty record set, so the gap stays visible + rather than vanishing from the sidecar. + """ + + records_by_stage: dict[str, list[dict[str, str]]] = {} + for stage_name in stage_names: + implementation = implementations.get(stage_name) + if implementation is None: + continue + # Detect the hook without evaluating it: a raising property must + # count as a fitting stage with unreadable records, not vanish. + exposes_records = ( + getattr(type(implementation), "fit_weight_records", None) is not None + or "fit_weight_records" in getattr(implementation, "__dict__", {}) + ) + if not exposes_records: + continue + try: + records = tuple(implementation.fit_weight_records or ()) + except Exception: # noqa: BLE001 - unreadable records fail the audit + records_by_stage[stage_name] = [] + continue + records_by_stage[stage_name] = [ + { + "fit_name": str(record.fit_name), + "weight_kind": str(record.weight_kind), + } + for record in records + ] + return records_by_stage + + def _build_sidecar( *, frame, @@ -729,19 +778,24 @@ def _spine_gate_report_path(spine_h5: Path) -> Path: def _spine_gate_manifest_from_spec(spec) -> GatesManifest | None: + """The spine build's scoped battery manifest, from the shared helper. + + A spec without a gates block leaves the battery unarmed (``None``), + exactly as before; when armed, the filtering runs through the one + scope-filtering implementation every scoped producer shares. The + driver passes the spec it already loaded, which is also the hermetic + tests' stub point. Digests are identical to the previous local copy + because entries, phases, and the policy suffix are unchanged. + """ + source = getattr(spec, "gates", None) if source is None: return None - entries = tuple(entry for entry in source.gates if entry.id in UK_SPINE_GATE_SCOPE) - missing = sorted(set(UK_SPINE_GATE_SCOPE) - {entry.id for entry in entries}) - if missing: - raise RuntimeError(f"UK spine gate scope names undeclared gate id(s): {missing}.") - return GatesManifest( - country=source.country, - version=source.version, - policy=f"{source.policy}; spine_build_scope", + return uk_scoped_gate_manifest( + UK_SPINE_GATE_SCOPE, phases=("assembled", "transferred"), - gates=entries, + policy_suffix="spine_build_scope", + source=source, ) @@ -1072,6 +1126,12 @@ def main(argv: list[str] | None = None) -> int: ) if stage_evidence: sidecar["stage_evidence"] = stage_evidence + fit_weight_records = _collect_fit_weight_records( + stage_names=_STAGE_NAMES, + implementations=implementations, + ) + if fit_weight_records: + sidecar["fit_weight_records"] = fit_weight_records atomic_write_json(sidecar_path, sidecar) append_phase(state, "build_sidecar_written") if args.emit_nonzero_shares is not None: diff --git a/tools/calibrate_uk_national_dataset.py b/tools/calibrate_uk_national_dataset.py index 815c8f3f..2f969784 100644 --- a/tools/calibrate_uk_national_dataset.py +++ b/tools/calibrate_uk_national_dataset.py @@ -33,6 +33,7 @@ load_uk_calibration_measure_exclusions, ) from microcosm.build.uk_runtime.national_doctrine import uk_doctrine_with_overrides +from microcosm.build.uk_runtime.release_identity import UK_NATIONAL_RELEASE_ID from microcosm.calibrate import TargetRegistry _SHA256 = re.compile(r"[0-9a-f]{64}") @@ -110,7 +111,6 @@ def main(argv: list[str] | None = None) -> int: "ledger_facts": _ledger_facts_pin(artifact), }, run_config_extra={"calibration_year": calibration_year}, - release_candidate=args.release_candidate, release_id=args.release_id, logbook_prev_row_digest=args.logbook_prev_row_digest, ) @@ -149,34 +149,28 @@ def _parse_args(argv: list[str] | None) -> argparse.Namespace: args.terminal_gate_json = args.terminal_gate_json or args.staging_h5.with_suffix( ".terminal_gates.json" ) - override_flags = { - "--epochs": args.epochs, - "--target-weight-rule": args.target_weight_rule, - "--learning-rate": args.learning_rate, - "--target-loss-cap": args.target_loss_cap, - } - passed_overrides = [flag for flag, value in override_flags.items() if value is not None] - if args.release_candidate and passed_overrides: - parser.error( - "--release-candidate is refused with doctrine override flag(s): " - + ", ".join(passed_overrides) - ) - if args.release_candidate and args.measure_exclusions is not None: - # An exclusion register prunes the compiled target surface before the - # solve, and the calibration-scoped battery does not carry the - # target-surface gate — so an operator-supplied register would be an - # unreviewed narrowing of what a release candidate was measured - # against. Release candidates use the committed register only. + if args.release_candidate: + # The seam refuses release-candidate posture outright (the #757 + # release-cut audit, issue comment 5413502559): its scoped battery + # covers 6 of the declared entries and must never sign a + # shippability claim, and its evidence_absent gaps already refuse + # upstream. A candidate's verdict comes only from the release-cut + # certification producer (tools/certify_uk_release_cut.py). parser.error( - "--release-candidate is refused with --measure-exclusions: a " - "release candidate is measured against the committed target " - "surface, not an operator-supplied one" + "--release-candidate is refused on the calibration seam: the " + "seam's scoped battery cannot sign shippability; run the " + "release-cut certification producer instead" ) if ( - not args.release_candidate - and (_CANONICAL_UK_RELEASE_ID.fullmatch(args.release_id) or args.release_id == _UK_JUNE_RELEASE_ID) + _CANONICAL_UK_RELEASE_ID.fullmatch(args.release_id) + or args.release_id == _UK_JUNE_RELEASE_ID + or args.release_id == UK_NATIONAL_RELEASE_ID ): - parser.error("canonical UK release ids require --release-candidate") + parser.error( + "canonical UK release ids belong to the release-cut " + "certification producer; the seam runs under a staging or dev " + "release id" + ) _validate_distinct_paths( { "--input-h5": args.input_h5, diff --git a/tools/certify_uk_release_cut.py b/tools/certify_uk_release_cut.py new file mode 100644 index 00000000..4ec6db5b --- /dev/null +++ b/tools/certify_uk_release_cut.py @@ -0,0 +1,318 @@ +"""Certify a calibrated UK national candidate for release. + +The release-cut certification producer (microcosm#757 item B5): runs the 16 +declared national preflight/terminal gates over the calibrated candidate — +the executable home the June driver's retirement left empty — then composes +the multi-part certification over the spine build's battery report, the +calibration seam's battery report, and the fresh release-cut report. The +parts must union to the full declared gate-entry set with no gap and no +overlap beyond the declared shared ids, each signed by its producer, over +one closed identity join (spine report -> sidecar -> build record -> +diagnostics -> candidate bytes). A candidate's shippability verdict comes +only from the certification this driver writes. + +The battery always runs at release-candidate strictness: evidence_absent +gaps block. The rule-1 score receipt is cross-pinned into the certification +(the audit's third carried defect), so the score is signed run evidence +rather than a null slot. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import time +from datetime import UTC, datetime +from pathlib import Path + +from microcosm.build.ledger_artifact import load_ledger_consumer_artifact +from microcosm.build.logbook_adoption import ( + AttemptState, + append_phase, + apply_error_verdict, + error_receipt_path, + git_code_pin, + local_artifact_reference, + record_terminal_attempt, + resolve_predecessor, + role_pins_digest, + write_error_receipt, +) +from microcosm.build.uk_runtime.frs_release import load_uk_frs_release +from microcosm.build.uk_runtime.ledger_targets import compile_uk_target_registry +from microcosm.build.uk_runtime.measure_simulation import ( + apply_uk_calibration_measure_exclusions, + load_uk_calibration_measure_exclusions, +) +from microcosm.build.uk_runtime.national_frame import load_uk_national_frame +from microcosm.build.uk_runtime.parity_reference import load_efrs_parity_reference +from microcosm.build.uk_runtime.release_certification import ( + compose_uk_release_certification, + rehydrate_uk_fit_weight_records, + run_uk_release_cut_battery, + uk_release_parity_evidence, +) +from microcosm.build.uk_runtime.release_input_coverage import ( + PolicyEngineUKCoverageEngine, +) +from microcosm.build.uk_runtime.weighted_integrity import ( + exclusion_evaluation_date, + load_uk_input_mass_reference, +) + +_SHA256 = re.compile(r"[0-9a-f]{64}") +_REPOSITORY = Path(__file__).resolve().parents[1] +_PIPELINE = "uk-frs-release-certification" +_LEDGER_COMPILE_PARITY_PERIODS = (2023, 2025) + + +def main(argv: list[str] | None = None) -> int: + args = _parse_args(argv) + started_at = time.perf_counter() + started_ts = datetime.now(UTC) + code_pin = git_code_pin(_REPOSITORY) + predecessor = resolve_predecessor(args.logbook_prev_row_digest) + source_pins = { + "candidate_h5": {"sha256": args.candidate_sha256}, + "ledger_facts": {"sha256": args.ledger_facts_sha256}, + } + state = AttemptState( + build_id=f"{_PIPELINE}-attempt-{started_ts.strftime('%Y%m%dT%H%M%SZ')}", + identity_digest=hashlib.sha256( + json.dumps( + { + "pipeline": _PIPELINE, + "release_id": args.release_id, + "candidate_sha256": args.candidate_sha256, + }, + sort_keys=True, + ).encode("utf-8") + ).hexdigest(), + input_pins_digest=role_pins_digest(source_pins), + phases_reached=["attempt_started"], + gate_verdicts={}, + ) + spool_dir = args.certification_json.parent / "logbook-spool" + try: + summary = _run(args, state) + except BaseException as error: + error_path = write_error_receipt( + error_receipt_path( + args.certification_json.parent / "logbook-receipts", + build_id=state.build_id, + ), + state=state, + pipeline=_PIPELINE, + error=error, + ) + apply_error_verdict( + state, + f"{local_artifact_reference(error_path, repository_hint=_REPOSITORY)}" + "#/error_type", + ) + record_terminal_attempt( + state=state, + started_at=started_at, + started_ts=started_ts, + pipeline=_PIPELINE, + rung="f100", + seed=None, + code_pin=code_pin, + disposition=( + "discarded" if isinstance(error, KeyboardInterrupt) else "failed" + ), + predecessor=predecessor, + spool_dir=spool_dir, + ) + raise + state.artifact_location = local_artifact_reference( + args.certification_json, repository_hint=_REPOSITORY + ) + record_terminal_attempt( + state=state, + started_at=started_at, + started_ts=started_ts, + pipeline=_PIPELINE, + rung="f100", + seed=None, + code_pin=code_pin, + disposition="certified", + predecessor=predecessor, + spool_dir=spool_dir, + ) + print(json.dumps(summary, indent=2, sort_keys=True)) + return 0 + + +def _run(args: argparse.Namespace, state: AttemptState) -> dict[str, object]: + measured = hashlib.sha256(args.candidate_h5.read_bytes()).hexdigest() + if measured != args.candidate_sha256: + raise SystemExit( + "error: --candidate-h5 sha mismatch: " + f"measured {measured}, pinned {args.candidate_sha256}" + ) + sidecar_path = args.spine_h5.with_suffix(".build.json") + if not sidecar_path.is_file(): + raise SystemExit(f"error: spine build sidecar absent: {sidecar_path}") + sidecar = json.loads(sidecar_path.read_text(encoding="utf-8")) + spine_report_path = args.spine_h5.with_suffix(".spine_gates.json") + + diagnostics_bytes = args.diagnostics_json.read_bytes() + diagnostics = json.loads(diagnostics_bytes) + diagnostics_sha = hashlib.sha256(diagnostics_bytes).hexdigest() + build_record = json.loads(args.build_record_json.read_text(encoding="utf-8")) + recorded_diagnostics = ( + build_record.get("artifacts", {}).get("diagnostics_json", {}).get("sha256") + ) + if recorded_diagnostics != diagnostics_sha: + raise SystemExit( + "error: --diagnostics-json bytes do not match the build record's " + f"binding ({diagnostics_sha} != {recorded_diagnostics})" + ) + append_phase(state, "inputs_bound") + + artifact = load_ledger_consumer_artifact( + args.ledger_facts, + expected_facts_sha256=args.ledger_facts_sha256, + expected_manifest_sha256=args.ledger_manifest_sha256, + ) + ledger_registries = {} + for period in _LEDGER_COMPILE_PARITY_PERIODS: + compilation = compile_uk_target_registry( + artifact.facts, target_period=period + ) + ledger_registries[period] = compilation.registry + calibration_year = load_uk_frs_release().calibration_year + if calibration_year in ledger_registries: + reference_compiled = ledger_registries[calibration_year] + else: + reference_compiled = compile_uk_target_registry( + artifact.facts, target_period=calibration_year + ).registry + evaluated_on = exclusion_evaluation_date(None) + exclusions = load_uk_calibration_measure_exclusions() + reference_registry, _receipt = apply_uk_calibration_measure_exclusions( + reference_compiled, exclusions, now=evaluated_on + ) + append_phase(state, "registries_compiled") + + frame, _provenance = load_uk_national_frame(args.candidate_h5) + engine = PolicyEngineUKCoverageEngine() + parity_evidence = uk_release_parity_evidence( + frame, + diagnostics_targets=diagnostics["targets"], + reference_registry=reference_registry, + parity_reference=load_efrs_parity_reference(), + ) + report = run_uk_release_cut_battery( + frame, + report_path=args.release_cut_gate_json, + release_id=args.release_id, + diagnostics_sha256=diagnostics_sha, + coverage_engine=engine, + build_stage_names=sidecar["stages"], + ledger_registries=ledger_registries, + parity_evidence=parity_evidence, + fit_weight_records=rehydrate_uk_fit_weight_records(sidecar), + input_mass_reference=load_uk_input_mass_reference( + args.input_mass_reference + ), + exclusions_evaluated_on=evaluated_on, + ) + append_phase(state, "release_cut_gates_evaluated") + for gate_id, payload in report["gates"].items(): + state.gate_verdicts[gate_id] = { + "verdict": payload["status"], + "receipt": ( + f"local://{args.release_cut_gate_json.name}#/gates/{gate_id}" + ), + } + + certification = compose_uk_release_certification( + release_id=args.release_id, + candidate_name=args.candidate_name, + candidate_path=args.candidate_h5, + candidate_sha256=args.candidate_sha256, + spine_report_path=spine_report_path, + seam_report_path=args.seam_gate_report, + release_cut_report_path=args.release_cut_gate_json, + spine_sidecar=sidecar, + build_record=build_record, + score_receipt_path=args.score_receipt, + exclusions_evaluated_on=evaluated_on, + certification_path=args.certification_json, + ) + append_phase(state, "certification_written") + return { + "certification_json": str(args.certification_json), + "certification_sha256": hashlib.sha256( + args.certification_json.read_bytes() + ).hexdigest(), + "release_cut_gate_json": str(args.release_cut_gate_json), + "shippable": certification["shippable"], + "parts": { + name: part["statuses"] for name, part in certification["parts"].items() + }, + } + + +def _parse_args(argv: list[str] | None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--candidate-h5", required=True, type=Path) + parser.add_argument("--candidate-sha256", required=True, type=_sha256) + parser.add_argument( + "--candidate-name", + required=True, + help="The dataset name the certification certifies, e.g. microcosm_uk_2024.", + ) + parser.add_argument("--spine-h5", required=True, type=Path) + parser.add_argument("--diagnostics-json", required=True, type=Path) + parser.add_argument("--build-record-json", required=True, type=Path) + parser.add_argument("--seam-gate-report", required=True, type=Path) + parser.add_argument("--ledger-facts", required=True, type=Path) + parser.add_argument("--ledger-facts-sha256", required=True, type=_sha256) + parser.add_argument("--ledger-manifest-sha256", required=True, type=_sha256) + parser.add_argument("--input-mass-reference", required=True, type=Path) + parser.add_argument("--score-receipt", required=True, type=Path) + parser.add_argument("--release-id", required=True) + parser.add_argument("--release-cut-gate-json", type=Path) + parser.add_argument("--certification-json", type=Path) + parser.add_argument("--logbook-prev-row-digest", type=_sha256) + args = parser.parse_args(argv) + args.release_cut_gate_json = ( + args.release_cut_gate_json + or args.candidate_h5.with_suffix(".release_cut_gates.json") + ) + args.certification_json = ( + args.certification_json + or args.candidate_h5.with_suffix(".release_certification.json") + ) + distinct = { + "--candidate-h5": args.candidate_h5, + "--spine-h5": args.spine_h5, + "--diagnostics-json": args.diagnostics_json, + "--build-record-json": args.build_record_json, + "--seam-gate-report": args.seam_gate_report, + "--release-cut-gate-json": args.release_cut_gate_json, + "--certification-json": args.certification_json, + "--score-receipt": args.score_receipt, + } + resolved: dict[Path, str] = {} + for flag, path in distinct.items(): + canonical = path.resolve() + if canonical in resolved: + parser.error(f"{flag} aliases {resolved[canonical]}: {path}") + resolved[canonical] = flag + return args + + +def _sha256(value: str) -> str: + if not _SHA256.fullmatch(value): + raise argparse.ArgumentTypeError("expected a 64-character lowercase sha256") + return value + + +if __name__ == "__main__": + raise SystemExit(main())