Skip to content

S10.1 — Playlist/queue persistence spine (schema v3 + DAO + Gate-1 closure)#53

Merged
ramith merged 6 commits into
mainfrom
sprint/s10-1-playlist-queue-store
Jul 14, 2026
Merged

S10.1 — Playlist/queue persistence spine (schema v3 + DAO + Gate-1 closure)#53
ramith merged 6 commits into
mainfrom
sprint/s10-1-playlist-queue-store

Conversation

@ramith

@ramith ramith commented Jul 13, 2026

Copy link
Copy Markdown
Owner

First sprint of the S10 series (see s10 plan). Store spine only — no UI (queue/playlist UX is S10.2/S10.3). Design + founder-brainstorm decisions in s10-1-playlist-queue-store-design.md.

What it adds

  • Schema v3: playlists + playlist_entries (ordered, own entry id so a track can repeat; UNIQUE(name) WHERE is_builtin=0; single-builtin partial index). migrateV2toV3 seeds the built-in "current" queue playlist.
  • Playlist DAO (LibraryStore+Playlists): create / untitled (Apple-style lowest-unused) / rename / delete (built-in immutable); ordered append / appendEntries / insert-at / remove / removeEntries / reorder; addLooseFileToPlaylist; idempotent builtin bootstrap. Typed PlaylistNameConflict / PlaylistMutationError. Rides GRDB single-writer transactions.
  • Closes Gate-1 (SEQ-1): unreferencedTrackIDs filters against playlist_entries, so removeRoot spares playlist-referenced tracks (kept loose). File-gone → CASCADE drop (founder decision §0.2).

Founder-brainstorm decisions (locked)

Durability kept simple / deferred (eraseOnSchemaChange stays true; logged as DUR-1, a hard gate before R1); file-gone → remove; queue = built-in "current"; duplicate names prevented; Apple-style default names.

QA

  • VerifyLibraryStore 102/102 (20 pl-* checks incl. Gate-1 keep/sweep pair, same-track-twice, reorder, insert-at, remove, loose-add, persistence).
  • qa-expert + the-fool break-it pass found 6 real defects behind the initial green gate — all fixed with fix-covering checks: D1 partial-reorder scramble, D2 loose-add detaching a folder track, D3 unscoped delete, D4 raw-error→typed, D5/D6 name validation. D7 (sweepOrphans partial-rescan) confirmed + deferred with DUR-1.
  • make gate + make strict-gate green.

Not merged blockers, but note: DUR-1 must be resolved before R1 ships playlists.

ramith added 6 commits July 13, 2026 09:46
Founder decision: make the S10 chunks individual done-done sprints. Sub-numbered
S10.1-S10.5 (not flat S10/S11/...) to avoid renumbering S11-S18 and breaking the
R1/R2/R3-after-S10/S14/S17 anchors.

- sprint-plan.md: S10 row -> five S10.x rows (S10.1 spine 8sp / S10.2 queue UX 6 /
  S10.3 playlists+M3U 9 / S10.4 macOS control 5 / S10.5 browse polish 3). Status,
  critical path, Phase-1 total (~95sp), R1 milestone all updated to S10.1-S10.4.
- s10-queue-playlists-macos-plan.md: reframed chunks -> five sprints (+SP, per-sprint DoD).
- roadmap.md: S10 row relabeled 'runs as S10.1-S10.5'; R1/critical-path anchors updated.
…l gate)

Design phase for S10.1 (first S10 sprint). BA + swift-expert + qa-expert design,
then architect + the-fool gate (GO-WITH-CHANGES). Must-fixes folded in:
- sweepOrphans symmetric fix (gate's top catch): the per-scan orphan sweep must
  spare playlist-referenced tracks too, not just removeRoot — else a partial
  rescan silently CASCADE-drops playlist entries.
- durability: eraseDatabaseOnSchemaChange=false UNIFORM (not DEBUG-only) +
  make reset-db + additive v3 migration + migration-immutability gate check.
- defer builtin 'current' seed out of the v3 migration to a lazy bootstrap at S10.2.
- position = non-contiguous ordering key (reconciled swift-vs-QA contradiction).
- UNIQUE(name) scoped WHERE is_builtin=0.

Pending FOUNDER BRAINSTORM before implementation (per the dev process).
Founder decisions locked:
1. Durability = keep it simple, DEFERRED — eraseDatabaseOnSchemaChange stays true;
   pre-R1 a schema change drops-and-recreates (playlists included). Real durability
   is a new deferred known-issue. (Drops migration-guard/reset-db machinery.)
2. File gone from disk -> removed from playlists (track_id CASCADE; sweep drops).
   removeRoot still spares referenced tracks as loose (locked EP-LIBRARY rule).
3. Queue = built-in 'current' playlist (seeded in v3 migration).
4. Duplicate names PREVENTED (UNIQUE(name) WHERE is_builtin=0 + typed reject).
5. Default name Apple-style ('New Playlist N', lowest-unused).

Ready to implement.
…-1 closure)

Implements the S10.1 store spine (design s10-1-playlist-queue-store-design.md):
- Schema v3: playlists + playlist_entries tables (ordered, own entry id so a track
  can repeat; UNIQUE(name) WHERE is_builtin=0; single-builtin partial index).
  migrateV2toV3 seeds the built-in 'current' queue playlist. eraseOnSchemaChange
  stays true (durability deferred, founder decision §0.1).
- LibraryStore+Playlists: create/untitled(Apple-style lowest-unused)/rename/delete
  (built-in immutable), ordered append/appendEntries/insert-at/remove/removeEntries/
  reorder, addLooseFileToPlaylist (folder_id NULL), idempotent builtin bootstrap.
  Typed PlaylistNameConflict / PlaylistMutationError. Rides GRDB single-writer txns.
- Gate-1 (SEQ-1) CLOSED: unreferencedTrackIDs filters against playlist_entries, so
  removeRoot spares playlist-referenced tracks (kept loose). File-gone -> CASCADE drop.
- VerifyLibraryStore: 14 pl-* checks incl. the Gate-1 keep/sweep pair + same-track-twice
  + reorder + insert-at + remove + loose-add + dup-name-reject + persistence. Updated
  FTS/migration checks + fullMigrator for v3. make gate 96/96; make strict-gate green.

Next: qa-expert + the-fool break-it pass, then founder manual testing + retro.
… R1 gate)

- SEQ-1: Gate 1 CLOSED by S10.1 (unreferencedTrackIDs now filters playlist_entries;
  proven by pl-gate1-* checks). Both SEQ-1 gates now closed.
- DUR-1 (new): playlist durability across schema change is deferred (founder §0.1,
  keep-it-simple pre-release) but flagged as a HARD GATE that MUST be resolved
  before R1 ships playlists — eraseOnSchemaChange + tracks.id churn would corrupt
  playlists. Logged so the deferral isn't forgotten (the-fool tripwire).
…pass

The green gate had false confidence (checks only hit happy paths). Break-it pass found:
- D1 (correctness): reorderPlaylist with a partial list collided positions -> silent
  scramble. Now renumbers the WHOLE playlist (supplied order + omitted appended;
  foreign ids ignored) — like insertEntry.
- D2 (correctness): addLooseFileToPlaylist of an already-library URL re-upserted it,
  nulling folder_id/relative_path + resetting metadata_scanned. Now reuses the
  existing row; only a new URL inserts a loose one.
- D3 (data-loss footgun): removeEntry/removeEntries weren't playlist-scoped -> a
  foreign entry id deleted from another playlist. Added playlistID + AND playlist_id=?.
- D4 (robustness): appending to a deleted/absent playlist leaked a raw GRDB FK error
  -> now typed PlaylistMutationError.notFound (playlist-exists pre-check).
- D5/D6 (robustness): a user playlist named 'current' (any case) and empty/whitespace
  names were accepted -> rejected via validatedUserName + .invalidName.
- D7 (sweepOrphans partial-rescan): confirmed, consistent with locked §0.2; deferred
  (logged as design §5 note + DUR-1).

Added 6 fix-covering pl-* checks (partial-reorder, loose-add-existing, remove-foreign,
append-notfound, name-validation, remove-entries). VerifyLibraryStore 102/102;
make gate + make strict-gate green.
@ramith
ramith merged commit e4a2e83 into main Jul 14, 2026
1 check passed
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