Skip to content
Open
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ including Hypothesis properties) run in the normal `uv run pytest` suite.

## Releasing & alerts

[The release contract](docs/release-contract.md) (C1 to C5) says what the
publisher enforces today and what every release must meet from 2026-11-06.

The [native SPM role source-enrichment lane](docs/us-native-spm-role-source-enrichment.md)
creates a new US H5 from the exact reviewed BuildP parent, preserves its original
variables and schema-5 calibration evidence, and requires fresh country/wrapper
Expand Down
3 changes: 3 additions & 0 deletions changelog.d/1285-release-contract.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
`docs/release-contract.md` puts the release contract on paper (ARCH-1, cos d1160): the end state, what is served today, and contracts C1 to C5 (release manifest and certification, loader compatibility, staging and run outcome, the Logbook, publish and registry), each as the code enforces it now and as it will from 2026-11-06. It says how C1 keys by declared country instead of release-id strings, carries microcosm#378's parity rule, and replaces the branch-only `us-uk-release-path.md`. Nothing refuses yet.

`tools/generate_release_contract_fixtures.py` writes version 1 of the contract's golden fixtures under `packages/microcosm-data/tests/fixtures/release_contract/v1`, following the staging-fixture pattern: the UK national line's manifests and an inventory of what the contract requires of that line (C1), the `latest.json` and `latest-national.json` payloads and a registry snapshot (C5), with `SHA256SUMS` and `--check`. The UK national assembler's manifest construction moves into `uk_national_release_manifests`, a pure function the generator also calls; its output is unchanged.
1 change: 1 addition & 0 deletions changelog.d/1285-uk-exact-k-id-minting.removed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Retire `format_uk_release_id`, `UKReleaseIdentity` and `apply_uk_release_identity` from `microcosm.build.uk_runtime`: they minted `populace-uk-<year>-<tier>-k<N>` ids and had no caller. New releases take their id from the release contract's declared identity (`docs/release-contract.md`, C1). The UK line ids and the tier vocabulary stay, and the release contract still validates existing exact-k ids.
5 changes: 5 additions & 0 deletions docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ HF_STAGING_READ_TOKEN= bash tools/run_integration_tests.sh

## The PR-CI / certification boundary

[The release contract](release-contract.md) (C1 to C5) is what every
country's releases must meet. Change it, its golden fixtures
(`tools/generate_release_contract_fixtures.py`) and the code that enforces it in
the same pull request.

