Skip to content

Certify Microcosm builds in their own release folders, and tagged builds only where a repository has no releases.json - #683

Merged
anth-volk merged 18 commits into
mainfrom
release-build-folders
Nov 5, 2026
Merged

anth-volk merged 18 commits into
mainfrom
release-build-folders

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #682

Base: main. main was merged into this branch twice, with merge commits (no rebase): 36c3d8d3 (with #615's final code) and ae059ebd (84 commits of main, on 2026-11-04).

Why

Microcosm is moving each dataset repository to a layout where:

  • each build is published once in its own folder, releases/<release>/<build>/, and never changed;
  • a root releases.json (microcosm-emitter#24, microcosm_emitter_interface.releases) records every published build of each release, which one is current, and which are withdrawn;
  • no new Hugging Face tags are made.

Until a repository switches, it keeps publishing tagged builds with latest*.json pointer files. policyengine.py has to certify both, and never mix them in one repository.

What changes

One layout per repository (certification)

  • The read: certification reads the repository's releases.json at main (_releases_record_at_main). Only Hugging Face's 404 for the file means the repository has none, unless its error code (X-Error-Code) is RepoNotFound or RevisionNotFound. Any other answer refuses certification, so a failed read is never taken for a tagged repository.
  • A repository with releases.json: only builds in their own folders are certified. A tagged manifest, passed as --manifest-uri or --regional-manifest-uri, is refused (require_tagged_layout), and the error says to certify releases/<release>/<build>/release_manifest.json instead.
  • A repository without it: tagged builds are certified exactly as before, and a build-folder manifest is refused because no record lists it.
  • Where the rule applies: in certify_data_release. The input-surface measuring tool (regional_input_surface), which only reads a manifest, is unchanged.

Reading a build-folder manifest (provenance/manifest.py)

  • Identity: DataReleaseManifest.release is new. A manifest with string release and build is read with build as DataBuildInfo(build_id=<build>). Both names must match the publisher's rule, ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$. A string build without release, or release with an object build, is refused.
  • Build details: the build's details are at the manifest's top level, where microcosm#1505's release writer puts them (decided 2026-10-29): built_at, built_with_model_package (version, git SHA, data build fingerprint) and built_with_core_package. They are read into build, so the built_with_model_package basis, the data build fingerprint and the TRO's start time apply as for tagged builds. A dump writes build back as the string and the details back at the top level, so merges round-trip it.
  • release_build_id is unchanged. Such a build has no cut_tag, so it returns the build name, and two builds of one release never match.
  • build_folder returns releases/<release>/<build>, or None for older manifests.
  • DataReleaseArtifact.revision may be absent only in a build-folder manifest.

Certifying a build in its own folder (provenance/certification.py, through the shared read_build_folder)

  • Where it is read from: the manifest URI must be the build's own folder, releases/<release>/<build>/release_manifest.json.
  • The record at main: releases.json at main must list the build and must not mark it withdrawn, and the whole record must be one the release writer's parser accepts. policyengine.py repeats ReleasesRecord.parse and check_record without depending on the interface package: exact keys, schema_version 1, safe names and roles, published_at and withdrawn types, default and current, and the 4 MiB limit. The same applies to a local-area release passed with --regional-manifest-uri.
  • The build's own files (its artifacts in the manifest's repository) are pinned at releases/<release>/<build>/<file> and at the URI's revision. Each path must be a bare file name, alone or after the folder; a nested path, . or .., or a backslash is refused. Artifacts from other repositories keep their own paths and pins.
  • Names: the build's name becomes data_releases.uk.version, the certificate's data_build_id and the changelog fragment name.
  • Withdrawn builds: unchanged by this round. Certification refuses a build the record marks withdrawn. Whether a loader or an installed bundle should refuse such a build, or load it with a warning, waits on a separate decision.

At import

  • get_data_release_manifest checks a build-folder manifest against the bundle's pinned release_manifest_path. A manifest naming another release or build raises ReleaseBuildFolderError, a ValueError that is deliberately not a DataReleaseManifestUnavailableError, so the offline fallback to the bundled certificate never covers it.
  • It then pins the build's files the same way certification does.
  • Since the merge of main, this covers both the fetched manifest and main's bundle-pinned local manifest file (POLICYENGINE_<COUNTRY>_DATA_MANIFEST_FILE). Both go through _bind_to_bundle and then main's _validate_bundle_artifacts.
  • Import doesn't read releases.json.

Docs and changelog: docs/engineering/skills/data-certification.md, section "Builds in their own release folders"; changelog.d/682.added.md.

The merge of main (ae059ebd)

There was one conflict, in provenance/manifest.py. main had moved the remote fetch into _fetch_data_release_manifest, added the local manifest file, and checked every loaded manifest against the bundle (_apply_release_revision, then _validate_bundle_artifacts). The fetched manifest now goes through _bind_to_bundle: read_build_folder for a build folder, _apply_release_revision otherwise, then the bundle check. The local manifest file got the same treatment in a red-green pair (ba7bd592 / 87bfd56f).

Environment variables

No variable is added, and no workflow or deployment file changes. The new releases.json read for tagged manifests uses the same token as the manifest read: certify_data_release's token argument, which scripts/bundle.py certify-data leaves unset, as before. CI's HUGGING_FACE_TOKEN secret for bundle verification is unchanged. The scheduled UK certification workflow reads policyengine/policyengine-uk-data-private through uk_qualification and doesn't call certify_data_release, so it is unaffected.

Tests

Only the tests this PR adds or changes were run locally, each by pytest node ID: an explicit list, and the runner refuses an empty one. That covers the existing certification tests whose shared HTTP fakes this round changes. CI runs the rest. All fixtures are synthetic; nothing was fetched.

Commit Red (tests first) Green
9a5286c2, 158b5aee, 0320e5cc read, certify, and refuse at import a manifest naming another build (27 tests; see their commits) 27 passed
edc5356b / ea01e115 certify only while releases.json lists the build and hasn't withdrawn it: 5 of 6 failed 33 passed
ef297cb6 / 62419805 bare file names directly inside the folder: 7 of 7 failed (DID NOT RAISE) 40 passed
58d43c6f / 90301909 only records the writer accepts: 22 of 28 failed 68 passed
89c2cff3 the rules most likely to drift from the writer's 73 passed
ae059ebd merge of main — 73 passed
ba7bd592 / 87bfd56f TestImportCheckBuildFolder::test__given_local_manifest_of_its_folder__then_artifacts_resolve_in_it (pytest.fail: the bundle check refused the local copy) and …::test__given_local_manifest_naming_another_build__then_refuses (AssertionError: a ValueError, not ReleaseBuildFolderError) failed both passed; 75 in the file
edbb041c / 79670733 7 new tests, 6 failed (details below) 80 in tests/test_release_build_folders.py passed; 49 existing node IDs in tests/test_certify_data_release.py and tests/test_regional_input_evidence.py (70 tests with their parameters) passed

The 6 failures at edbb041c:

  • TestBuildFolderManifest::test__given_folder_layout_manifest__then_reads_top_level_build_details (assert None == '2026-10-19T08:33:23+00:00');
  • …::test__given_folder_layout_manifest__then_dump_keeps_build_details_on_top (the same, for the dump);
  • TestCertifyBuildFolder::test__given_build_built_with_the_model__then_certified_on_its_build (pytest.fail: "matches neither the build-time model (unrecorded)…");
  • TestCertifyOnlyListedBuilds::test__given_tagged_release_without_releases_record__then_certifies (releases.json was never requested);
  • …::test__given_tagged_release_where_releases_record_exists__then_refuses (DID NOT RAISE CertificationError);
  • …::test__given_tagged_release_and_unreadable_releases_record__then_refuses (DID NOT RAISE CertificationError).

…::test__given_folder_layout_manifest_without_build_details__then_has_none passed: it guards the old behaviour.

One test change after red: the first build-details test compared the model package's model_dump(), which includes its unset sha256 and wheel_url. The green commit compares model_dump(exclude_none=True). The red failure was the missing built_at, before that comparison.

The existing tests' HTTP fakes: the tagged-layout certification tests in test_certify_data_release.py (and test_regional_input_evidence.py, which uses its _certify_uk) now answer the repository's releases.json with Hugging Face's "entry not found" 404 (_without_releases_record). Those repositories publish tags. They passed at red (70) and at green (70).

ruff format --check and ruff check pass on the changed files. mypy on the two changed modules reports only no-any-return at release_manifest_dir, which predates this PR.

Review rounds

  • 2026-10-22: compared with microcosm-emitter#24 at 7a47f9a. No bugs; 89c2cff3 added the drift tests, and 72347a46 corrected a docstring.
  • 2026-11-04: merged main and fixed the local manifest file. Added the top-level build details (H2c's decision) and the one-layout rule (decided 2026-11-04).

🤖 Generated with Claude Code

anth-volk and others added 4 commits October 20, 2026 16:55
A release manifest in Microcosm's side-by-side layout names its release
and build as strings, and lives at releases/<release>/<build>/. Parse it:
the string build becomes DataBuildInfo with only build_id set, so
release_build_id names the build and two builds of one release differ,
and dumps write it back as the string. Such a manifest may leave its own
files' revision to certification. Manifests whose build is an object,
with or without cut_tag, are read as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A build published side by side has no Hugging Face tag, so the
certificate pins it by its folder. fetch_release_manifest now requires a
manifest that names its release and build to be read from
releases/<release>/<build>/release_manifest.json, and pins the build's
own files at releases/<release>/<build>/<file> and at the manifest URI's
revision: the publisher writes a build's files and manifest in one
commit and never changes them, while an artifact's own revision field
cannot name that commit. Artifacts from other repositories keep their
own pins; a file of the build outside its folder is refused.

The bundle's datasets, region templates, certified dataset URI and the
reachability checks all use the pinned paths, and the build names the
certificate. Manifests published before the layout are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The import-time compatibility check reads the remote release manifest
from the bundle's pinned folder. For a build in its own release folder,
get_data_release_manifest now requires the manifest to name that folder's
release and build, and pins the build's own files inside the folder at
the bundle's revision, as certification does. A mismatch raises
ReleaseBuildFolderError, which is not an unavailable manifest, so the
bundled certificate never covers it. Manifests published before the
layout keep the existing revision rewrite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now holds #615's final code (merged as 2591af3, head 521ab47),
which this branch was built on at an earlier head (0f0d5cd). Merged, not
rebased, so #683's commits keep their hashes. No conflicts: certification.py
and data-certification.md merged automatically, and the diff against main
is still only #683's own five files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk changed the base branch from uk-553-area-routing-tooling to main October 20, 2026 18:34
anth-volk and others added 9 commits October 20, 2026 22:35
A build published in its own release folder is certified only while the
repository's releases.json at main lists it and has not withdrawn it.
The certification helper now serves releases.json by URL, so these tests
can set its contents; the earlier tests keep their behaviour.

Red at this commit: 5 of the 6 new tests fail (the record is never read,
so a withdrawn, unlisted or missing record raises nothing); the tagged
release test passes, since tagged releases have no record to read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fetch_release_manifest now reads the repository's releases.json at main
for a build published in its own release folder, and refuses the build
when the record is missing or unreadable, has another schema version,
does not list the build, or marks it withdrawn. The record is read at
main rather than at the URI's revision, because a build is withdrawn
after the commit that published it. The primary and the regional
manifest both go through this check. Tagged releases are unchanged and
read no record.

Only certification reads the record. Whether an installed bundle should
refuse a build withdrawn after it was certified is not decided here.

Green at this commit: 33 passed in tests/test_release_build_folders.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release writer publishes only bare file names, directly inside
releases/<release>/<build>/. A path that starts with the folder and then
leaves it through "..", a nested path, a dot segment or a backslash must
be refused, at certification and at import. HTTP clients remove dot
segments from URLs, so "<folder>/../<other build>/<file>" would otherwise
fetch another build's file under this build's certificate.

Red at this commit: all 7 new tests fail (nothing is raised).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
read_build_folder checked a build's own files with a string prefix, so
"<folder>/../<other build>/<file>" passed, and HTTP clients then fetched
the other build's file. An own file's path must now be a bare file name,
alone or after "<folder>/": one segment, not "." or "..", with no slash
or backslash, which is what the release writer publishes
(microcosm_emitter_interface.identifiers.bare_file_name). Certification
and the import-time check share this rule.

Green at this commit: 40 passed in tests/test_release_build_folders.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The certification guide, the certification module's docstring and the
changelog fragment now say that a build in its own folder is certified
only while releases.json at main lists it and has not withdrawn it, that
its own files must be bare file names directly inside the folder, and
that import does not re-read releases.json. Documentation only, so no
test step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… accepts

The release writer's parser (microcosm-emitter#24, ReleasesRecord.parse with
check_record) refuses a releases.json with missing or unknown keys, a wrong
schema_version, malformed release or build entries, unsafe release or build
names, a default naming an unlisted release, a current naming an unlisted or
withdrawn build, or more than 4 MiB. Certification should refuse the same
records, even when the build being certified is listed and not withdrawn.

Adds 24 refused records, one oversized record, and 3 records the writer
accepts (no default, a release without builds, a withdrawn build that is not
current), which certification must keep accepting. The helper can now serve
releases.json as raw bytes.

The record fixture now makes the national release's current build its newest
listed, non-withdrawn build. Before, the withdrawn-build and unlisted-build
tests used records whose current named the withdrawn or unlisted build, which
the writer refuses for that reason alone.

Red: 22 failed, 46 passed (68 in the file). All 22 fail with
"DID NOT RAISE CertificationError". Three refused records (missing
schema_version, schema_version 2, missing releases) were already refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check_build_listed now parses the whole record with the release writer's
rules before it looks up the build, so .py no longer certifies from a record
that the writer and its other readers refuse. The rules repeat those of
microcosm-emitter's ReleasesRecord.parse and check_record
(microcosm_emitter_interface.releases, microcosm-emitter#24 at 35481d4d):
exact keys at every level, schema_version 1, safe release and build names,
valid roles, a non-empty published_at and a boolean withdrawn on every build,
a default that is null or a listed release, a current that is null or a
listed build that is not withdrawn, and at most 4 MiB. .py does not depend on
that package; a comment names the writer as the source of the rules.

Also corrects the certification guide: a build withdrawn after it was
certified keeps loading in every installed .py release whose bundle pins it,
and certifying again changes only the .py releases published afterwards.
That is a documentation correction, so it needs no red step.

Green: 68 passed (the test file is unchanged since the previous commit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.py repeats the release writer's parser for releases.json instead of
importing it. These cases pin the rules that are easiest to lose in a
later edit: schema_version must be the integer 1 (not true or 1.0), and a
record that is not JSON, not text, or nested too deeply to decode is
refused like any other record the writer refuses. They pass already; the
fake response now decodes text the way requests does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Certification raises it when releases.json is a record the release
writer refuses, does not list the build, or marks it withdrawn, as well
as for a manifest or file outside its build folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 5 commits November 4, 2026 15:07
One conflict, in provenance/manifest.py: main moved the remote manifest
fetch into _fetch_data_release_manifest, added a bundle-pinned local
manifest file, and checks every loaded manifest against the bundle's
artifacts (_apply_release_revision, then _validate_bundle_artifacts). The
fetched manifest now goes through _bind_to_bundle: a build in its own folder
is pinned to that folder by read_build_folder, any other manifest takes the
bundle's release manifest revision as before, and both are then checked
against the bundle. The local manifest file keeps main's path in this
commit. The imports keep both sides' names.

The 73 tests in tests/test_release_build_folders.py pass on the merged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main added a bundle-pinned local manifest file
(POLICYENGINE_<COUNTRY>_DATA_MANIFEST_FILE). A local copy of a manifest in
its own build folder is still read the old way: its files keep their bare
names, so the bundle check refuses it, and a copy naming another build is
not refused with ReleaseBuildFolderError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bundle-pinned local manifest file now goes through _bind_to_bundle like
the fetched manifest: a build in its own folder has its files pinned inside
that folder, and a copy naming another build raises ReleaseBuildFolderError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…itory

Microcosm's release writer (microcosm#1505) records a build's built_at,
built_with_model_package and built_with_core_package at the top level of a
build-folder release_manifest.json (decided 2026-10-29). They should be read
as the build's details, written back there by a dump, and give the
built-with basis.

A repository is read in one layout: where it has releases.json at main,
only builds in their own folders are certified, and a tagged manifest is
refused. Where Hugging Face answers 404 for it, tagged builds are certified
as before. Any other answer is an error.

The tagged-layout certification tests, here and in
test_certify_data_release.py, now serve their repositories' releases.json
request with Hugging Face's "entry not found" 404 (_without_releases_record),
since those repositories publish tags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…json

- Build details: a build-folder manifest's built_at,
  built_with_model_package and built_with_core_package are read from its
  top level into its build (BUILD_DETAIL_FIELDS), and a dump writes them
  back there. The built-with basis, the data build fingerprint and the
  TRO's start time apply to such builds as to tagged ones.
- One layout per repository: certification reads releases.json at main to
  tell which layout the repository publishes (_releases_record_at_main).
  Only Hugging Face's 404 for the file means none, unless its error code
  says the repository or revision is missing.
  - With a record, a tagged manifest, primary or regional, is refused
    (require_tagged_layout).
  - Without one, tagged builds are certified as before, and a build in its
    own folder is refused because no record lists it.
- The guide and the changelog fragment describe both.

The build details test compares the package's set fields
(model_dump(exclude_none=True)); its red version also compared unset ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk anth-volk changed the title Certify Microcosm builds published side by side in their own release folders Certify Microcosm builds in their own release folders, and tagged builds only where a repository has no releases.json Nov 4, 2026
@anth-volk
anth-volk marked this pull request as ready for review November 5, 2026 19:22
@anth-volk
anth-volk merged commit 5f4ebac into main Nov 5, 2026
13 checks passed
@anth-volk
anth-volk deleted the release-build-folders branch November 5, 2026 19:23
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.

Certify Microcosm builds published side by side in their own release folders

1 participant