Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,10 @@ 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 version 1, which the ARCH-1 plan adds, asks
of every release.

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, the releases in service on 2026-10-15, 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 today and as the plan's version 1 would, with how far each milestone had got on 2026-11-03. It says how C1 version 1 keys by declared country instead of release-id strings, records where the recertification lane and C2 version 1 disagree, carries microcosm#378's parity rule, and replaces the branch-only `us-uk-release-path.md`. It changes nothing that publishes.

`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. Version 1 of the release contract (`docs/release-contract.md`, C1, planned for M3) derives a new release's id from its declared identity instead. The UK line ids and the tier vocabulary stay, and the release contract still validates existing exact-k ids.
6 changes: 6 additions & 0 deletions docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ HF_STAGING_READ_TOKEN= bash tools/run_integration_tests.sh

## The PR-CI / certification boundary

[The release contract](release-contract.md) (C1 to C5) states what the
publisher enforces today and what version 1, which the ARCH-1 plan adds, asks
of every country's releases. 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
655 changes: 655 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 @@ -791,7 +788,6 @@
"UKRowwiseDatasetResult",
"UKReleaseInputColumn",
"UKReleaseInputCoverageManifest",
"UKReleaseIdentity",
"UKEffectiveMassCoveragePolicy",
"UKSPISupportResult",
"UKSPIIncomeImputationResult",
Expand All @@ -809,7 +805,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 @@ -863,7 +858,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,24 @@
"""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.

microcosm#1285 retired the minting helpers for the exact-cardinality
``populace-uk-<year>-<tier>-k<N>`` grammar, which had no caller
(docs/release-contract.md, retire list). Version 1 of the release contract,
planned for M3, derives a new release's 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 +60,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