feat(snapshot): record table and copied-asset integrity in format.json (#397) - #401
Conversation
Hebbian-Robotics#397) Keep format version 1 and add path/size_bytes/sha256 receipts for required Parquet tables and copy-mode assets, plus a content_id over the normalized inventory so missing members are detectable without shipping a verifier yet.
kstonekuan
left a comment
There was a problem hiding this comment.
Holding this one, and the reason is a shape change rather than anything wrong with the work. The receipt content is right: per-file sha256, the inventory content_id that makes a deleted member visible, and references mode leaving assets empty rather than fetching remote media to hash it. Gate is clean here too, 16 passed.
tables changed type under an unchanged format version. It was a name-to-filename mapping and is now a name-to-record mapping:
- "tables": {
- "samples": _SAMPLES_TABLE_FILE_NAME, # "samples.parquet", a string
+ tables[table_name] = _file_integrity_record(file_name, absolute_path) # a dictSo format_marker["tables"]["samples"] returns "samples.parquet" before this and {"path": ..., "size_bytes": ..., "sha256": ...} after, with format_version still 1. Main's own documentation calls that section the Format contract and says the marker "names each table", so the mapping is published, not incidental. Nothing in the repo reads it besides the tests you updated, but external readers are the entire audience for a snapshot: #397's premise is that this artifact moves independently of the catalog.
This one is partly on me. My direction said added keys are backward compatible and that adding under v1 beats a version break. That was true of adding, and I did not say the obvious corollary: not redefining a field that is already there. Reading it back, it invited exactly this.
The fix, and the shape is yours to pick. Put the integrity block under its own key and leave tables alone:
"tables": { "samples": "samples.parquet", ... },
"integrity": { "tables": { "samples": { "path": ..., "size_bytes": ..., "sha256": ... } },
"assets": [ ... ],
"content_id": "..." }That keeps the whole thing genuinely additive, which makes your doc sentence about backward compatibility true rather than nearly true, and it keeps the verifier purely a reader. Nesting the per-table receipts under integrity.tables rather than flattening them also leaves room for a future non-table member without another reshuffle.
Two smaller things while you are in there.
_build_snapshot_integrity_marker_fields hashes every copied asset, so copy mode now reads every media byte a second time on export. For a snapshot with real video that is a visible slowdown and nobody has costed it. It is very likely worth paying, but say so in the PR with a rough number rather than leaving it for someone to discover.
The 16-hex truncation on content_id deserves a sentence. Matching #389's width is a reasonable instinct for a familiar shape, but that value identifies an episode while this one is the delivery's integrity digest, and #397 cited Croissant's full-SHA-256 recommendation. The per-file hashes are full length so the exposure is limited to the set digest, and 64 bits is ample against corruption while being weak against deliberate tampering. Either keep it and say which threat it covers, or make it full length; I lean toward full length, since nothing here needs it to be short.
The FileNotFoundError when a required table is missing from staging is fine and I would keep it: staging always writes all seven, so it is an invariant check rather than an error path, and failing loudly there beats publishing a marker that promises a file nobody wrote.
Nothing else needs changing, and none of the test work is wasted; the assertions move to a new path rather than being rewritten.
Leave the published name-to-filename tables map unchanged under format v1, move receipts to integrity.tables/assets, and use a full-length inventory content_id. Documents the copy-mode second media read for hashing.
|
Thanks, reshaped it so this stays additive under format v1:
|
kstonekuan
left a comment
There was a problem hiding this comment.
The reshape is right. tables is a string map again, so a v1 reader that already knows the format keeps working, and everything new sits under one key it can ignore.
Validated on your branch against current main: full gate clean, 1585 passed / 6 skipped. I went after the one thing that worried me, which is that the new tests recompute their expected digests with the same helpers the implementation uses. That shape usually cannot notice a helper that stops covering something, because both sides move together. Yours does:
- Receipts stop carrying
sha256, or stop carryingsize_bytes: two tests red. - Assets left out of the inventory, or the
assets/walk skipped: red. - Inventory digest reduced to paths only, contents ignored:
test_dataset_snapshot_marker_integrity_detects_post_export_mutationsred on its own.
That last one is the one that counts, and it works because you mutate the file on disk and let the helper recompute. A contents-blind digest then matches when the test says it must not.
Pushed one commit, docs(snapshot): say the receipt is not a tamper defence. Three changes:
- The
_inventory_content_iddocstring said the digest covers "deliberate set tampering". It cannot: it travels unsigned inside theformat.jsonit describes, so anyone who edits a table can recompute it to match. What it does give is corruption and accidental loss, which is worth having and is what the tests demonstrate. Left the reasoning in the docstring and added the same caveat to the how-to, since someone deciding whether to trust a delivered snapshot will read the docs, not the source. snapshot_module.shutil.copytreein the new test to a plainshutil.copytree. The two existing uses reach through the module because they monkeypatch it; yours does not, and the idiom reads as required when it is copied without that reason.
Nice work on the second pass.
Summary
format_versionat1and add delivery integrity toformat.json: each required Parquet table getspath/size_bytes/sha256.assets/gets the same receipt fields inline (no separate asset manifest); references mode leavesassetsempty and does not fetch remote media.content_idover the normalized inventory so a deleted member is detectable later without a second format change.Closes #397
Test plan
uv run ruff checkuv run ruff format --checkuv run ty checkuv run pytest tests/test_dataset_snapshot.py -q(16 passed)uv run pytest -q(1529 passed, 6 skipped)Notes
content_idwidth matches prepared-manifest episodecontent_idfrom feat(import): per-episode receipts in prepared-manifest.json #389 for a familiar receipt shape.