Skip to content

Assemble the certified UK national candidate into a publishable release (#806) - #812

Open
juaristi22 wants to merge 4 commits into
mainfrom
uk-release-assembler-806
Open

Assemble the certified UK national candidate into a publishable release (#806)#812
juaristi22 wants to merge 4 commits into
mainfrom
uk-release-assembler-806

Conversation

@juaristi22

Copy link
Copy Markdown
Collaborator

Closes the packaging gap in #806's plumbing: the certification decided shippability, but nothing produced the release directory, the constant national id escaped the contract's UK-keyed checks, and the id ruling collided with the tag-equals-release-id revision scheme.

What this adds

  • tools/assemble_uk_release_dir.py — verifies the certification's closed identity join against measured bytes before writing anything (candidate, diagnostics, all three part reports, score receipt, spine H5), mints the calibration NPZ (household_weight + initial_household_weight, the warm-start shape), writes build_manifest.json + release_manifest.json with uniform per-cut artifact revisions, copies signed evidence byte-for-byte, runs validate_release_dir on its own output, and prints the exact inspect-lane publish command.
  • Contract coverage for microcosm-uk-2024-25-national — the uk_diagnostics block validation and the policyengine-uk model pin now apply to the national id (the June legacy release keeps its exclusion); artifact revisions may be the release id or a release-id-prefixed per-cut tag.
  • Publish tag guards generalized — any pinned revisions require tag creation, and every declared revision must equal the tag being created, so a forgotten --tag-name or a typo refuses instead of publishing dangling revision pins.
  • Shared green-certification fixtures (uk_certification_fixtures.py) used by the certification and assembler suites; runbook docs/uk-national-release-assembly-runbook-806.md for the calibrate → certify → assemble → inspect-publish sequence.

Scope

Inspect lane only (--no-latest): promotion needs the loader/pointer extension (the certified loader fetches the manifest at a tag named exactly the release id and asserts revision == release_id) — follow-up issue to come.

Verification

  • Targeted suites: 301 passed (test_contract.py, test_release.py, test_uk_release_assembler.py, test_uk_release_certification.py, test_gate_battery_contract_pins.py)
  • Full UK lane: 1290 passed, 39 skipped (engine-gated), 0 failed
  • ruff check . clean; tools/ci_test_groups.py --verify ok (new test routed to spine-uk + uk)

🤖 Generated with Claude Code

@juaristi22
juaristi22 force-pushed the uk-release-assembler-806 branch from 72836c8 to aac61f6 Compare August 28, 2026 14:47
@juaristi22
juaristi22 marked this pull request as ready for review August 28, 2026 16:48
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Automated review pass (Claude Code, high effort, diff only — no execution). Draft, so nits are skipped. Four findings, and three of them are the publication gate failing open rather than closed — which matters more here than anywhere else in the lane, since this is the step where an artifact stops being a candidate and starts being something people use.

1. packages/microcosm-data/src/microcosm/data/contract.py:4325 — the #796 rule the national id was meant to unblock is still not closed

The national id is added only to the diagnostics branch inside the diagnostics-present block. Nothing in this PR adds an id-keyed required-files rule for microcosm-uk-2024-25-national, so a national release directory that simply omits calibration_diagnostics.json or release_certification.json still validates clean.

This is the deferral from #793: the content-keyed refusal there was explicitly an interim, with the id-keyed rule waiting on a canonical national release-id form. That form now exists and is used in this very file, so the blocker is gone — but the rule did not land with it. The assembler always writing those files is not the same guarantee: validate_release_dir is the thing that has to refuse when they are absent, precisely because it is what runs against a directory nobody watched being built.

2. packages/microcosm-data/src/microcosm/data/release.py:327-336 — an empty revision set satisfies both new guards

Both guards are if artifact_revisions and ..., so an empty set skips the create-tag requirement and the uniformity check entirely. That is reachable two ways: a manifest with no artifacts at all, and — more likely — a manifest whose artifacts carry a missing or non-string revision, since _release_manifest_artifact_revisions drops those before the guards ever see them.

So a manifest can vacuously satisfy both checks by declaring no revisions, and the malformed case degrades to the same silence as the empty one. Refusing an empty revision set outright, and distinguishing "no artifacts declared" from "artifacts whose revisions could not be read", would make both cases loud.

3. packages/microcosm-data/src/microcosm/data/contract.py:1091-1097 — the revision suffix is unvalidated

For the national id, any revision matching microcosm-uk-2024-25-national-<anything non-empty> passes. The suffix is never checked against the attempt-derived cut-tag form (<YYYYMMDDTHHMMSSZ>-<uuid8>) that _cut_tag enforces on the writing side.

The two sides therefore disagree about what a revision means: the assembler mints a structured, attempt-derived tag, and the contract accepts any non-empty string behind the prefix. A hand-edited or stale manifest revision validates cleanly, so the contract no longer pins the manifest to the cut it claims to describe — which is the property the revision exists to carry.

4. tools/assemble_uk_release_dir.py:~250 — the two weight vectors are paired without an alignment check

The np.savez of household_weight / initial_household_weight reads the candidate and spine household weight vectors from two independently loaded frames, with no length check and no household-id alignment before writing them into the calibration NPZ.

This is the #468 class arriving in a shipped file. The spine carries 10,000 synthetic zero-weight households and clones every household at mass 0.5, so the household axis is exactly the kind of thing that can differ between two artifacts for structural reasons rather than corruption. If it does, the baseline vector is silently mis-paired — and because the NPZ's own sha256 is then recorded as authoritative, the mispairing ships signed. A digest over mis-paired bytes authenticates the bytes, not the pairing.

Asserting equal length and identical household ids before the write is cheap, and it is the one check that makes the recorded digest mean what a downstream reader will assume it means.


1 and 4 are the ones I would want closed before this leaves draft. 1 because the deferral's precondition is now satisfied and this is the natural place for it; 4 because it is the only finding here that can corrupt a published artifact rather than merely fail to catch a bad one.

juaristi22 added a commit that referenced this pull request Aug 28, 2026
…ring

Vahid's #812 review, findings 2-4 (finding 1 is answered on the PR: the
id-keyed required-files rule landed in #793 and refuses both omissions by
name).

A present-but-non-string artifact revision now fails the contract instead
of sliding past the isinstance guard, and the publish guards no longer go
vacuous on an empty revision set: unreadable pins refuse naming the
artifacts, an empty set refuses outright, and the create-tag and
uniformity requirements apply unconditionally.

The national per-cut revision suffix is validated against the same
<YYYYMMDDTHHMMSSZ>-<uuid8> grammar the assembler mints, on both sides:
the contract rejects hand-edited suffixes and the --cut-tag override may
re-tag a cut but never leave the grammar.

The assembler refuses to write the calibration NPZ when the candidate and
spine household axes are misaligned - equal ids in equal order, checked
before the pairing is digested. A digest over mis-paired bytes
authenticates the bytes, not the pairing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Review round addressed in ba2d73d. Dispositions:

1. Id-keyed required-files rule — no change needed; the rule already exists. required_release_files("microcosm-uk-2024-25-national") returns the four files including release_certification.json (the id-keyed branch landed with the id itself in #793, contract.py's _UK_NATIONAL_RELEASE_ID arm), and validate_release_dir enforces presence for every entry (required file 'X' is missing.). Verified by execution: a national directory carrying only the two manifests refuses with required file 'calibration_diagnostics.json' is missing. and required file 'release_certification.json' is missing. — plus test_national_release_id_requires_the_certification pins it. The diagnostics-branch addition in this PR is a separate check (the uk_diagnostics content validation), not the presence rule.

2. Empty/unreadable revision sets — fixed, both layers. The contract now fails a present-but-non-string revision instead of letting it slide past the isinstance guard (where it would vanish from publish's string-only collection). _release_manifest_artifact_revisions returns unreadable artifact keys separately; publish refuses unreadable pins by name, refuses an empty revision set outright, and the create-tag and uniformity guards now apply unconditionally. An empty set can only mean "no artifacts declared" — never "pins that could not be read".

3. Revision suffix grammar — fixed, both sides. The contract validates the national suffix against the same <YYYYMMDDTHHMMSSZ>-<uuid8> shape the assembler mints (_UK_NATIONAL_REVISION_SUFFIX_RE, mirrored comment pointing at the producer), and the assembler's --cut-tag override may re-tag a cut but never leave the grammar. Hand-edited suffixes (-hotfix, wrong case, uppercase uuid) now refuse in both places.

4. NPZ pairing — fixed. The assembler asserts equal household counts and identical household_id arrays (values and order) across the candidate and spine frames before np.savez, refusing with the row counts or "household ids differ" named. Two new tests cover the length and id-mismatch cases through the real driver with re-pinned build records.

Verification: assembler suite 9/9, contract + release suites green (incl. 4 new revision cases and 2 new publish refusals), certification/pins/seam-driver suites green, ruff clean.

juaristi22 and others added 4 commits August 28, 2026 19:21
…se (#806)

The release-cut certification decided shippability but nothing packaged it:
no UK tool wrote the two release manifests, the constant national id escaped
the contract's UK-keyed checks, and the id ruling collided with the
tag-equals-release-id revision scheme. This closes all three for the
inspect lane (publication stays --no-latest until the loader/pointer
follow-up).

tools/assemble_uk_release_dir.py verifies the certification's closed
identity join against the measured bytes before writing anything, mints the
calibration weight NPZ (household_weight + initial_household_weight, the
warm-start shape), writes build_manifest.json and release_manifest.json
with uniform per-cut artifact revisions, copies the signed evidence
byte-for-byte, validates its own output, and prints the exact publish
command.

The contract now applies the UK diagnostics block and the policyengine-uk
model pin to microcosm-uk-2024-25-national (the June legacy release keeps
its exclusion) and accepts that id's per-cut artifact revisions
(release-id-prefixed, non-empty suffix). publish_release's tag guards
generalize from release-id-pinned revisions to any pinned revisions: a tag
must be created and every declared revision must equal the tag being
created, so a forgotten --tag-name or a typo refuses instead of publishing
dangling revision pins.

The green-certification test machinery moves to a shared fixture module
used by the certification and assembler suites. The runbook documents the
calibrate -> certify -> assemble -> inspect-publish sequence, the stranded
release-staging branch recovery, and why promotion waits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wheels lane runs the suite from installed wheels in a clean venv
without pytables, and the assembler fixture writes UK national frames
through pandas HDF. Guard the fixture with the same importorskip the
UK national-frame tests use, so the wheels battery skips instead of
erroring six times.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ring

Vahid's #812 review, findings 2-4 (finding 1 is answered on the PR: the
id-keyed required-files rule landed in #793 and refuses both omissions by
name).

A present-but-non-string artifact revision now fails the contract instead
of sliding past the isinstance guard, and the publish guards no longer go
vacuous on an empty revision set: unreadable pins refuse naming the
artifacts, an empty set refuses outright, and the create-tag and
uniformity requirements apply unconditionally.

The national per-cut revision suffix is validated against the same
<YYYYMMDDTHHMMSSZ>-<uuid8> grammar the assembler mints, on both sides:
the contract rejects hand-edited suffixes and the --cut-tag override may
re-tag a cut but never leave the grammar.

The assembler refuses to write the calibration NPZ when the candidate and
spine household axes are misaligned - equal ids in equal order, checked
before the pairing is digested. A digest over mis-paired bytes
authenticates the bytes, not the pairing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebasing the fixture extraction over #795 took our side of
test_uk_release_certification.py wholesale, which lost main's one
semantic change among the rewraps: run_uk_release_cut_battery grew a
required local_ledger_registries argument with the local-contracts join.
Restore it at the only call site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@juaristi22
juaristi22 force-pushed the uk-release-assembler-806 branch from ba2d73d to 82fabe9 Compare August 28, 2026 17:38
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Program Review

Source Documents

  • PDF: N/A — this is an infrastructure/data-release packaging PR with no policy source documents.
  • Year: 2024–25
  • Scope: PR changes only
  • Reviewed head SHA: 82fabe9
  • Mode: full
  • CI: 23 GitHub checks passed.
  • Targeted verification: 271 tests passed for the changed data-shard contract/release files; a separate focused national-revision/publisher selection passed 15 tests. The wheel built, installed in a clean temporary environment, and microcosm-publish-release --help passed. A second validator could not repeat the suite because its isolated environment lacked dependencies and network access.

Branch Status

The PR head is 4 commits ahead of and 13 commits behind current main. A rebase is recommended, but the review used the merge-base-to-head diff, so staleness did not affect these findings.

Critical (Must Fix)

  1. [C1] A per-cut inspect release can still promote latest.json. publish_release proves only that all artifact revisions equal the selected tag; it does not require update_latest=False when that tag differs from the constant release id. Calling the public API with a valid per-cut tag_name and its default update_latest=True, or omitting only --no-latest from the CLI, passes the guard, creates the cut tag, and commits latest.json. The assembler's printed command is correctly inspect-only, but that spelling is not a fail-closed publisher invariant. The runbook states that the current pointer/loader design cannot promote this line because the pointer names only the constant release id. Refuse latest updates whenever the uniform artifact revision differs from release_id, before any remote mutation, and add API plus installed-CLI negative tests proving that the fake Hub receives no branch, commit, tag, or pointer operation. — packages/microcosm-data/src/microcosm/data/release.py:325-354,547-567; CLI default at packages/microcosm-data/src/microcosm/data/publish_cli.py:186-195.

  2. [C2] The assembler derives certified release identity from a mutable, unsigned build record. The certification authenticates the diagnostics bytes, whose build object records the original build id, input posture, and spine provenance. Assembly authenticates only diagnostics.build.code_pin; it instead trusts the separately supplied build_record.json for the spine digest, attempt id/cut tag, gate summary, and PolicyEngine UK version, without binding the build-record bytes or cross-checking those fields against the signed diagnostics. Reproduction: with an otherwise valid certified candidate, change build_record.build_id to another grammar-valid attempt id and assembly mints a tag claiming the new cut. A stronger variant replaces spine.h5 with a valid same-household-id Frame containing different design weights and updates only build_record.input_posture.sha256; the identity and row-alignment checks pass and the shipped NPZ records the substituted initial_household_weight. Bind the build-record digest into certification or require exact equality for every release-relevant build-record field against authenticated diagnostics/part evidence, and derive the cut tag only from the authenticated attempt id. — tools/assemble_uk_release_dir.py:95-164,177-210.

  3. [C3] Signed evidence can be removed or rewritten while the release still validates and publishes. The certification signs the part and score-receipt digests, but the release validator checks only that those signed digest fields look like SHA-256 values; it never compares them with the local spine_gates.json, terminal_gates.json, release_cut_gates.json, or score_vs_enhanced_frs.json. Local hashing is against the unsigned release manifest and silently skips absent files. Reproduction: after a green assembly, replace release_cut_gates.json or the score receipt and update only its digest in release_manifest.json; validate_release_dir accepts evidence different from the signed evidence. Alternatively, delete terminal_gates.json; validation succeeds and publication reclassifies the bare path as a root artifact based on file existence. Require every declared release-scoped evidence file to exist, bind each local digest to the corresponding certification part/receipt digest, and represent artifact scope explicitly rather than infer it from local existence. Add mutation and deletion tests that must fail before remote operations. — packages/microcosm-data/src/microcosm/data/contract.py:1461-1487,2873-2911,4436-4452; packages/microcosm-data/src/microcosm/data/release.py:578-627.

  4. [C4] Runtime compatibility pins can describe an invented assembly environment instead of the certified build environment. The assembler takes most versions from the packaging environment, takes PolicyEngine UK from the unsigned build record, and permits every value to be replaced by --runtime-version; it writes the chosen values consistently to both manifests, so contract validation cannot detect a false pin. Reproduction: change a valid fixture's recorded PolicyEngine UK provenance from 2.89.0 to an override of 9.99.0; _runtime_versions returns 9.99.0, and after changing the build and compatibility manifest fields together, validate_release_dir succeeds. This can admit an incompatible model version or reject the version that actually produced the artifact. Capture package/runtime versions during calibration, bind them into signed diagnostics/certification, and allow overrides only when they match authenticated provenance. — tools/assemble_uk_release_dir.py:217-250,285-317,471-505.

Should Address

  1. [A1] Assembly writes destructively before final validation. The calibration NPZ is written beside the candidate, the constant-id output directory is reused, manifests and evidence are overwritten in place, and only then is validate_release_dir called. A late bad-signature or manifest failure leaves a plausible partial release and can corrupt a previous assembly; existing destination symlinks are followed by np.savez and shutil.copyfile. Validate immutable inputs first, assemble into a fresh private staging directory using regular files, validate there, and atomically rename into an empty destination. Add late-failure, pre-existing-output, and symlink tests. — tools/assemble_uk_release_dir.py:207-216,252-253,372-384.

  2. [A2] The generated operator command is not shell-safe. release_dir and the artifact-root path are interpolated without quoting even though the runbook tells operators to execute the emitted command. Spaces break the command and shell metacharacters can change it. Render an argv list with shlex.join or emit a JSON argv array, and test paths containing spaces and metacharacters. — tools/assemble_uk_release_dir.py:385-389.

  3. [A3] Release-directory validation accepts mixed valid national cut revisions. Each artifact revision is checked independently against the grammar, but the contract does not require one uniform value. A manifest containing two different grammar-valid cut revisions passes validate_release_dir; publish_release rejects it later, so remote publication currently fails closed. Move the uniformity invariant into the release contract and add mixed release-id/per-cut plus two-per-cut cases. — packages/microcosm-data/src/microcosm/data/contract.py:1093-1130.

Suggestions

None.

PDF Audit Summary

Category Count
Confirmed correct N/A
Mismatches (code-path confirmed + visually verified) N/A
Mismatches rejected (code-path cleared) N/A
Unmodeled items N/A
Pre-existing issues 0 included

No PDF audit applied to this release-tooling PR.

Validation Summary

Check Result
Regulatory Accuracy N/A — no policy formulas or parameters changed
Reference Quality N/A — no source references changed
Code Patterns / Release Integrity 4 critical, 3 should-address findings
Formatting (params & vars) N/A
Test Coverage Existing suites pass; adversarial gaps are attached to C1-C4 and A1-A3
PDF Value Audit N/A
CI Status Passing — 23/23 checks
Local Test Evidence 271 passed; separate focused 15 passed; wheel/install/CLI smoke passed

Review Severity: REQUEST_CHANGES

Next Steps

Close the four fail-open release-integrity paths and add the cited negative tests before merge. Then rerun this review against the updated head. To auto-fix issues, run the fix-pr workflow for this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants