Skip to content

refactor(snapshot): type receipt entries while preserving the content_id invariant - #493

Merged
kstonekuan merged 2 commits into
Hebbian-Robotics:mainfrom
Sagar-024:fix/489-receipt-records
Sep 11, 2026
Merged

kstonekuan merged 2 commits into
Hebbian-Robotics:mainfrom
Sagar-024:fix/489-receipt-records

Conversation

@Sagar-024

Copy link
Copy Markdown
Contributor

refactor(snapshot): type receipt entries while preserving the content_id invariant

Closes #489.

What changed

Receipt entries no longer travel as dict[str, str | int] through the verifier. A frozen FileIntegrityRecord (path: str, size_bytes: int, sha256: str) is parsed once per entry at the marker-reading boundary, next to the #473 content_id gate, and everything after the boundary is typed: the inventory gate, the per-file loop, and the sha256 comparison at what was :942.

The boundary refuses without coercion, naming the exact field: a receipt whose sha256 arrived as a JSON number used to reach a comparison that can never succeed and was reported as damaged bytes (exit 1, "your data is damaged"); the truth is that the receipt is malformed, which is unreadable input (exit 2, "your receipt is written wrong"). The same refusal covers a non-string path, a non-int size_bytes, and bool masquerading as int.

The invariant, proven

The hard constraint: _inventory_content_id serializes entries with json.dumps(sorted-by-path, sort_keys=True, separators=(",", ":")), and #483 made every verify compare that digest against the stored content_id. Moving one byte breaks every snapshot ever exported.

Proof from this branch: the exact serialized string over an 8-entry inventory (7 tables, 1 asset, 1000 characters) and its digest are byte-identical before and after the refactor. Digest before and after: c117d138cfdd8cd4c613906b8813aa92a89a96192c89ee6522e7a30fc9b69b9a. The bridge is to_dict_for_hashing(), which rebuilds exactly the dict shape the exporter has always serialized, and the digest is computed over that dict form, never over the dataclass.

Durable pins in the suite: an invariant test that computes the digest the old raw-dict way and the new records way and asserts both equal a computed golden value, and a refusal test feeding a numeric sha256 and asserting the boundary raises naming the field.

Exporter unchanged, proven

The exporter now builds records and serializes them through to_dict_for_hashing when writing the marker, so the emitted format.json carries the identical dict shapes. The dataset-snapshot suite, which pins the marker's structure and recomputes its content_id, passes unchanged apart from parsing records where it recomputes.

Mutation

With the parser changed to accept an int sha256, exactly the refusal test fails and the other 42 stay green. Restored, all green.

Validation

43 passed across tests/test_snapshot_verify.py and tests/test_dataset_snapshot.py, ruff check, ruff format, ty clean on the changed files, zero non-ASCII and zero em dashes. Boundary-family note: #486 (format identity) and #483 (inventory gate) are merged; #469 (path containment) stays with VARUN3WARE; #474 (post-sync CRC reads) is claimed and next.

…rrowed

Typing the entries changed the digest from 'every key the entry carried' to
'path, size_bytes, sha256'. An entry with an extra key now hashes the same
where it used to hash differently: verified against main, which raises on
that marker while this branch certifies it.

That is the right trade, because it lets a later format revision add
metadata without invalidating every snapshot already exported, and the
receipt was never a tamper defence. But it was silent in both the code and
the tests, so it gets a case and a paragraph.

@kstonekuan kstonekuan left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, merging.

Verified across versions rather than within one: exported a copy-mode snapshot on main, then ran this branch's verifier against it. Digest identical.

Pushed one commit. The digest now covers path, size_bytes, sha256 and nothing else, so an entry with an extra key hashes the same where it used to differ. That is the right trade (a later revision can add metadata without invalidating existing snapshots) but it was silent in the code and the tests.

@kstonekuan
kstonekuan merged commit 28cbebd into Hebbian-Robotics:main Sep 11, 2026
6 checks passed
@Sagar-024

Copy link
Copy Markdown
Contributor Author

Fair point on the cross-version check. I proved the two paths agreed on the branch, which only proves internal consistency, not stability. Exporting on main to verify against the branch is exactly the blind spot I missed.

The extra-key delta is a sharp catch too. I was so focused on byte-identity for today's shape that I didn't map out the input space for future metadata. I will carry that four-point checklist (added, removed, reordered, retyped) into anything involving serialization from now on.

I am starting the lane-entry validation for #474 now.
Hope Demo Day week leaves you enough time to sleep. Catch you in the next review. @kstonekuan

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.

Integrity receipt entries are dict[str, str | int], so four use sites coerce and one of them forgets

2 participants