Repository navigation
Certify Microcosm builds in their own release folders, and tagged builds only where a repository has no releases.json - #683
Merged
Merged
Conversation
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>
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>
10 of 13 tasks
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
marked this pull request as ready for review
November 5, 2026 19:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #682
Base:
main.mainwas merged into this branch twice, with merge commits (no rebase):36c3d8d3(with #615's final code) andae059ebd(84 commits ofmain, on 2026-11-04).Why
Microcosm is moving each dataset repository to a layout where:
releases/<release>/<build>/, and never changed;releases.json(microcosm-emitter#24,microcosm_emitter_interface.releases) records every published build of each release, which one is current, and which are withdrawn;Until a repository switches, it keeps publishing tagged builds with
latest*.jsonpointer files. policyengine.py has to certify both, and never mix them in one repository.What changes
One layout per repository (certification)
releases.jsonatmain(_releases_record_at_main). Only Hugging Face's 404 for the file means the repository has none, unless its error code (X-Error-Code) isRepoNotFoundorRevisionNotFound. Any other answer refuses certification, so a failed read is never taken for a tagged repository.releases.json: only builds in their own folders are certified. A tagged manifest, passed as--manifest-urior--regional-manifest-uri, is refused (require_tagged_layout), and the error says to certifyreleases/<release>/<build>/release_manifest.jsoninstead.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)DataReleaseManifest.releaseis new. A manifest with stringreleaseandbuildis read withbuildasDataBuildInfo(build_id=<build>). Both names must match the publisher's rule,^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$. A stringbuildwithoutrelease, orreleasewith an objectbuild, is refused.built_at,built_with_model_package(version, git SHA, data build fingerprint) andbuilt_with_core_package. They are read intobuild, so thebuilt_with_model_packagebasis, the data build fingerprint and the TRO's start time apply as for tagged builds. A dump writesbuildback as the string and the details back at the top level, so merges round-trip it.release_build_idis unchanged. Such a build has nocut_tag, so it returns the build name, and two builds of one release never match.build_folderreturnsreleases/<release>/<build>, orNonefor older manifests.DataReleaseArtifact.revisionmay be absent only in a build-folder manifest.Certifying a build in its own folder (
provenance/certification.py, through the sharedread_build_folder)releases/<release>/<build>/release_manifest.json.main:releases.jsonatmainmust list the build and must not mark it withdrawn, and the whole record must be one the release writer's parser accepts. policyengine.py repeatsReleasesRecord.parseandcheck_recordwithout depending on the interface package: exact keys,schema_version1, safe names and roles,published_atandwithdrawntypes,defaultandcurrent, and the 4 MiB limit. The same applies to a local-area release passed with--regional-manifest-uri.releases/<release>/<build>/<file>and at the URI's revision. Eachpathmust 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.data_releases.uk.version, the certificate'sdata_build_idand the changelog fragment name.At import
get_data_release_manifestchecks a build-folder manifest against the bundle's pinnedrelease_manifest_path. A manifest naming another release or build raisesReleaseBuildFolderError, aValueErrorthat is deliberately not aDataReleaseManifestUnavailableError, so the offline fallback to the bundled certificate never covers it.main, this covers both the fetched manifest andmain's bundle-pinned local manifest file (POLICYENGINE_<COUNTRY>_DATA_MANIFEST_FILE). Both go through_bind_to_bundleand thenmain's_validate_bundle_artifacts.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.mainhad 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_folderfor a build folder,_apply_release_revisionotherwise, 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.jsonread for tagged manifests uses the same token as the manifest read:certify_data_release'stokenargument, whichscripts/bundle.py certify-dataleaves unset, as before. CI'sHUGGING_FACE_TOKENsecret for bundle verification is unchanged. The scheduled UK certification workflow readspolicyengine/policyengine-uk-data-privatethroughuk_qualificationand doesn't callcertify_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.
9a5286c2,158b5aee,0320e5ccedc5356b/ea01e115releases.jsonlists the build and hasn't withdrawn it: 5 of 6 failedef297cb6/62419805DID NOT RAISE)58d43c6f/9030190989c2cff3ae059ebdmerge ofmainba7bd592/87bfd56fTestImportCheckBuildFolder::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: aValueError, notReleaseBuildFolderError) failededbb041c/79670733tests/test_release_build_folders.pypassed; 49 existing node IDs intests/test_certify_data_release.pyandtests/test_regional_input_evidence.py(70 tests with their parameters) passedThe 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.jsonwas 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_nonepassed: 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 unsetsha256andwheel_url. The green commit comparesmodel_dump(exclude_none=True). The red failure was the missingbuilt_at, before that comparison.The existing tests' HTTP fakes: the tagged-layout certification tests in
test_certify_data_release.py(andtest_regional_input_evidence.py, which uses its_certify_uk) now answer the repository'sreleases.jsonwith 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 --checkandruff checkpass on the changed files.mypyon the two changed modules reports onlyno-any-returnatrelease_manifest_dir, which predates this PR.Review rounds
7a47f9a. No bugs;89c2cff3added the drift tests, and72347a46corrected a docstring.mainand 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