Skip to content

Classify Engine-added playlists as external edits, not corruption (W3) - #10

Merged
jrgutier merged 1 commit into
mainfrom
feat/external-edit-classifier
Aug 1, 2026
Merged

Classify Engine-added playlists as external edits, not corruption (W3)#10
jrgutier merged 1 commit into
mainfrom
feat/external-edit-classifier

Conversation

@jrgutier

@jrgutier jrgutier commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Implements W3 from .omc/plans/playlist-phantom-entry-root-cause.md. Independent of #9 — nothing here reads a fingerprint or journal, so the two can merge in either order.

Why

verify reported a database Engine DJ had legitimately modified exactly the way it reports corruption:

track library: playlist_count: expected=45 actual=48

Measured on a real 3,673-track stick this session, with the conversion held identical and only Engine's desktop library varied:

Engine desktop library Playlists after opening Engine Rows not carrying our epoch
Populated 48 (3 added) 6
Cleared 45 (none added, no row changed) 0

Those extras were Engine syncing its own desktop library — a legitimate feature. Reporting it identically to a corrupt write trains the operator to ignore verify, which is the failure mode this whole line of work exists to prevent.

The discriminator, and why it isn't the id watermark

The plan originally proposed classifying by id watermark — we allocate Playlist.id contiguously 1..N, so anything above N is external. Real hardware disproved that: after the desktop merge, ids ran to 84 against our contiguous 1..45. Engine reassigns ids, it does not merely append.

writer/playlists.py pins lastEditTime to 1970-01-01 00:00:00 on every playlist row it writes. So:

a row whose lastEditTime is not that epoch was not written by rb2engine

and that survives id reassignment. The constant is now public (PINNED_LAST_EDIT_TIME) and imported by verify rather than duplicated as a magic string — re-deriving writer behaviour by hand in verify is what produced the pairing defects in #3.

beyond_watermark is computed for display corroboration only and never gates classification.

Behaviour

  • Unexpected playlists carrying a foreign lastEditTime are reported as informational external edits, naming each with its id and timestamp.
  • Exit code is untouched. ok still counts only discrepancies; the plan's caveat stands, since an Engine in-place migration rewriting every row would otherwise mass-report.
  • playlist_count compares db_count - len(external_edits), so a merge fully explained by Engine emits no discrepancy — while an impostor carrying our epoch still lands in the count.
  • Output ends with the remedy: your conversion is not corrupt; re-run convert to make the stick match rekordbox exactly. Consistent with the troubleshooting section shipped in Document what Engine DJ does to a converted library #7.

No laundering — the tests that matter

All five were confirmed to fail against unmodified source before the change:

  • extra playlist with a real lastEditTime → external edit, exit code unchanged
  • extra playlist carrying our epoch → still a real playlist_count discrepancy
  • wrong membership under our epoch → still playlist[...].track_order
  • id-reassignment trap: every id shifted +100 with pinned epochs → zero external reports, ok=True
  • mixed case: one genuine external + one impostor → external named, discrepancy kept

Verification

709 tests (+5), coverage 88.22% (floor 85), ruff + mypy clean. The 0.4.0 gates (G1d in reader/pdb.py, the pre-publish oracle in writer/build.py) are untouched and their tests pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V3aF4y4GdsuJta2bfYUZ8w

verify used to report an Engine DJ desktop-library merge with the same
playlist_count discrepancy as a corrupt conversion (measured 2026-07-31:
a clean 45-playlist stick became "expected=45 actual=48" after merely
opening Engine DJ with a populated desktop library). Training users to
read a legitimate Engine feature as corruption teaches them to ignore
verify.

The discriminator is lastEditTime, now exported from the writer as
PINNED_LAST_EDIT_TIME: every playlist row we write carries exactly
1970-01-01 00:00:00, so an extra playlist with any other value was
written by Engine DJ. Verify names those rows in an informational
"External edits" section — telling the user the conversion is not
corrupt and to re-run convert for an exact rekordbox match — and
excludes them from playlist_count, so the exit code stays 0.

Deliberately NOT keyed on the Playlist.id watermark: the same experiment
showed Engine reassigns ids across a merge (ids ran to 84 against our
contiguous 1..45), so id position alone would mass-misclassify our own
renumbered rows. max(id) survives only as corroborating detail per entry.

The classifier cannot launder defects: an extra playlist carrying our
pinned epoch, or one of ours with wrong membership, still reports at
full severity — covered by tests, alongside the id-reassignment trap.
Ships informational (never exit 1) until an Engine in-place migration is
confirmed to preserve lastEditTime; a migration rewriting every row
would otherwise mass-report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3aF4y4GdsuJta2bfYUZ8w
@jrgutier
jrgutier force-pushed the feat/external-edit-classifier branch from 40aba59 to 334897d Compare August 1, 2026 05:08
@jrgutier
jrgutier merged commit 18ceca6 into main Aug 1, 2026
9 checks passed
@jrgutier
jrgutier deleted the feat/external-edit-classifier branch August 1, 2026 05:23
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