Root identity: hash-pin every raw microdata input and reference its Chronicle registration (#848) - #853
Open
MaxGhenis wants to merge 13 commits into
Open
Root identity: hash-pin every raw microdata input and reference its Chronicle registration (#848)#853MaxGhenis wants to merge 13 commits into
MaxGhenis wants to merge 13 commits into
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ader Every artifact entry whose kind names microdata now has to say which exact bytes a stage reads and which single Chronicle registration witnesses them. source_manifest validates that shape at load: lowercase 64-hex sha256, a chronicle_artifact object carrying source_id/package_id/year/sha256/access (plus filename, required whenever access is public because only public registrations have an R2 object key), agreement between the entry's sha256 and the registration's, and no repeated locator inside a stage. Unknown access classes and unknown keys fail loudly rather than being ignored. Alongside the validation this adds the reader side the rest of microcosm#848 builds on: microdata_artifact_entries to enumerate the roots of a manifest, resolved_chronicle_registrations to collect the deduped registrations a build actually consumed, load_microdata_pin_allowlist for the per-country microdata_pins_pending.json ratchet, and audit_microdata_pins to report the entries that are neither pinned nor allowlisted. No manifest declares a registration yet, so this commit is inert on the real manifests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…registration per file Three invariants turn the registration reference from free text into something a reviewer cannot get wrong: - access class is derived, not chosen: licensed_microdata registers as licensed, restricted_microdata and private_microdata as restricted (the conservative side — no bytes in any Chronicle store), public_microdata as public. versioned_derived_microdata has no mapping at all: a Microcosm-derived HDF5 is not a publisher release, so it belongs in the pending allowlist rather than in a registration. - the registration year must be one the entry already declares — exactly tax_year_start where present, otherwise any four-digit year in vintage. - entries sharing a SHA-256 must resolve to the same registration. The FRS adult tab feeds five UK stages and the SIPP public-use file feeds four US stages; identity belongs to the bytes, so one file cannot enter a build graph as two differently named roots. Also add load_country_microdata_pin_allowlist, which returns the empty zero-baseline allowlist for a country that ships no pending file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the rest
The UK side is now fully witnessed: all 29 microdata entries in
uk/source_stages.json plus the frozen HMRC/SPI replay manifest carry a
chronicle_artifact, so the UK ratchet baseline is zero. The US side pins the
two roots that name a publisher-distributed file with a verified hash — the
2023 CPS ASEC archive and the SCF 2022 summary extract — and lists the other
37 with a reviewed reason.
Registration ids follow Chronicle's live db/data convention (source_id a
publisher slug, package_id kebab-case and publisher-prefixed) and one package
per distributed file, because a Chronicle manifest holds one file per
(package, year):
dwp/dwp-frs-2024-25-<tab> licensed 14 FRS 2024-25 tabs, SN 9563
ons/ons-was-round-8-household restricted WAS round 8 EUL
ons/ons-lcfs-2023-24-{household,person} restricted
ons/ons-etb-1977-2024-household restricted
hmrc/hmrc-spi-public-use-tape-2022-23 restricted SN 9422
census_cps/census-cps-asec-2023 public asecpub23csv.zip
federal_reserve/federal-reserve-scf-2022-summary-extract public
The pending allowlist is one file for the whole workspace,
build/microdata_pins_pending.json, with a country tag per row, so the ratchet
is a single reviewable number (39) rather than one per country. Rows say what
specifically blocks each pin: 24 US entries name a pooled ASEC column universe
rather than one file, 6 pin the retired pipeline's Hugging Face mirror rather
than the Census release, 2 are Microcosm-derived HDF5s with no publisher
release behind them, and the rest are named individually.
sources.schema.json gains the chronicle_artifact definition, with filename and
access annotated operational to match how the artifact-level keys of those
names are already classified. Byte pins over the changed manifests move with
them: the US stage_asset digest and its two test copies, the UK frozen replay
digest, the regenerated UK release_input_coverage_manifest, and the four
spec_sha256 vectors that attest the schema set.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… ones source_runtime gains the fail-closed root-identity gate. verify_microdata_files hashes each raw microdata file a run was handed and refuses to continue unless the bytes are the pinned ones; the refusal names the publisher, the vintage, the locator, the path, and both digests, so an operator can tell a reissued vintage from a corrupted download without rerunning anything. Keys resolve against both the locator and the declared filename, because a caller-supplied private input's locator is the placeholder "caller-supplied local input". verify_recorded_microdata_pins is the cheap path for a producing run that already hashed its inputs. The ASEC raw-stage checkpoint records a sha256/member_sha256 pin per archive, so load_asec_raw_stage_checkpoint now cross-checks those against the US manifest instead of re-reading hundreds of megabytes per archive. Disagreement is fatal; absence is not — a locator the manifest declares no pin for is reported as unregistered, which is exactly what microdata_pins_pending.json records, and the checkpoint metadata contract is unchanged. Both builds record what they resolved. build_uk_frs_spine verifies every FRS tab and caller-supplied licensed input before any stage reads a table and writes the resulting registrations into the spine sidecar; build_us_fiscal_refresh_release writes a microdata_registrations block beside the existing Chronicle consumer-artifact pin, carrying both the resolved registrations and the pending rows, so a release manifest states its own unwitnessed surface. test_microdata_root_identity is the contract: pinned-or-listed for all four countries and the frozen UK replay manifest, the allowlist ratchet and its stale/orphan row failures, the derived access class, the public-only R2 key, the registration/pin agreement, and both gate paths including the exact diagnosable message. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ment DESIGN.md's process rules gain "build graphs have no anonymous roots": what a microdata root must declare, why licensed and restricted releases register hash-only, that the build gate is fail-closed, and that the pending allowlist is a ratchet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Records what was built, the Chronicle registration ids chosen and why, the 39-row ratchet baseline, and the note chronicle#221 needs: the FRS the UK build reads is 2024-25 / SN 9563, not 2023-24. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
"caller-supplied local input" is the locator every UK private root shares, so resolving a supplied file by that key would compare one file against the WAS, LCFS, ETB, and SPI pins at once and report three spurious mismatches. The gate now names the ambiguity and points at the declared filename, which is what the UK builder already keys by. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Main moved the UK source manifest and the release input-coverage manifest, so the byte pins over both had to be recomputed on the rebased tree rather than carried across: the regenerated uk/release_input_coverage_manifest.json and the four spec_sha256 vectors that attest the schema set. No contract changed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
python -O strips asserts, so a bare assert is the wrong guard inside a fail-closed check. The ambiguity check just above already proves every match shares one pinned digest, so read that value directly and raise if it is somehow absent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pping The allowlist spans every country, so auditing the frozen UK replay manifest — read as JSON rather than through the loader — would have reported all 37 US rows as orphans of it. A SourceManifest names its own country; anything else must now be told, and the audit takes an explicit country override. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…spec digests microcosm.build.source_runtime is a _DIRECT_KERNEL_MODULE in spec_engine/seeds.py, so its source is attested by every country's spec_sha256: the am, be, uk, and loader golden vectors move whenever the gate's code changes, by design. This re-pins them once against the final code, and drops the quotes the registration blocks in uk/spec/sources.yaml were written with so they read like the surrounding entries and like the generator's output in us/spec/sources.yaml. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Anyone editing the gate again will move four spec_sha256 vectors with it; say so once rather than let the next editor rediscover it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
Author
|
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. |
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 #848.
A build graph should have no anonymous roots. Today Microcosm pins raw
microdata in per-country manifests, checkpoint metadata, code constants, and
command-line arguments, with no shared registry, no licence record, and no
timestamp anyone outside the build can check. This makes every raw microdata
input declare the SHA-256 of the exact file its stage reads plus the one
Chronicle registration that witnesses it, refuses to build when the bytes on
disk disagree, and lists the roots that cannot be pinned yet in one ratcheted
allowlist.
This is the consumer half of the contract shared with chronicle#221 and the
Chronicle ADR "Raw microdata in Chronicle is identity, not content."
The contract
Every artifact entry whose
kindis one ofpublic_microdata,licensed_microdata,restricted_microdata,private_microdata, orversioned_derived_microdatacarries eithersha256(64 lowercase hex) of the file the stage reads, pluschronicle_artifact{source_id, package_id, year, sha256, filename, access}— where the R2 key
raw/{source_id}/{package_id}/{year}/{sha256}/{filename}exists only for
access: public; orpackages/microcosm-build/src/microcosm/build/microdata_pins_pending.jsonwith
country,stage,locator,reason,issue.Three invariants make the reference hard to get wrong, so review is about
whether the registration is right rather than whether it is well-formed:
licensed_microdata→licensed;private_microdataandrestricted_microdata→restricted(the conservative side — no bytes in any Chronicle store);
public_microdata→public.versioned_derived_microdatahas no mappingat all: a Microcosm-derived HDF5 is not a publisher release, so it must be
allowlisted rather than registered.
tax_year_startwhere present, otherwise a four-digit year appearing invintage.the same registration; the FRS
adulttab feeds five UK stages and the SIPPpublic-use file feeds four US stages, and identity belongs to the bytes.
Where archive and member are already distinguished (
weeks_unemployed_input,scf_wealth),member_sha256is kept unchanged and the registration witnessesthe distributed archive — the file a publisher actually released and the only
thing Chronicle can register — while the member pin remains Microcosm's extra
reach inside it.
The fail-closed gate
microcosm.build.source_runtimegains:verify_microdata_files— hashes every raw microdata file a run washanded and refuses to continue unless the bytes are the pinned ones. The
refusal names the publisher, the vintage, the locator, the path, and both
digests, so an operator can tell a reissued vintage from a corrupted download
without rerunning anything. Keys resolve against the locator and the
declared filename, because a caller-supplied private input's locator is the
shared placeholder
"caller-supplied local input"; a key that resolves toseveral differently-pinned files is refused as ambiguous rather than
arbitrarily matched.
verify_recorded_microdata_pins— the cheap path when a producing runalready hashed its inputs. Disagreement is fatal; absence is not: a locator
the manifest declares no pin for is reported as unregistered, because that is
exactly what the allowlist records, and this check must not invent a
registration the repository has not made.
Wired into the three places raw microdata actually enters a build:
tools/build_uk_frs_spine.pymicrodata_registrationsus_runtime/asec_checkpoint.pysha256/member_sha256source pins against the US manifest instead of re-hashing hundreds of megabytes per archive; the checkpoint metadata contract is unchangedtools/build_us_fiscal_refresh_release.pymicrodata_registrationsblock next to the existing Chronicle consumer-artifact pin, carrying both the resolved registrations and the pending rows, so a release manifest states its own unwitnessed surfaceRegistration ids chosen
Chronicle's live
db/data/*/manifest.yamluses a publisher-slugsource_idand a kebab-case, publisher-prefixed
package_id(hmrc/hmrc-spi-income-bands-2023-24), andfiles[year]holds exactly one file per(package, year). So this uses one package per distributed file ratherthan one per study — the same granularity Chronicle already uses for
soi-table-1-1vssoi-table-1-2. That is a deviation from the tentativedwp/frs-2023-24andhmrc/spi-<vintage>in the brief, and it is what thekey shape requires.
Note for chronicle#221: the FRS this build reads is 2024-25, UKDS
SN 9563 (
tax_year_start: 2024), not 2023-24, and the UK manifest carries21 licensed references over 14 distinct tabs.
Pin / allowlist table
Pinned — 32 entries resolving to 21 distinct registrations. UK is fully
pinned, so its ratchet contribution is zero.
frs_spineaccounts.tabdwp/dwp-frs-2024-25-accounts@ 2024 (licensed)frs_spineadult.tabdwp/dwp-frs-2024-25-adult@ 2024 (licensed)frs_spinebenefits.tabdwp/dwp-frs-2024-25-benefits@ 2024 (licensed)frs_spinebenunit.tabdwp/dwp-frs-2024-25-benunit@ 2024 (licensed)frs_spinechild.tabdwp/dwp-frs-2024-25-child@ 2024 (licensed)frs_spinechldcare.tabdwp/dwp-frs-2024-25-chldcare@ 2024 (licensed)frs_spineextchild.tabdwp/dwp-frs-2024-25-extchild@ 2024 (licensed)frs_spinehousehol.tabdwp/dwp-frs-2024-25-househol@ 2024 (licensed)frs_spinejob.tabdwp/dwp-frs-2024-25-job@ 2024 (licensed)frs_spinemaint.tabdwp/dwp-frs-2024-25-maint@ 2024 (licensed)frs_spinemortgage.tabdwp/dwp-frs-2024-25-mortgage@ 2024 (licensed)frs_spineoddjob.tabdwp/dwp-frs-2024-25-oddjob@ 2024 (licensed)frs_spinepenprov.tabdwp/dwp-frs-2024-25-penprov@ 2024 (licensed)frs_spinepension.tabdwp/dwp-frs-2024-25-pension@ 2024 (licensed)frs_employmentadult.tabdwp/dwp-frs-2024-25-adult@ 2024 (licensed)frs_council_taxhousehol.tabdwp/dwp-frs-2024-25-househol@ 2024 (licensed)frs_educationadult.tabdwp/dwp-frs-2024-25-adult@ 2024 (licensed)frs_educationchild.tabdwp/dwp-frs-2024-25-child@ 2024 (licensed)frs_legacy_proxiesadult.tabdwp/dwp-frs-2024-25-adult@ 2024 (licensed)frs_hmrc_spine_leavesadult.tabdwp/dwp-frs-2024-25-adult@ 2024 (licensed)frs_hmrc_spine_leavesbenefits.tabdwp/dwp-frs-2024-25-benefits@ 2024 (licensed)was_wealthcaller-supplied local inputons/ons-was-round-8-household@ 2018 (restricted)lcfs_consumptioncaller-supplied local inputons/ons-was-round-8-household@ 2018 (restricted)lcfs_consumptiondvhh_ukanon_v2_2023.tabons/ons-lcfs-2023-24-household@ 2023 (restricted)lcfs_consumptiondvper_ukanon_202324_2023.tabons/ons-lcfs-2023-24-person@ 2023 (restricted)etb_vathouseholdv2_1977-2024.tabons/ons-etb-1977-2024-household@ 1977 (restricted)etb_serviceshouseholdv2_1977-2024.tabons/ons-etb-1977-2024-household@ 1977 (restricted)hmrc_spi_income_spinecaller-supplied local inputhmrc/hmrc-spi-public-use-tape-2022-23@ 2022 (restricted)hmrc_spi_incomecaller-supplied local inputhmrc/hmrc-spi-public-use-tape-2022-23@ 2022 (restricted)hmrc_spi_incomecaller-supplied local inputhmrc/hmrc-spi-public-use-tape-2022-23@ 2022 (restricted)weeks_unemployed_input…/cps/datasets/2023/march/asecpub23csv.zipcensus_cps/census-cps-asec-2023@ 2023 (public)scf_wealth…/econres/files/scfp2022s.zipfederal_reserve/federal-reserve-scf-2022-summary-extract@ 2022 (public)Pending — 39 rows, the committed ratchet baseline. Grouped by what
specifically blocks the pin; each row in the file states its own reason.
education_inputs,retirement_contributions,childcare_inputs,adult_care_inputs,energy_subsidy,child_support_inputs,disability_benefits_input,workers_compensation_input,ssi_take_up,meps_esi_premiums,prior_year_income,immigration_status,hours_worked,snap_take_up,snap_state_take_up,relationship_inputs,medicare_take_up_input,retirement_distributions,eligibility_inputs,pregnancy,snap_abawd_discretionary_exemption,aca_marketplace_inputs,medicaid_take_up,other_health_insurance_premiumsus_runtime/education_assistance_source.py, so this is a manifest-shape change, not a discovery problem.scf_wealth,ssi_disability_criteria,sipp_head_start,vehicle_assets,voluntary_filing_input21280dca…, not the Census-distributed SIPP 2023 release. Chronicle registers the publisher artifact, whose own hash needs one network-enabled fetch.sipp_tipspu2023_slim.csvis a PolicyEngine-derived column subset of SIPP 2023 with no publisher release of its own; registration must reference the Census file behind it.puf_tax_detail,education_inputspuf_tax_detail,acs_rentpuf_2024.h5andacs_2022.h5areversioned_derived_microdataproduced by the retired pipeline, not publisher releases, so they have no Chronicle access class.acs_2022.h5is hash-pinned; its official PUMS zips are named in the entry and unhashed.acs_rentscf_wealth(scf2022s.zip)integrity_notealready records.org_wagesjan24pub–dec24pub); the entry pins the generated cache's content hash, not the twelve distributed archives.silc_loadload_populace_us_support_poolpolicyengine/populace-ussupport pool has no certified revision, filename, or SHA-256 yet — its ownintegrity_notemakes those a harvest item.24 + 5 + 1 + 2 + 2 + 1 + 1 + 1 = 37 US rows, plus one BE and one AM row, is
the 39-row baseline.
education_inputsandacs_renteach appear in twogroups because each declares two microdata artifacts.
One allowlist, not one per country
The brief's example path was
build/us/microdata_pins_pending.json. A fileinside a country package is a declared package resource, so adding one to
am/and
be/moves theirCountrySpecfingerprints andresource_hashes, whichwould require regenerating
tests/golden/{am,be}_country_spec.json— expresslyout of scope here. The allowlist is therefore a single country-tagged file one
directory up, at
microcosm/build/microdata_pins_pending.json. That also makesthe ratchet a single number a reviewer can watch (39) instead of four.
Contract tests
packages/microcosm-build/tests/test_microdata_root_identity.py(37 tests,shared-specgroup,tools/ci_test_groups.py --verifygreen, fully hermetic —the gate tests build synthetic files in
tmp_path):am/be/uk/usand the frozen UK replay manifest;every row states a reason and an issue, an over-baseline file is refused, and
stale and orphan rows are reported as gaps;
agreement, one registration per file, and the refusals for a mismatched
digest, an unknown access class, an access class the kind does not imply, a
public registration without a filename, a registration without a pin, an
uppercase digest, an undeclared year, and a derived artifact carrying a
registration;
raise with publisher/vintage/locator/expected/actual in the message, unknown
keys and missing files are refused, a caller-supplied input resolves by its
declared filename, the shared placeholder locator is refused as ambiguous,
and the real UK manifest rejects a tampered
adult.tabacross all fivestages that read it;
member digest raises, and an unregistered locator is reported rather than
refused.
Digests that moved
Byte pins over the manifests this PR edits, re-pinned in the same commits that
changed the bytes. No dataset golden was regenerated and no on-disk, artifact,
or diagnostic key was renamed (#639).
us/spec/sources.yamlstage_asset.sha256and its two test copies, plus theFROZEN_LEGACY_RESOURCE_SHA256constant intools/generate_us_bundle_from_constants.py; the US bundle YAML wasregenerated by that tool rather than hand-edited.
FROZEN_SOURCE_STAGES_SHA256intest_uk_source_stages.py— the frozenHMRC/SPI replay manifest is a live runtime resource
(
uk_runtime/hmrc_source_contract.py), so it carries its registration inlockstep with the canonical copy, as the lockstep test already requires.
uk/release_input_coverage_manifest.json, regenerated withtools/build_uk_release_input_coverage_manifest.py.spec_sha256vectors (am,be,uk, and the synthetic loader goldenvector). These move for two reasons, both by design: they attest the schema
set, which gains the
chronicle_artifactdefinition; andmicrocosm.build.source_runtimeis a_DIRECT_KERNEL_MODULEinspec_engine/seeds.py, so its source is attested by every country's specidentity. Any later change to the gate's code moves these four again — they
were re-pinned last, against the final code.
Scope
No publish, promotion, or HF destination change; no golden regeneration; no key
renames. PR CI stays secrets-free — every new test is hermetic and none touches
restricted microdata.
Residual risks
committed its manifests when this was written; if it lands different ids,
each is a one-line change per entry, and the contract tests pin the shape,
not the names.
ons-was-round-8-household@ 2018 andons-etb-1977-2024-household@ 1977take their year from the
vintagethe entry declares (2018_20,1977_24),which is the series/reference start rather than the edition year. If
Chronicle keys those files by edition year instead, the year field moves.
licensed FRS tabs, which PR CI deliberately cannot see. Its behaviour is
covered by hermetic tests and by rejecting a tampered
adult.tabagainst thereal manifest pins.
🤖 Generated with Claude Code