From d4a9dc08b277667a0850a44a657d9829f6f176f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:49:27 +0200 Subject: [PATCH 01/12] Sign the adjudicated target-fit exclusion pass onto the windowed register The 42 dispositions of microcosm#757 issue comment 5427936411 (adjudicated 2026-08-26) land as schema-2 windowed exclusions: 11 top-income 1m+ channel cells, 3 SLC channels, 16 UC payment-distribution cells, the OBR welfare-cap pair, 3 ONS composition cells pending the relationship-to-head column (microcosm#791, filed with this change), 6 sparse HMRC band cells, and obr.fuel_duties. The six UC caseload/two-child-limit cells stay bound for the would_claim_uc lever run; couple_no_children and the marginal state_pension 40-50k cell stay bound to ride the re-run. Co-Authored-By: Claude Fable 5 --- .../757-target-fit-exclusion-pass.changed.md | 12 + .../uk/calibration_measure_exclusions.json | 378 ++++++++++++++++++ .../tests/test_uk_measure_simulation.py | 62 ++- 3 files changed, 445 insertions(+), 7 deletions(-) create mode 100644 changelog.d/757-target-fit-exclusion-pass.changed.md 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/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..60975429 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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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": "policyengine-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/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): From 683d44f6c0536816c862198f25c26f91a9571a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:53:49 +0200 Subject: [PATCH 02/12] Raise the would_claim_uc lever to 0.85: the pre-registered U8 re-open The seven UC caseload/two-child-limit targets get their adjudicated lever run (uk-data#452): the take-up contract carries a dated 2024 value of 0.85 over the 2015 incumbent-parity 0.55, with the support-arithmetic derivation recorded in the entry. The contract digest moves with the resource; the parity divergence on the rebuilt candidate is signed from its measured extraction in the licensed phase. Co-Authored-By: Claude Fable 5 --- changelog.d/757-would-claim-uc-lever.changed.md | 7 +++++++ .../src/microcosm/build/uk/take_up_contract.json | 9 +++++---- .../microcosm-build/tests/test_uk_take_up_contract.py | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 changelog.d/757-would-claim-uc-lever.changed.md 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..7f03387f --- /dev/null +++ b/changelog.d/757-would-claim-uc-lever.changed.md @@ -0,0 +1,7 @@ +`would_claim_uc` rises 0.55 -> 0.85 at build year 2024: the pre-registered +U8 lever (uk-data#452) for the seven UC caseload/two-child-limit targets, +re-adjudicated by the #757 `uk_target_fit` dispositions (issue comment +5427936411). The derivation is recorded in the contract entry; the +incumbent-parity 0.55 stays as dated history, and the whole-spine parity +divergence this creates on the `would_claim_uc` column is signed on the +quantitative register from the rebuilt candidate's measured extraction. 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..8fd971ac 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 @@ -64,15 +64,16 @@ "output": "would_claim_uc", "entity": "benunit", "values": { - "2015-01-01": 0.55 + "2015-01-01": 0.55, + "2024-01-01": 0.85 }, "source": { - "source": "U8 adjudication and upstream issue uk-data#452", + "source": "U8 re-adjudication (microcosm#757 uk_target_fit dispositions, issue comment 5427936411) and upstream issue uk-data#452", "agency": "PolicyEngine", - "citation": "Frozen at 0.55 by adjudication for parity with the incumbent UK pipeline.", + "citation": "Raised 0.55 to 0.85 as the pre-registered would_claim_uc lever for the seven UC caseload/two-child-limit targets. Derivation, measured on the rebind packet: at 0.55 the design-weight UC caseload mass is 3.13m benunits against the 6.76m administrative target, a 2.16x required lift the solve could not reach (it stalled at 1.49x, -31%); support scales with the rate, so 0.85 projects 4.84m initial mass and a 1.40x required lift, inside the solve's demonstrated envelope. 0.55 (dated 2015) was the incumbent-parity freeze and stays as history; no published UC take-up statistic exists to source either value (uk-data#452).", "status": "frozen_by_adjudication", "freeze": { - "decision": "U8", + "decision": "U8 (re-opened by the 2026-08-26 uk_target_fit disposition)", "followup": "uk-data#452" } } diff --git a/packages/microcosm-build/tests/test_uk_take_up_contract.py b/packages/microcosm-build/tests/test_uk_take_up_contract.py index 76f55e59..54ff47b7 100644 --- a/packages/microcosm-build/tests/test_uk_take_up_contract.py +++ b/packages/microcosm-build/tests/test_uk_take_up_contract.py @@ -42,7 +42,7 @@ def test_uk_contract_loads_and_selects_build_year_rates() -> None: 0.89, 0.23, 0.7, - 0.55, + 0.85, 0.5, 0.88, 0.812, From a51de2ffa11333a7c503c2a2abd88bb6205f765e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:58:56 +0200 Subject: [PATCH 03/12] Refuse shippability claims on the seam: the release verdict moves to the certification --release-candidate and canonical release ids are refused on the calibration driver (the seam's 6-entry scoped battery can never sign a shippability claim, per the 5413502559 audit); the release_candidate parameter leaves the seam runtime whole, and the build record's hand-written shippable literal is replaced by a pointer to the release-cut certification artifact. Co-Authored-By: Claude Fable 5 --- ...-seam-refuses-release-candidate.changed.md | 7 +++ .../build/uk_runtime/calibration_run.py | 25 ++++++---- .../tests/test_uk_calibration_run.py | 28 ++++++----- .../tests/test_uk_calibration_seam_driver.py | 50 +++++++++++-------- tools/calibrate_uk_national_dataset.py | 41 ++++++--------- 5 files changed, 81 insertions(+), 70 deletions(-) create mode 100644 changelog.d/757-seam-refuses-release-candidate.changed.md 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/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..099dc620 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,7 +500,6 @@ def _run_calibration_gate_battery( stage: UKNationalCalibrationStage, path: Path, *, - release_candidate: bool, release_id: str, diagnostics_sha256: str, ) -> dict[str, object]: @@ -519,8 +518,12 @@ 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}, ) diff --git a/packages/microcosm-build/tests/test_uk_calibration_run.py b/packages/microcosm-build/tests/test_uk_calibration_run.py index f21b1aa4..d25bd298 100644 --- a/packages/microcosm-build/tests/test_uk_calibration_run.py +++ b/packages/microcosm-build/tests/test_uk_calibration_run.py @@ -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() @@ -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", ) @@ -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 @@ -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"]) @@ -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..70009ba4 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)] ) diff --git a/tools/calibrate_uk_national_dataset.py b/tools/calibrate_uk_national_dataset.py index 815c8f3f..e019b413 100644 --- a/tools/calibrate_uk_national_dataset.py +++ b/tools/calibrate_uk_national_dataset.py @@ -110,7 +110,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 +148,26 @@ 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: + 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 doctrine override flag(s): " - + ", ".join(passed_overrides) + "--release-candidate is refused on the calibration seam: the " + "seam's scoped battery cannot sign shippability; run the " + "release-cut certification producer instead" ) - 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 _CANONICAL_UK_RELEASE_ID.fullmatch(args.release_id) or ( + args.release_id == _UK_JUNE_RELEASE_ID + ): parser.error( - "--release-candidate is refused with --measure-exclusions: a " - "release candidate is measured against the committed target " - "surface, not an operator-supplied one" + "canonical UK release ids belong to the release-cut " + "certification producer; the seam runs under a staging or dev " + "release id" ) - if ( - not args.release_candidate - and (_CANONICAL_UK_RELEASE_ID.fullmatch(args.release_id) or args.release_id == _UK_JUNE_RELEASE_ID) - ): - parser.error("canonical UK release ids require --release-candidate") _validate_distinct_paths( { "--input-h5": args.input_h5, From 863425e223eb75fc6a3238337b0e840180bbceda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:22:17 +0200 Subject: [PATCH 04/12] Give the sixteen national gates their runner, and the release its certification The release-cut battery runs scoped (preflight+terminal) at release-candidate strictness over the calibrated candidate, with evidence adapted from persisted artifacts - the spine sidecar now persists fit-weight records so the weights audit survives the process boundary. The multi-part certification composes the spine, seam, and release-cut reports: union with no gap and no overlap beyond the declared shared id, signed parts over the committed spec's scoped digests, full phase coverage, and a closed identity join down to the candidate bytes. The nine verifier refusal points of the 5413502559 audit move from per-report to per-certification. Co-Authored-By: Claude Fable 5 --- .../757-release-cut-certification.added.md | 16 + .../build/uk_runtime/release_certification.py | 756 ++++++++++++++++++ .../tests/test_uk_frs_spine.py | 38 + .../tests/test_uk_release_certification.py | 387 +++++++++ tools/build_uk_frs_spine.py | 52 ++ tools/certify_uk_release_cut.py | 318 ++++++++ 6 files changed, 1567 insertions(+) create mode 100644 changelog.d/757-release-cut-certification.added.md create mode 100644 packages/microcosm-build/src/microcosm/build/uk_runtime/release_certification.py create mode 100644 packages/microcosm-build/tests/test_uk_release_certification.py create mode 100644 tools/certify_uk_release_cut.py 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/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..1121f187 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/release_certification.py @@ -0,0 +1,756 @@ +"""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 + +# The certification shares the seam's scoped-manifest, admin-anchor, and +# re-signing helpers deliberately: one implementation, two producers, so the +# two reports cannot drift apart in shape. Promoting them to public names is +# a rename this increment does not need. +from microcosm.build.uk_runtime.calibration_run import ( + UK_CALIBRATION_GATE_SCOPE, + UK_NATIONAL_GATE_SCOPE, + UK_SHARED_GATE_IDS, + UK_SPINE_GATE_SCOPE, + _aggregate_admin_totals, + _resign_gate_report, + _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 _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] = [] + for _stage_name, records in block.items(): + if not isinstance(records, Sequence) or not records: + return () + for record in records: + if not isinstance(record, Mapping): + return () + collected.append( + FitWeightRecord( + fit_name=str(record["fit_name"]), + weight_kind=str(record["weight_kind"]), + ) + ) + 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 = { + str(row["name"]): float(row["relative_error"]) + for row in diagnostics_targets + } + 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 = _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() + payload["posture"] = UK_RELEASE_CUT_POSTURE + payload["scope_exclusions"] = uk_release_cut_scope_exclusions() + payload["aggregate_admin_measurement"] = admin_receipt + _resign_gate_report(payload) + _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( + parts_raw[part_name][2] + if len(parts_raw[part_name]) > 2 + else _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: + payload = json.dumps(receipt) + if candidate_sha256 not in payload: + raise UKReleaseCertificationError( + "the score receipt does not name the candidate's sha256; the " + "rule-1 score must be measured on the candidate under " + "certification." + ) + + +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 = _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, + ) + payload = run.report_payload() + return { + "gates_manifest_sha256": str(payload["gates_manifest_sha256"]), + "policy_sha256": str(payload["policy_sha256"]), + "spec_fingerprint": str(payload["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/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_release_certification.py b/packages/microcosm-build/tests/test_uk_release_certification.py new file mode 100644 index 00000000..0834b03c --- /dev/null +++ b/packages/microcosm-build/tests/test_uk_release_certification.py @@ -0,0 +1,387 @@ +"""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_gate_report, + _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 = _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_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({"candidate": {"sha256": candidate_sha}, "verdict": "scored"}), + 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": []}} + ) + == () + ) + + +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_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, + "_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/tools/build_uk_frs_spine.py b/tools/build_uk_frs_spine.py index b739c57e..64ad73dc 100644 --- a/tools/build_uk_frs_spine.py +++ b/tools/build_uk_frs_spine.py @@ -504,6 +504,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, @@ -1072,6 +1118,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/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()) From 43b2aee94ddc416a46d342dbc8bced08322edd92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:22:27 +0200 Subject: [PATCH 05/12] Re-arm the QRF tail gate from the L3 baselines, with the measuring run in the notes top_k 100 stays the grid anchor; max_top_share re-mints at the exact measured checked-surface maximum (0.9994670564654868, hmrc_spi_other_social_security_income, 104 carriers on spine-a); min_nonzero_records at the thinnest measured column above the anchor (104). The three saturated sub-anchor columns go thin visibly on every run. Gate policy/manifest/fingerprint digests re-cut from the live producer payload; contract threshold mirrors move in lockstep. Co-Authored-By: Claude Fable 5 --- changelog.d/757-qrf-tail-rearm.changed.md | 12 ++++++++++++ .../src/microcosm/build/uk/gates.json | 6 +++--- .../microcosm-build/tests/test_country_spec.py | 4 ++-- .../src/microcosm/data/contract.py | 16 +++++++++------- packages/microcosm-data/tests/test_contract.py | 10 +++++----- 5 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 changelog.d/757-qrf-tail-rearm.changed.md 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/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/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-data/src/microcosm/data/contract.py b/packages/microcosm-data/src/microcosm/data/contract.py index d6b295e8..cc04b42c 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 diff --git a/packages/microcosm-data/tests/test_contract.py b/packages/microcosm-data/tests/test_contract.py index c25f3148..20fe6240 100644 --- a/packages/microcosm-data/tests/test_contract.py +++ b/packages/microcosm-data/tests/test_contract.py @@ -134,13 +134,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 +876,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": {}, From ff76c16341e9a6831c878d6a7ecabde8fc3be638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:27:13 +0200 Subject: [PATCH 06/12] Teach the publication contract the certification's shape The nine refusal points the 5413502559 audit measured against a scoped report resolve at the certification: microcosm-data verifies the composed document - mirrored part scopes, scoped-manifest digest pins, full-manifest spec pins, the union partition, recomputed shippability, the diagnostics join, and the release-key signature - with build-shard sync tests holding every mirror in lockstep. Co-Authored-By: Claude Fable 5 --- .../757-certification-contract.added.md | 8 + .../tests/test_gate_battery_contract_pins.py | 56 +++ .../src/microcosm/data/contract.py | 328 ++++++++++++++++++ .../microcosm-data/tests/test_contract.py | 137 ++++++++ 4 files changed, 529 insertions(+) create mode 100644 changelog.d/757-certification-contract.added.md 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/packages/microcosm-build/tests/test_gate_battery_contract_pins.py b/packages/microcosm-build/tests/test_gate_battery_contract_pins.py index 7997cef9..096b1740 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,59 @@ 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 + ) diff --git a/packages/microcosm-data/src/microcosm/data/contract.py b/packages/microcosm-data/src/microcosm/data/contract.py index cc04b42c..38934a1e 100644 --- a/packages/microcosm-data/src/microcosm/data/contract.py +++ b/packages/microcosm-data/src/microcosm/data/contract.py @@ -512,6 +512,120 @@ ) +# --- 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_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-"): @@ -2648,6 +2762,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], @@ -4039,6 +4356,17 @@ def validate_release_dir(release_dir: Path | str) -> None: f"got {report_schema!r}." ) + certification_path = release_dir / _UK_RELEASE_CERTIFICATION_FILE + 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 20fe6240..31380a48 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, @@ -4893,3 +4894,139 @@ 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) + ) From 7d8b3e318bc82b588aa974a448f34b63d2026c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:52:55 +0200 Subject: [PATCH 07/12] Run the U8 lever, measure it, and re-freeze at 0.55 on the receipts The pre-registered would_claim_uc raise to 0.85 was built (spine-h, 14/14 signed RC battery), calibrated, and measured against the 0.55 counterfactual on the same exclusion register: every failing UC caseload cell is unchanged (dwp.uc.households -44.9% at both rates, the capital test caps the support gain at +22%) while the raise destroys the legacy housing-benefit surface (obr.housing_benefit -0.0% at 0.55 vs -52.4% at 0.85: claimants move off legacy benefits) and perturbs the QRF predictor surface through engine-computed household_net_income. The contract entry records the run and stays frozen at 0.55; receipts live in the 757-swap acceptance evidence. The binding constraint on UC caseload is capital-test support (microcosm#750), not take-up support. Co-Authored-By: Claude Fable 5 --- .../757-would-claim-uc-lever.changed.md | 18 +++++++++++------- .../microcosm/build/uk/take_up_contract.json | 9 ++++----- .../tests/test_uk_take_up_contract.py | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/changelog.d/757-would-claim-uc-lever.changed.md b/changelog.d/757-would-claim-uc-lever.changed.md index 7f03387f..c658aa11 100644 --- a/changelog.d/757-would-claim-uc-lever.changed.md +++ b/changelog.d/757-would-claim-uc-lever.changed.md @@ -1,7 +1,11 @@ -`would_claim_uc` rises 0.55 -> 0.85 at build year 2024: the pre-registered -U8 lever (uk-data#452) for the seven UC caseload/two-child-limit targets, -re-adjudicated by the #757 `uk_target_fit` dispositions (issue comment -5427936411). The derivation is recorded in the contract entry; the -incumbent-parity 0.55 stays as dated history, and the whole-spine parity -divergence this creates on the `would_claim_uc` column is signed on the -quantitative register from the rebuilt candidate's measured extraction. +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/uk/take_up_contract.json b/packages/microcosm-build/src/microcosm/build/uk/take_up_contract.json index 8fd971ac..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 @@ -64,16 +64,15 @@ "output": "would_claim_uc", "entity": "benunit", "values": { - "2015-01-01": 0.55, - "2024-01-01": 0.85 + "2015-01-01": 0.55 }, "source": { - "source": "U8 re-adjudication (microcosm#757 uk_target_fit dispositions, issue comment 5427936411) 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": "Raised 0.55 to 0.85 as the pre-registered would_claim_uc lever for the seven UC caseload/two-child-limit targets. Derivation, measured on the rebind packet: at 0.55 the design-weight UC caseload mass is 3.13m benunits against the 6.76m administrative target, a 2.16x required lift the solve could not reach (it stalled at 1.49x, -31%); support scales with the rate, so 0.85 projects 4.84m initial mass and a 1.40x required lift, inside the solve's demonstrated envelope. 0.55 (dated 2015) was the incumbent-parity freeze and stays as history; no published UC take-up statistic exists to source either value (uk-data#452).", + "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 (re-opened by the 2026-08-26 uk_target_fit disposition)", + "decision": "U8 (re-opened and re-frozen by the 2026-08-26 lever run)", "followup": "uk-data#452" } } diff --git a/packages/microcosm-build/tests/test_uk_take_up_contract.py b/packages/microcosm-build/tests/test_uk_take_up_contract.py index 54ff47b7..76f55e59 100644 --- a/packages/microcosm-build/tests/test_uk_take_up_contract.py +++ b/packages/microcosm-build/tests/test_uk_take_up_contract.py @@ -42,7 +42,7 @@ def test_uk_contract_loads_and_selects_build_year_rates() -> None: 0.89, 0.23, 0.7, - 0.85, + 0.55, 0.5, 0.88, 0.812, From 7d88c52b709cab6a0fb4544ba8ea453008bc23b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:59:22 +0200 Subject: [PATCH 08/12] Move the acceptance receipt to spine-i, and record the blocked first cut spine-i rebuilds the candidate at the follow-up tip: payload-identical to spine-g, battery 14/14 signed at release-candidate strictness, ladder and strict parity green. The Phase-3 seam attempt on it binds 361 targets at loss 0.031 with five of six gates green and blocks, correctly, at uk_target_fit on 13 characterized cells; the certification therefore waits on the UC-family adjudication (microcosm#750 is the real lever) and the exclusion-set-dependence defect (microcosm#792). Co-Authored-By: Claude Fable 5 --- changelog.d/757-phase3-acceptance.changed.md | 12 ++++++++++++ .../build/uk/spine_candidate_acceptance.json | 18 ++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 changelog.d/757-phase3-acceptance.changed.md 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/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" } } From 58979b731bfb0b61f2174433d0983882baffa7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:40:09 +0200 Subject: [PATCH 09/12] Bring the compile-parity signed registers current before their runner ships Exercising the release-cut preflight against the pinned chronicle feed found both ledger compile-parity gates failing on register rot: 13 stale entries (live now matches the fixture) and 13 SLC entries whose kind moved when the SLC chronicle waves completed after the June fixtures froze. Regenerated from the feed with the packaged tool - zero entries added, so no unsigned difference was absorbed - and re-verified: both gates pass. The producer's first real preflight will not open on a known-stale register. Co-Authored-By: Claude Fable 5 --- ...-compile-parity-registers-current.fixed.md | 11 + ...ity_incumbent_2025_signed_differences.json | 204 ++++++------------ ...ty_production_2023_signed_differences.json | 96 +++++---- 3 files changed, 122 insertions(+), 189 deletions(-) create mode 100644 changelog.d/757-compile-parity-registers-current.fixed.md 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/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 From 36501aca43fa5aa5acab2aa3f6bdf21e75e5c99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:12:14 +0200 Subject: [PATCH 10/12] Cite the upstream issue without naming the retired package The sixteen UC payment-distribution exclusion entries tracked uk-data#452 under the retired package's full name; the live-tree guard (test_no_incumbent_data_package_references_in_live_tree) refused it on the first real CI run, exactly as designed. The sanctioned citation form is uk-data#452 - the same wording ruling #787 applied at 8ff3ff06. Co-Authored-By: Claude Fable 5 --- .../uk/calibration_measure_exclusions.json | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) 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 60975429..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 @@ -175,7 +175,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -184,7 +184,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -193,7 +193,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -202,7 +202,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -211,7 +211,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -220,7 +220,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -229,7 +229,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -238,7 +238,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -247,7 +247,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -256,7 +256,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -265,7 +265,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -274,7 +274,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -283,7 +283,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -292,7 +292,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -301,7 +301,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", @@ -310,7 +310,7 @@ { "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": "policyengine-uk-data#452", + "tracking": "uk-data#452", "approved_by": "juaristi22", "adjudication": "microcosm#757 (the uk_target_fit dispositions, issue comment 5427936411)", "approved_on": "2026-08-26", From 58829b6218ab83f5e7e1d221812d148e9aff5612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:45:37 +0200 Subject: [PATCH 11/12] Disposition the review round: close the green-by-absence class, and dedupe the scoped-battery seams All four findings confirmed in substance and fixed: national-line artifacts without a certification refuse (content-keyed until the canonical national release-id exists), the score cross-pin reads artifacts.candidate.sha256 exactly, malformed fit-weight blocks raise as corruption while empty ones still fail the audit, and the name@period parity grain is enforced loudly (it was already correct - diagnostics label rows name@period - now it is self-evident). The restructure the round invited: one scope-filtering helper with a source parameter, one graft-and-resign helper for every scoped report, public names for the shared seam helpers, digest properties on GateBatteryRun, and the dead zero-caller spine-manifest variant deleted. Co-Authored-By: Claude Fable 5 --- .../757-review-dispositions-793.fixed.md | 18 ++++ .../src/microcosm/build/gate_battery.py | 18 ++++ .../build/uk_runtime/calibration_run.py | 64 +++++++---- .../build/uk_runtime/release_certification.py | 101 +++++++++++------- .../tests/test_uk_calibration_run.py | 16 +-- .../tests/test_uk_release_certification.py | 98 +++++++++++++++-- .../src/microcosm/data/contract.py | 28 +++++ .../microcosm-data/tests/test_contract.py | 42 ++++++++ tools/build_uk_frs_spine.py | 28 +++-- 9 files changed, 334 insertions(+), 79 deletions(-) create mode 100644 changelog.d/757-review-dispositions-793.fixed.md 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/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_runtime/calibration_run.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py index 099dc620..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 @@ -504,7 +504,7 @@ def _run_calibration_gate_battery( 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( @@ -530,10 +530,12 @@ def _run_calibration_gate_battery( 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 @@ -670,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: @@ -715,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. @@ -876,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 index 1121f187..1e996488 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/release_certification.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/release_certification.py @@ -45,19 +45,14 @@ 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 - -# The certification shares the seam's scoped-manifest, admin-anchor, and -# re-signing helpers deliberately: one implementation, two producers, so the -# two reports cannot drift apart in shape. Promoting them to public names is -# a rename this increment does not need. from microcosm.build.uk_runtime.calibration_run import ( UK_CALIBRATION_GATE_SCOPE, UK_NATIONAL_GATE_SCOPE, UK_SHARED_GATE_IDS, UK_SPINE_GATE_SCOPE, - _aggregate_admin_totals, - _resign_gate_report, - _scoped_gate_manifest, + finalize_uk_scoped_gate_report, + uk_aggregate_admin_totals, + uk_scoped_gate_manifest, ) __all__ = [ @@ -119,7 +114,7 @@ def _uk_gates_spec() -> GatesManifest: def uk_national_gate_manifest() -> GatesManifest: """The release-cut battery's scoped manifest (preflight + terminal).""" - return _scoped_gate_manifest( + return uk_scoped_gate_manifest( UK_NATIONAL_GATE_SCOPE, phases=("preflight", "terminal"), policy_suffix="release_cut_scope", @@ -169,18 +164,36 @@ def rehydrate_uk_fit_weight_records( "spine sidecar fit_weight_records must map stage names to record lists." ) collected: list[FitWeightRecord] = [] - for _stage_name, records in block.items(): - if not isinstance(records, Sequence) or not records: - return () + 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): - return () + 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) @@ -201,10 +214,16 @@ def uk_release_parity_evidence( runner's ``_stage_parity_evidence`` enforced. """ - target_relative_errors = { - str(row["name"]): float(row["relative_error"]) - for row in diagnostics_targets - } + 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}" @@ -261,7 +280,7 @@ def run_uk_release_cut_battery( battery.run_phase("preflight", EvidenceContext(artifacts=preflight_artifacts)) battery.enforce("preflight", mode=BlockingMode.BLOCKS_ARTIFACT) - admin_totals, admin_receipt = _aggregate_admin_totals( + admin_totals, admin_receipt = uk_aggregate_admin_totals( frame, uk_national_gate_manifest() ) terminal_artifacts: dict[str, Any] = { @@ -280,10 +299,12 @@ def run_uk_release_cut_battery( ) battery.enforce("terminal", mode=BlockingMode.BLOCKS_ARTIFACT) payload = battery.report_payload() - payload["posture"] = UK_RELEASE_CUT_POSTURE - payload["scope_exclusions"] = uk_release_cut_scope_exclusions() - payload["aggregate_admin_measurement"] = admin_receipt - _resign_gate_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 @@ -342,9 +363,12 @@ def compose_uk_release_certification( ) part_summaries[part_name] = { "path": str( - parts_raw[part_name][2] - if len(parts_raw[part_name]) > 2 - else _part_path(part_name, spine_report_path, seam_report_path, release_cut_report_path) + _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"]), @@ -682,12 +706,18 @@ def _verify_identity_join( def _verify_score_receipt( receipt: Mapping[str, Any], *, candidate_sha256: str ) -> None: - payload = json.dumps(receipt) - if candidate_sha256 not in payload: + 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 does not name the candidate's sha256; the " - "rule-1 score must be measured on the candidate under " - "certification." + "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." ) @@ -706,7 +736,7 @@ def _scoped_digests( phases: tuple[str, ...], policy_suffix: str, ) -> dict[str, str]: - manifest = _scoped_gate_manifest( + manifest = uk_scoped_gate_manifest( scope, phases=phases, policy_suffix=policy_suffix ) return _manifest_digests(manifest) @@ -725,11 +755,10 @@ def _manifest_digests(manifest: GatesManifest) -> dict[str, str]: release_candidate=False, registry=UK_GATE_REGISTRY, ) - payload = run.report_payload() return { - "gates_manifest_sha256": str(payload["gates_manifest_sha256"]), - "policy_sha256": str(payload["policy_sha256"]), - "spec_fingerprint": str(payload["spec_fingerprint"]), + "gates_manifest_sha256": run.gates_manifest_sha256, + "policy_sha256": run.policy_sha256, + "spec_fingerprint": run.spec_fingerprint, } diff --git a/packages/microcosm-build/tests/test_uk_calibration_run.py b/packages/microcosm-build/tests/test_uk_calibration_run.py index d25bd298..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" @@ -399,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" @@ -467,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 @@ -485,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(): @@ -504,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) @@ -523,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(): @@ -601,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" @@ -647,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" diff --git a/packages/microcosm-build/tests/test_uk_release_certification.py b/packages/microcosm-build/tests/test_uk_release_certification.py index 0834b03c..20c5d7e5 100644 --- a/packages/microcosm-build/tests/test_uk_release_certification.py +++ b/packages/microcosm-build/tests/test_uk_release_certification.py @@ -25,8 +25,8 @@ UK_NATIONAL_GATE_SCOPE, UK_SHARED_GATE_IDS, UK_SPINE_GATE_SCOPE, - _resign_gate_report, - _scoped_gate_manifest, + resign_uk_gate_report, + uk_scoped_gate_manifest, ) from microcosm.build.uk_runtime.release_certification import ( UKReleaseCertificationError, @@ -77,7 +77,7 @@ def _gate(**_kwargs): def _write_part(path: Path, scope, phases, *, release_id, release_candidate, release_evidence=None, augment=None, block_phase=None): registry = _stub_registry() - manifest = _scoped_gate_manifest( + manifest = uk_scoped_gate_manifest( frozenset(scope), phases=tuple(phases), policy_suffix={ @@ -100,7 +100,7 @@ def _write_part(path: Path, scope, phases, *, release_id, release_candidate, payload = battery.report_payload() if augment: payload.update(augment) - _resign_gate_report(payload) + resign_uk_gate_report(payload) path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") return payload @@ -180,7 +180,15 @@ def green_certification_inputs(tmp_path: Path): } score_receipt = tmp_path / "score_vs_enhanced_frs.json" score_receipt.write_text( - json.dumps({"candidate": {"sha256": candidate_sha}, "verdict": "scored"}), + 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 { @@ -229,6 +237,59 @@ def test_rehydrate_fit_weight_records(): ) == () ) + # 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): @@ -348,6 +409,31 @@ def test_compose_refuses_unpinned_score_receipt(green_certification_inputs): 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"): @@ -357,7 +443,7 @@ def test_compose_refuses_absent_signing_key(green_certification_inputs, monkeypa def test_release_cut_battery_runs_and_signs(tmp_path: Path, monkeypatch): monkeypatch.setattr( release_certification, - "_aggregate_admin_totals", + "uk_aggregate_admin_totals", lambda frame, manifest: ({}, {"stub": True}), ) report_path = tmp_path / "release_cut_gates.json" diff --git a/packages/microcosm-data/src/microcosm/data/contract.py b/packages/microcosm-data/src/microcosm/data/contract.py index 38934a1e..e03f13b1 100644 --- a/packages/microcosm-data/src/microcosm/data/contract.py +++ b/packages/microcosm-data/src/microcosm/data/contract.py @@ -523,6 +523,7 @@ # 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" _UK_RELEASE_CERTIFICATION_SCHEMA_VERSION = 1 _UK_RELEASE_CERTIFICATION_KIND = "uk_release_certification" _UK_CERTIFICATION_SHARED_GATE_IDS = frozenset({"uk_aggregate_admin"}) @@ -4357,6 +4358,33 @@ def validate_release_dir(release_dir: Path | str) -> None: ) 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: diff --git a/packages/microcosm-data/tests/test_contract.py b/packages/microcosm-data/tests/test_contract.py index 31380a48..7b2c895c 100644 --- a/packages/microcosm-data/tests/test_contract.py +++ b/packages/microcosm-data/tests/test_contract.py @@ -5030,3 +5030,45 @@ def test_uk_release_certification_refusals(monkeypatch) -> None: "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 + ) diff --git a/tools/build_uk_frs_spine.py b/tools/build_uk_frs_spine.py index 64ad73dc..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, @@ -775,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, ) From cebc9f5033fd040e2401ba740b03b06464cfb205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:25:37 +0200 Subject: [PATCH 12/12] Name the certified national line: one constant id, and the certification required for it microcosm-uk-2024-25-national (ruling 2026-08-27): the id stays fixed across cuts because the Logbook and versioning carry run identity; the vintage segment follows uk-data naming and -national stays disjoint from the exact-k -k shape. The seam refuses the shippable name, the contract requires the certification for it (finding 1's id-keyed layer), and a lockstep test holds the data-shard mirror to the build-shard constant. Co-Authored-By: Claude Fable 5 --- changelog.d/757-national-release-id.added.md | 10 ++++++++++ .../microcosm/build/uk_runtime/release_identity.py | 9 +++++++++ .../tests/test_gate_battery_contract_pins.py | 7 +++++++ .../tests/test_uk_calibration_seam_driver.py | 11 +++++++++++ .../microcosm-data/src/microcosm/data/contract.py | 12 ++++++++++++ packages/microcosm-data/tests/test_contract.py | 10 ++++++++++ tools/calibrate_uk_national_dataset.py | 7 +++++-- 7 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 changelog.d/757-national-release-id.added.md 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/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_gate_battery_contract_pins.py b/packages/microcosm-build/tests/test_gate_battery_contract_pins.py index 096b1740..bc1d37e7 100644 --- a/packages/microcosm-build/tests/test_gate_battery_contract_pins.py +++ b/packages/microcosm-build/tests/test_gate_battery_contract_pins.py @@ -367,3 +367,10 @@ def test_certification_identity_mirrors(self) -> None: 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_seam_driver.py b/packages/microcosm-build/tests/test_uk_calibration_seam_driver.py index 70009ba4..8ed5850e 100644 --- a/packages/microcosm-build/tests/test_uk_calibration_seam_driver.py +++ b/packages/microcosm-build/tests/test_uk_calibration_seam_driver.py @@ -192,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-data/src/microcosm/data/contract.py b/packages/microcosm-data/src/microcosm/data/contract.py index e03f13b1..c423461c 100644 --- a/packages/microcosm-data/src/microcosm/data/contract.py +++ b/packages/microcosm-data/src/microcosm/data/contract.py @@ -524,6 +524,12 @@ # 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"}) @@ -633,6 +639,12 @@ def required_release_files(release_id: str) -> tuple[str, ...]: 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 diff --git a/packages/microcosm-data/tests/test_contract.py b/packages/microcosm-data/tests/test_contract.py index 7b2c895c..4f4910eb 100644 --- a/packages/microcosm-data/tests/test_contract.py +++ b/packages/microcosm-data/tests/test_contract.py @@ -5072,3 +5072,13 @@ def test_national_line_artifacts_require_the_certification(tmp_path) -> None: "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/calibrate_uk_national_dataset.py b/tools/calibrate_uk_national_dataset.py index e019b413..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}") @@ -160,8 +161,10 @@ def _parse_args(argv: list[str] | None) -> argparse.Namespace: "seam's scoped battery cannot sign shippability; run the " "release-cut certification producer instead" ) - if _CANONICAL_UK_RELEASE_ID.fullmatch(args.release_id) or ( - args.release_id == _UK_JUNE_RELEASE_ID + if ( + _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 belong to the release-cut "