Register raw microdata releases as source artifacts: licence/access fields, hash-only registration, first UK registrations (#221) - #227
Conversation
Replace the stale Lane C5 progress log with this lane's state, contract, and next steps for chronicle#221 (register raw microdata releases as source artifacts). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Introduce chronicle/registration.py as the leaf module owning the access class closed set (public/licensed/restricted), the manifest kind closed set (publisher_table/microdata_release), file-entry validation, and register_hash_only_artifact. Wire it through artifacts.py (fetch refuses hash-only access, inventory accepts entries with no local file, publish refuses to upload them), source_package.py (microdata releases are never parsed), and the harness/CLI (new `chronicle register-artifact`, plus --access/--licence on fetch-artifact and --skip-hash-only on publish-raw). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/register_microdata_releases.py resolves a declarative catalogue of releases against a read-only PolicyEngine/microcosm checkout. Every sha256, size, filename, and vintage is read verbatim from Microcosm's reviewed pins; the script authors only the Chronicle identity and the publisher terms. `emit` writes hash-only kind: microdata_release manifests for licensed and restricted releases: 14 distinct DWP FRS 2023-24 tabs (19 references across five Microcosm stages resolve to 14 distinct files) and the HMRC SPI Public Use Tape 2022-23. No bytes are fetched or stored and no R2 key is recorded. BE-SILC 2023 is reported as a blocker rather than registered: Microcosm pins it with no sha256, size, or per-file filename, and no hash is invented. `plan` prints the exact fetch-artifact commands for public releases, copying publisher URLs verbatim and printing TODO where Microcosm records none. Also updates the two artifact-count assertions for the new hash_only_count and hash_only_refused_count report keys. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
59 tests covering the access and kind vocabularies, every manifest-entry validation code, and each command's behaviour: register-artifact refuses public access, a missing or malformed checksum, a missing licence, vintage, access route, or verification timestamp, bytes on disk, a publisher_table manifest, a foreign source_id, and an unrequested reissue; fetch-artifact refuses a hash-only access class and refuses to pull bytes over an existing registration without reading the source; publish-raw refuses hash-only entries without touching bytes; inventory-artifacts accepts them with no local file and flags them when bytes appear. Also pins the committed FRS and SPI registrations as identity-only: no storage block, no R2 key, no bytes beside the manifest. Reorders the fetch refusals so overwriting a hash-only registration reports the byte-boundary violation rather than prompting for --licence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds the microdata-release example to the storage doc's Object Key Conventions (public releases keep the ordinary content-addressed key; licensed and restricted ones have no key at all), a Hash-Only Registrations section to the harness doc, a register-artifact entry to the README artifact command block, and a data-sources note recording why BE-SILC 2023 cannot be registered yet. The governance, architecture, AGENTS, and non-goal amendments are left to the ADR branch (PR #222), which already makes them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Only a kind: microdata_release manifest may list several files under one vintage. _year_mapping previously returned the list and let the caller raise TypeError on spec["filename"], which validate_source_package does not catch. It now raises a ValueError naming the shape, so a malformed publisher-table manifest becomes a validation error instead of a crash. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The byte boundary was escapable and the fetch plan did not run: - _assert_no_hash_only_entry only inspected files[year] for the requested year and only entries that survived the kind-gated list expansion, while the write target and R2 key are directory-scoped. A licensed release registered under one vintage could be fetched into the tree under another, and a list under a manifest with an absent or misspelled kind was invisible to the guard. Both wrote licensed bytes into the package tree and would have uploaded them with --upload-r2. The guard now walks every vintage and expands lists unconditionally; validating the shape stays the validator's job. - _upsert_manifest replaced the whole files[year] value, so fetching a different filename into a package silently deleted an existing hash-only registration. It now replaces only the entry for its own filename. - register_hash_only_artifact raised on the first filename match with a different checksum, so re-registering a current pin failed once a reissue existed and `emit` stopped being re-runnable. Now two passes: an exact (filename, sha256) match is replaced wherever it sits. - publish-raw returned before validating a hash-only entry, so --skip-hash-only reported bytes on disk or a recorded R2 key as valid, disagreeing with inventory-artifacts about the same tree. The entry is now always validated; the flag turns off the refusal, not the contract check. - Nothing could register a public microdata release as kind: microdata_release, though the ADR puts redistributable public-use files in scope. fetch-artifact gains --kind, and a microdata-release manifest keeps the list shape so several files can share one vintage. - Every command `plan` printed used --source-url, which fetch-artifact does not accept; all nine died at argparse. They now use --url and pass --kind, and a test parses each generated command against the real CLI parser. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Title Case headings and prose hard-wrapped at 78 characters, matching every other heading and paragraph in these files. Only lines this branch added are touched; the diff stays a pure insertion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Review (Fable, main): approving, with the Sol gate running. Cross-checked every registered hash against Microcosm's Sequencing: #222 merges first (its gate is running; merge-on-agreement), then this. No file overlap between the two. Notes, none blocking:
|
|
Gate round 1: changes requested. The 15 registrations match the consumer pins and add no bytes or fact surfaces, but Sol found real escapes around the boundary, all of which stand: filename aliases ( Two additions from the ADR review (#222 round 2) fold into the same fix: byte custody requires an affirmative, evidence-backed redistribution signal, not merely Fix lane queues behind #226's round-2 fixes because both rewrite |
|
Cross-PR convention ruling (chronicle#227 ↔ microcosm#853), so both sides land on one shape:
Chronicle's #227 fix round and a small #853 follow-up apply this. |
Summary
Implements chronicle#221: Chronicle registers every raw microdata release its
consumers build from, and stores the content of none of them.
Depends on #222 (the raw-microdata-identity ADR and the governance,
architecture,
AGENTS.md, and non-goal amendments). This PR deliberately makesnone of those edits, so the two do not conflict — but until #222 merges, the
governance doc still forbids what this branch does, and the ADR file that four
files cite does not exist on
main. Merge #222 first.Contract is shared verbatim with microcosm#848.
Schema and validation
Manifest file entries gain two fields:
licence— the publisher's terms, as an identifier or URL.access— a closed set:public|licensed|restricted.publicis inferred where absent;fetch-artifactnow writes the classexplicitly onto every entry it touches. Both fields are required on a
kind: microdata_releasemanifest, the new manifest-level carve-out.Only
publicbytes enter a Chronicle store, so the R2 keyraw/{source_id}/{package_id}/{year}/{sha256}/{filename}exists only for thatclass. A
licensedorrestrictedrelease is registered hash-only: themanifest carries the checksum, size, vintage, licence, access route, and
verification timestamp, and no bytes are fetched, written, or uploaded.
A registration is identified by
{source_id, package_id, year, sha256, filename}— the tuple consumers reference underchronicle_artifact.New command
chronicle register-artifactregisters a licensed or restricted release fromCLI arguments alone. It refuses
--access public, a missing or malformedchecksum, a missing licence, vintage, access route, or verification timestamp,
bytes sitting beside the manifest, a
publisher_tablemanifest, a foreignsource_id, and an unrequested reissue. Public releases keep the existingfetch-artifactpath, which gains--access,--licence, and--kind.First registrations
15 hash-only registrations, every value read verbatim from Microcosm's reviewed
pins and cross-checked field by field:
access: licensed, UK DataService End User Licence, study SN 9367, DOI
10.5255/UKDA-SN-9367-2.Microcosm carries 19
licensed_microdatareferences across five stages;they resolve to 14 distinct files (identical SHA-256 where they repeat).
put2223uk.tab,access: restricted, study SN 9422, DOI10.5255/UKDA-SN-9422-1.chronicle inventory-artifacts --root db/datais valid with 195 artifacts: 180with R2 links, 15 hash-only with no local bytes.
BE-SILC 2023 is not registered. Microcosm pins it with no
sha256, nosize_bytes, and no per-file filename — the locator names four file roles.No hash is invented; the blocker and its unblock steps are recorded in
docs/data-sources/be-silc-2023-registration-blocker.md.Tooling
scripts/register_microdata_releases.pyresolves a declarative catalogueagainst a read-only microcosm checkout.
emitwrites the hash-only manifestsbyte-stably;
planprints the exactfetch-artifact --upload-r2commands torun from a networked machine for the public US releases, copying publisher URLs
verbatim and printing
TODO_PUBLISHER_URLwhere Microcosm records none (CPSbasic monthly 2024, SIPP 2023). The catalogue is the only authored content —
every checksum, size, filename, and vintage is read from Microcosm.
Audit fixes
An adversarial pass over the first implementation found the byte boundary was
escapable. All six are fixed here with regression tests:
only inspected
files[year]for the requested year and only entries thatsurvived a kind-gated list expansion, while the write target and R2 key are
directory-scoped. A release registered under one vintage could be fetched
under another, and a list under a manifest with an absent or misspelled
kindwas invisible to the guard. Both were reproduced writing licensedbytes into the tree. The guard now walks every vintage and expands lists
unconditionally.
files[year]value. It now replaces only the entry for its own filename.emitstopped being re-runnable after a reissue — registration raised onthe first filename match with a different checksum before reaching the exact
match. Now two passes.
--skip-hash-onlyreported a contract violation as valid, disagreeingwith
inventory-artifactsabout the same tree. Hash-only entries are nowalways validated; the flag turns off the refusal, not the check.
kind: microdata_release, though the ADR puts redistributable public-use files inscope.
fetch-artifactgains--kind, and a release manifest keeps the listshape so several files share one vintage (ACS household + person).
planemitted--source-url, whichfetch-artifactdoes not accept. Fixed to--url, anda test now parses each generated command against the real CLI parser.
Also hardened
_year_mapping, which returned a list and let the caller raise anuncaught
TypeErrorthatvalidate-packagedoes not catch.No content, anywhere
No microdata row, cell, or fact is added. No source package parses a release:
validate-packagefails withmicrodata_release_not_parseable, and the FRS andSPI packages hold nothing but
manifest.yaml. Bucket names stay configurable(
ledger-rawdefault, unchanged).Chronicle Governance
For any source package, consumer contract, schema, or source-data
boundary change:
Approved Chronicle agent role:
ledger-source-ingestor— thedb/data/**microdata-release manifestsledger-contract-maintainer— the manifest schema (licence,access,kind), its validation, and the registration commandNote both roles'
allowed_pathsin.github/chronicle-agents.ymlpredatethis work and cover none of
chronicle/registration.py,chronicle/artifacts.py,chronicle/harness.py,chronicle/cli.py,db/data/**,scripts/**,docs/**, orREADME.md. Flagging rather thanquietly widening the manifest; reviewers may want a follow-up that extends
the globs.
Deterministic checks run:
uv run pytest -q— full suiteuv run ruff check .— passesuv run ruff format --check .— 14 pre-existing failures, all in files thisbranch does not touch (verified unchanged against
origin/main); thebranch's own files pass
uv run chronicle inventory-artifacts --root db/data— valid, 195artifacts, 15 hash-only
scripts/register_microdata_releases.py emit— re-run is byte-stableLLM judge verdicts:
ledger-source-fidelity: PASS — no fact is added. Every registeredvalue (sha256, size, filename, vintage) is transcribed verbatim from the
consumer's reviewed pin, machine-verified against Microcosm, and each entry
records
hash_sourceplus a note that Chronicle did not recompute thechecksum. BE-SILC is refused rather than guessed.
ledger-contract: PASS — the change adds two manifest fields and onemanifest kind, preserves provenance, and moves no Microcosm responsibility
into Chronicle. Registration is identity only; selection, reconciliation,
aging, and imputation are untouched.
ledger-boundary: PASS — no microdata row, cell, or fact entersChronicle; no source package parses a release; no licensed or restricted
bytes enter any Chronicle store, and the escapes that previously allowed
that are closed and regression-tested.
Tests
tests/test_chronicle_microdata_registration.py— 69 tests covering the accessand kind vocabularies, every manifest-entry validation code, each command's
refusals, the six audit regressions, public microdata releases, the generated
fetch plan, and the committed FRS/SPI registrations as identity-only.
Two existing assertions in
tests/test_chronicle_artifacts.pygain the newhash_only_countandhash_only_refused_countreport keys.🤖 Generated with Claude Code