PR CI never receives external writer credentials or touches restricted
microdata. Same-repository runs may receive the optional read-only
`HF_STAGING_READ_TOKEN` in the `integration-uk` job; fork pull requests receive
Expand Down
558 changes: 558 additions & 0 deletions docs/release-contract.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -427,9 +427,6 @@
UK_RELEASE_TIER_CPS_TRANSFER,
UK_RELEASE_TIER_FRS,
UK_RELEASE_TIERS,
UKReleaseIdentity,
apply_uk_release_identity,
format_uk_release_id,
validate_uk_release_tier,
)
from microcosm.build.uk_runtime.release_input_coverage import (
Expand Down Expand Up @@ -793,7 +790,6 @@
"UKRowwiseDatasetResult",
"UKReleaseInputColumn",
"UKReleaseInputCoverageManifest",
"UKReleaseIdentity",
"UKEffectiveMassCoveragePolicy",
"UKSPISupportResult",
"UKSPIIncomeImputationResult",
Expand All @@ -811,7 +807,6 @@
"VAT_LIABILITY_BANDS",
"VINTAGES",
"align_area_targets",
"apply_uk_release_identity",
"apply_uk_source_lineage_modulus",
"area_groups_from_codes",
"area_support_tables",
Expand Down Expand Up @@ -865,7 +860,6 @@
"fill_support_channel_from_source",
"fit_tables",
"footprint",
"format_uk_release_id",
"frozen_vs_recomputed",
"impute_uk_spi_income_support",
"replace_uk_spi_support_tables",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
"""Canonical source-tier and exact-cardinality identity for UK releases.
"""Canonical UK release ids and the closed source-tier vocabulary.

The UK has one extra release-id axis that the US does not: the licence class
of the household spine. It is provenance, not a quality label. Keeping the
closed vocabulary and formatter together prevents a manifest writer from
minting an unratified tier or an id that disagrees with its manifest fields.
of the household spine. It is provenance, not a quality label. The national
and dense lines keep one constant id each across cuts.

The minting helpers for the exact-cardinality ``populace-uk-<year>-<tier>-k<N>``
grammar were retired with no caller left (docs/release-contract.md, retire
list): new releases take their id from C1's declared identity,
``microcosm-<country>-<vintage>-<line>``.
"""

from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass

__all__ = [
"UK_DENSE_RELEASE_ID",
"UK_NATIONAL_RELEASE_ID",
"UK_RELEASE_TIERS",
"UK_RELEASE_TIER_CPS_TRANSFER",
"UK_RELEASE_TIER_FRS",
"UKReleaseIdentity",
"apply_uk_release_identity",
"format_uk_release_id",
"validate_uk_release_tier",
]

Expand Down Expand Up @@ -61,113 +59,3 @@ def validate_uk_release_tier(tier: object) -> str:
"adjectives such as 'true', 'full', and 'public' are invalid."
)
return tier


def _validate_positive_int(value: object, *, field: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
raise ValueError(
f"UK release {field} must be a positive integer, got {value!r}."
)
return value


def format_uk_release_id(
year: object,
tier: object,
record_count: object,
) -> str:
"""Format ``populace-uk-<year>-<tier>-k<N>`` after strict validation."""

valid_year = _validate_positive_int(year, field="year")
valid_tier = validate_uk_release_tier(tier)
valid_record_count = _validate_positive_int(record_count, field="record_count")
return f"populace-uk-{valid_year}-{valid_tier}-k{valid_record_count}"


@dataclass(frozen=True)
class UKReleaseIdentity:
"""The identity slice every future UK release manifest must record.

:meth:`as_release_manifest_fields` deliberately returns the build id next
to its component fields. A manifest assembler consumes this validated
slice instead of accepting a free-form tier or a separately formatted id.
"""

year: int
tier: str
record_count: int

def __post_init__(self) -> None:
_validate_positive_int(self.year, field="year")
validate_uk_release_tier(self.tier)
_validate_positive_int(self.record_count, field="record_count")

@property
def release_id(self) -> str:
"""Return the canonical exact-cardinality release id."""

return format_uk_release_id(self.year, self.tier, self.record_count)

def as_release_manifest_fields(self) -> dict[str, object]:
"""Return identity fields, including the shared ``n_records`` seam."""

return {
"country": "uk",
"year": self.year,
"tier": self.tier,
"record_count": self.record_count,
"n_records": self.record_count,
"build": {"build_id": self.release_id},
}


def apply_uk_release_identity(
manifest: Mapping[str, object],
identity: UKReleaseIdentity,
) -> dict[str, object]:
"""Apply a validated UK identity without overwriting conflicting fields.

Release manifests carry ``tier`` at the top level and ``build_id`` inside
the existing ``build`` object. The helper makes that schema seam
explicit, preserves all other build metadata, and refuses to relabel a
manifest whose identity was already assembled differently.
"""

if not isinstance(manifest, Mapping):
raise TypeError("UK release manifest must be a mapping.")
if not isinstance(identity, UKReleaseIdentity):
raise TypeError("identity must be a UKReleaseIdentity.")

expected_fields: dict[str, object] = {
"country": "uk",
"year": identity.year,
"tier": identity.tier,
"record_count": identity.record_count,
"n_records": identity.record_count,
}
for field, expected in expected_fields.items():
if field in manifest and manifest[field] != expected:
raise ValueError(
f"UK release manifest {field!r} is {manifest[field]!r}, "
f"expected {expected!r}."
)

raw_build = manifest.get("build")
if raw_build is None:
build: dict[str, object] = {}
elif isinstance(raw_build, Mapping):
build = dict(raw_build)
else:
raise ValueError("UK release manifest 'build' must be an object.")
if "build_id" in build and build["build_id"] != identity.release_id:
raise ValueError(
"UK release manifest 'build.build_id' is "
f"{build['build_id']!r}, expected {identity.release_id!r}."
)
build["build_id"] = identity.release_id

return {
**dict(manifest),
**expected_fields,
"build": build,
}
Loading
Loading