Skip to content

Record which bytes every m.db was built from (W1) - #9

Merged
jrgutier merged 1 commit into
mainfrom
feat/source-provenance
Aug 1, 2026
Merged

Record which bytes every m.db was built from (W1)#9
jrgutier merged 1 commit into
mainfrom
feat/source-provenance

Conversation

@jrgutier

@jrgutier jrgutier commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Implements W1 from .omc/plans/playlist-phantom-entry-root-cause.md, including the exit-code partition (W1c) and the fail-loud report fallback.

Why

The July 29 incident was undiagnosable for days because nothing tied an m.db to the bytes it was built from. When convert and verify disagreed, there was no way to tell whether the database was wrong or the source had moved — the two most likely explanations demanded opposite responses, and the evidence to choose between them was destroyed by the recommended remedy (re-run convert).

This records the source fingerprint at read time and lets verify name which oracle moved.

What lands

  • Fingerprint over the bytes actually parsed. sha256 is computed over the already-read buffer in parse_export_pdb, never by re-reading — the whole point is capturing what was parsed, and a torn read must be fingerprinted as torn.
  • SourceFingerprint on SourceLibrary, optional and defaulting to None, and excluded from to_json_obj — that serialization is load-bearing for golden byte-identity.
  • Append-only JSONL journal at Engine Library/rb2engine-journal.jsonl, one line per publish (pdb sha/size/mtime, staged m.db sha, max(Playlist.id), max(PlaylistEntity.id)), capped at 64 KB by dropping oldest lines, tolerant of a truncated final line. A journal survives repeated re-converts, which single-generation rotation does not.
  • Nothing time-based inside m.db, and no clock in writer/ or mapper/. build_library produces a time-free record; the only datetime.now is in report.py, invoked from the CLI. A test pins the record's field set so a timestamp can't drift into the writer's side.
  • Fail-loud report fallback. A successful run whose report falls back to the cwd now warns loudly that no provenance travelled with the stick. This really happened during testing: a convert reported success with its drive unmounted and quietly wrote its report into the source repo.

Exit-code contract

Condition Exit
Any source-independent finding (chain break, undecodable blob) 1 — always wins
Fingerprint mismatch, no source-independent finding 3 — not attributable, "re-run convert"
Discrepancies with matching fingerprint 1
Clean 0
Cannot verify (FatalError) 2 — keeps precedence

The partition matters: a stale source must never launder a real fault into "just re-run convert". The finding text also states the irreducible residue — source-dependent corruption co-occurring with a stale source is undetectable until convert re-runs.

db_changed (recorded staged sha ≠ current m.db) is informational and never alters the exit code, because Engine DJ legitimately rewrites m.db on open — measured this session.

Judgment call worth review

Missing provenance (a 0.4.0 stick, or the off-stick report case) is a rendered, visible finding but does not change the exit code. Routing it to 3 would fail every existing stick until re-convert. Easy to tighten later if you want mismatch-equivalent treatment.

Verification

731 tests (+27), coverage 88.45% (floor 85), ruff + mypy clean, test_no_wallclock green, goldens and determinism unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V3aF4y4GdsuJta2bfYUZ8w

…oracle that moved

The phantom-entry incident could not be attributed after the fact: convert
parsed export.pdb bytes that the settled file no longer contained (torn read,
14 s after rekordbox's last write on exFAT), verify later compared against the
settled bytes, and nothing recorded which side had moved. Separately, Engine DJ
provably rewrites this stick's m.db (desktop-library merge), and one convert
"succeeded" with its drive unmounted, dropping the report — the only record —
into the source repo. This is W1/W1c of the phantom-entry plan.

Fingerprint at read time
------------------------
parse_export_pdb hashes the buffer it actually parsed — never a re-read, which
under a torn read would fingerprint the wrong bytes — and carries
SourceFingerprint (sha256/size/mtime) on SourceLibrary. The field is excluded
from to_json_obj, which is load-bearing for golden byte-identity.

Provenance at publish time
--------------------------
build_library pairs that fingerprint with the staged m.db sha256 and the dense
id watermarks (max Playlist.id / PlaylistEntity.id) on the report. The record
is deliberately time-free: writer/ sits under the no-wallclock determinism
gate, so the timestamp is added only where the journal line is written, in
report.py, called from cli.py after publish. Nothing new goes inside m.db —
a timestamp would break byte-identical rebuilds and the m.db hash cannot
contain itself.

Append-only journal beside the report
-------------------------------------
Engine Library/rb2engine-journal.jsonl gains one line per publish, capped at
64 KB by dropping oldest lines. The fixed-name report alone is not enough:
"re-run convert" is exactly the remedy verify prescribes on staleness, and it
overwrites the report — destroying the fingerprint of the suspect bytes, which
is how the original incident's evidence was lost. A successful run whose report
falls back to cwd now warns loudly that no provenance travels with the stick.

verify: attribution before accusation, exit codes partitioned
-------------------------------------------------------------
verify compares the recorded fingerprint against the source it just parsed and
reports source changed / db changed / both / neither as top-level provenance
findings, never as playlist discrepancies. Exit contract:

  0 ok / 1 discrepancies / 2 cannot-verify / 3 not attributable

Source-independent findings (broken nextEntityId chains, undecodable blobs)
always exit 1 — a stale source must not launder a real defect into "re-run
convert". Source-dependent comparisons under a fingerprint mismatch become
informational with exit 3, whose finding text also states the irreducible
residue: corruption co-occurring with a changed source is undetectable until
convert re-runs. "db changed" is classified, not alarmed — Engine rewriting
m.db is legitimate. A missing record (0.4.0 stick, off-stick report) is its own
visible finding and degrades gracefully; exit 2 keeps precedence over both.

Tests
-----
27 new tests: the fingerprint is proven to hash the parsed buffer against a
file that settles underneath it; golden serialization unchanged; journal
append/cap/truncation; the co-occurrence cases that decide whether exit 3 can
swallow a defect (chain fork and garbage blob under a stale fingerprint both
force 1); missing/unreadable provenance reported without a crash; the CLI
journal write and the off-stick warning. 731 passing (704 before), coverage
88%, ruff and mypy clean; test_no_wallclock still holds — the clock lives
outside writer/ and mapper/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3aF4y4GdsuJta2bfYUZ8w
@jrgutier
jrgutier merged commit d163365 into main Aug 1, 2026
9 checks passed
@jrgutier
jrgutier deleted the feat/source-provenance branch August 1, 2026 05:07
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.

1 participant