Skip to content

S10.3 tail: dead-file handling (F) + playlist scan/scale checks (G); folders cut#60

Merged
ramith merged 2 commits into
mainfrom
sprint/s10-3-f-dead-files
Jul 16, 2026
Merged

S10.3 tail: dead-file handling (F) + playlist scan/scale checks (G); folders cut#60
ramith merged 2 commits into
mainfrom
sprint/s10-3-f-dead-files

Conversation

@ramith

@ramith ramith commented Jul 16, 2026

Copy link
Copy Markdown
Owner

S10.3 tail — dead-file handling (F) + scan/scale checks (G)

Follow-up to #59 (S10.3 core). Completes the S10.3 scope now that playlist folders are CUT (founder, 2026-07-16 — the folder data layer from #59 is left dormant; no folders UI).

Chunk F — dead / missing-file handling

A playlist entry whose track resolves but whose file is gone from disk (a loose file deleted, or a move not yet reconciled — a deleted library track is already FK-cascade-dropped) is now handled instead of silently failing to play:

  • PlaylistDetailEntry.isAvailable = track resolved AND file exists, checked off-main (a Task.detached(.utility) fileExists loop under the existing detailEpoch guard). Play + keyboard nav skip unavailable entries (never halt).
  • Unavailable row: dimmed metadata + a trailing warning-badge Menu (Locate / Remove) — visible, click- and keyboard-reachable, plus a combined VoiceOver element with the same actions. Count line shows "· N unavailable".
  • Locate: file-importer → re-points the track via the id-preserving moveTrack (fixes every playlist referencing it at once); a URL-conflict is a per-row alert, never the pane-wide error state.
  • Remove missing (bulk): confirmation dialog (irreversible) + disabled while the library is populating (availability flickers mid-scan).

swiftui-pro reviewed; it confirmed the epoch placement, skip-on-play index mapping, and Locate id-preservation were correct, and the three MAJORs it found (bulk-remove guardrail, Locate error UX, keyboard-reachable row actions) are all fixed. Sandbox note: the app is non-sandboxed Developer-ID, so a Locate'd out-of-root file plays; a security-scoped bookmark would be needed only IF App Sandbox is later adopted.

Chunk G — playlist ⇄ scanner / scale invariants (headless)

Four new VerifyLibraryStore checks (125/125):

  • pl-move-membership-survives (US-PLIST-08): a track's file moves, the real LibraryScanner move-matches it (keeps tracks.id), and the playlist entry is still intact after reconcile.
  • pl-reorder-isolation: reordering one playlist leaves every other playlist's order untouched.
  • pl-explain-plan: playlist list + entries reads are index-driven (idx_playlist_entries_playlist), never a full playlist_entries scan (scale tripwire).
  • pl-write-during-scan: a playlist write concurrent with a real scan completes without deadlock/corruption.

Also fixes two latent Periphery findings dormant on main (an unused SongsTable.playlists env var + an unused LibraryStore import).

Docs: sprint-plan §Status + design §6 updated (S10.3 core shipped #59; folders cut; F/G).

Test plan

make strict-gate green (VerifyLibraryStore 125/125 + unit tests + Periphery + the migrator/drop-path/erase guards). Pending founder by-ear: add a song to a playlist → move/delete its file → reopen: the row badges unavailable, Play skips it, the badge menu offers Locate/Remove, and "Remove N Missing" confirms.

ramith added 2 commits July 16, 2026 22:45
…cate + remove-missing

A playlist entry whose track resolves but whose FILE is gone from disk (a loose
file deleted, or a move not yet reconciled — a deleted library track is already
FK-cascade-dropped) is now handled instead of silently failing to play.

- PlaylistDetailEntry.isAvailable = track resolved AND file exists on disk, checked
  OFF-MAIN (Task.detached .utility stat loop, Sendable capture) in loadDetail under
  the existing detailEpoch guard. resolvedEntries/playableFiles filter isAvailable →
  play SKIPS missing entries (never halts); keyboard nav skips them too.
- Unavailable row: dimmed metadata + a trailing warning-badge MENU (Locate / Remove)
  — visible, click- AND keyboard-reachable (not right-click-only) + a combined
  VoiceOver element with the same actions. Count line shows "· N unavailable".
- Locate: .fileImporter → relocateEntry → reuses the id-preserving moveTrack (every
  playlist referencing the track is fixed at once); a URL-conflict is a per-row
  alert, never the pane-wide load-error state.
- Remove-missing (bulk): confirmationDialog (irreversible, no undo) + disabled while
  the library is populating (availability flickers mid-scan).

swiftui-pro review folded in (all of the above); it confirmed the epoch placement,
skip-on-play index mapping, and Locate id-preservation were already correct. Sandbox
note: app is non-sandboxed Developer-ID, so a Locate'd out-of-root file plays; a
security-scoped bookmark would be needed IF App Sandbox is later adopted.

Also: folders (Chunk D) CUT per founder — data layer left dormant; docs (§Status +
design §6) updated. Model dead-file helpers split to PlaylistsModel+DeadFiles;
detail actions/helpers to PlaylistDetailView+Actions (type-body length).

strict-gate PASSED.
…isolation + explain + write-during-scan

Chunk G — the headless VerifyLibraryStore invariants that close the S10.3 tail:

- pl-move-membership-survives (US-PLIST-08): a track's file moves on disk, the REAL
  LibraryScanner move-matches it (keeps tracks.id), and the playlist entry (by id) is
  still intact after reconcile — membership survives a Finder move.
- pl-reorder-isolation: reordering one playlist leaves every other playlist's order
  untouched (per-playlist renumber, scoped by playlist_id).
- pl-explain-plan: the playlist list + entries reads are index-driven —
  playlist_entries is reached via idx_playlist_entries_playlist, never a full scan
  (scale tripwire; new explainPlaylistReadsPlan hook reusing collectQueryPlan).
- pl-write-during-scan: a playlist write concurrent with a real scan completes without
  deadlock/corruption — the single serialized DatabaseWriter serializes them.

VerifyLibraryStore 125/125.

Also fixed two latent Periphery findings (unused SongsTable.playlists env var +
unused LibraryStore import in ChecksMigrationConvergence — both dormant on main).

strict-gate PASSED.
@ramith
ramith merged commit 82bc287 into main Jul 16, 2026
1 check failed
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