docs: record phase 4, and the counts a new guard has to agree with
Build & publish Arch package / arch-package (push) Successful in 2m2s
CI / check (push) Successful in 2m47s
Search index maintenance / maintain-index (push) Successful in 8s
CI / e2e (push) Successful in 6m11s

Plan 008 is complete and moves to completed/. The two findings worth
carrying forward are that a new table needs one schema file rather than
two (and a datamap entry, which is a gate nobody remembers), and that
excluding a path has to reach every place that counts what is in the
library — the soft scan's disk-vs-database comparison above all, which
would otherwise have rescanned the whole library on every launch with
nothing failing anywhere.
This commit is contained in:
2026-08-13 13:30:37 -04:00
parent 41a4dd7148
commit d33dfb2264
4 changed files with 203 additions and 4 deletions
+67
View File
@@ -2129,3 +2129,70 @@ the flow moves every cell on the playing row and nothing else. A
`::before` triangle in the 8px left padding costs no layout, and both
tiers assert it is *absent* on the other rows — a marker that renders
everywhere satisfies "the playing row has one" for free.
## A guard is only a feature if everything that counts agrees with it
Plan 008 phase 4: "remove from library" — the row goes, the path is
excluded from future scans, the file is never touched — and with it
`tracklist.delete`, which had been advertised in Settings for six
phases with nothing on the other end of it.
The generalisation: **an operation that changes what counts as "in the
library" has to be applied everywhere that number is computed, and the
places that compute it do not look like the feature.** The scan walk is
the obvious one, and skipping an excluded path there is the whole
feature as written in the plan. But the *startup soft scan* decides
whether to scan at all by comparing files on disk against rows in the
database, and an excluded path is on disk and deliberately not a row —
so the fix as specified would have left the two counts disagreeing
forever and queued a full scan of the entire library on **every
launch**. Nothing fails, nothing renders differently, and no tier looks
at it; the app is just permanently rescanning. The same shape one step
over: deleting an `audio_files` row cascades to `queue_tracks`, so the
queue's in-memory copy — and the playing track — goes stale unless the
removal calls the reload hook `RemoveLibrary` has had all along.
Six more things worth keeping:
- **A new table needs one schema file, and the two-file discipline is
not about it.** `applySchema` runs every file in `sql/schemas/` on
every open, so `CREATE TABLE IF NOT EXISTS` reaches an existing
install verbatim. The migration the plan asked for would have been a
*second description of the same table*, which is precisely what tore
out the old 48-step chain. Column order and "no index on a migrated
column" are rules about `ALTER TABLE ADD COLUMN`, and neither applies
when nothing is being altered.
- **The repo asked the question the plan did not.** `backend/datamap`
failed the build twice for the new table: once for having no entry at
all, then again because an *authored* table that cascades needs an
argued exemption rather than a default. Two gates, both right, and
neither in `references/schema-change.md` until now. A catalogue that
fails the build is worth more than a catalogue that is accurate.
- **Reversibility is a claim until something implements it.** The
decision picked shape A over deleting the file partly because it is
reversible — and nothing in the plan made it so. An exclusion with no
UI to clear it is a one-way door with the file sitting on disk the
whole time. A full rescan clears the table, which is the escape hatch
until there is a list to manage, and it is now written down instead
of assumed.
- **The copy was wrong for the case it will be used in most.** The
confirmation's message and impact were written for a multi-select and
used for both, so removing one track said "**They** are removed" under
a singular title. Nothing failed. Read in the first screenshot of the
dialog — sixth regression in four plans that only a PNG has caught,
and the one where it mattered most, since the copy is the only thing
standing between this feature and a user's music.
- **Both halves of a guard need their own test, or one of them is
decorative.** The walk's exclusion and the survey's exclusion are two
lines in two functions; neutering each in turn failed exactly one
test. Had they shared a test, either could have rotted invisibly.
Same reason the e2e case asserts a *control* path still returns from
the same scan: a guard that excluded everything passes "the removed
path did not come back" for free.
- **A Playwright hook gets 30 seconds regardless of the test's
timeout.** A `db/restore` in `afterAll` passed in isolation and timed
out in the full suite, where earlier specs have staged an explore
catalog and the copy takes longer. `test.setTimeout()` *inside the
hook* is what raises it — and a spec that spends the shared database
has to give it back, since the 90 specs share one backend in file
order.
@@ -1,7 +1,7 @@
# 008 — The last audit, and the one binding that outlived six phases
**Status:** active — Phases 1, 2 and 3 shipped. Phase 4 is all that
remains, and `a11y.md` is closed.
**Status:** complete — all four phases shipped. `a11y.md` is closed,
and with it all four audits from 2026-08-11.
**Branch:** main
**Created:** 2026-08-12
**Follows:** 007-ui-reconciliation
@@ -561,6 +561,77 @@ A Go test that a removed path survives a rescan; an e2e case that the
row is gone, the dialog said so, and the file still exists. Both halves
matter — the second is the promise the copy makes.
### Phase 4 — what actually shipped
Three landings, in the order the plan proposed, each watched failing on
the pre-fix build by neutering one line rather than stashing.
- **The schema, `RemoveFromLibrary`, and the scanner honouring the
list.** `excluded_paths` (one file — see below), rows deleted the way
the scan's own orphan cleanup deletes them, `TracksRemovedFromLibrary`
carrying `{filePaths, count}`, and both of the scanner's walks taking
the exclusion set.
- **The context-menu command**, behind `confirmAction()` with an impact
line that says the files are not deleted, plus `library-store`
splicing rather than invalidating.
- **`tracklist.delete`**, bound to opening that dialog, and the e2e
case.
Pinned by `remove_tracks_test.go` (6), `library-store.test.ts` (+4),
`keyboard-shortcuts.test.ts` (+1) and
`e2e/specs/remove-from-library.spec.ts` (2). `make ui-test` 672 →
**677**; `make e2e` 88 → **90**.
#### Where the plan was wrong — Phase 4
Six things, and the first two are the plan asking for work that does
not exist and skipping work that does.
- **"Following the two-file schema discipline" is wrong for a new
table.** `applySchema` runs every file in `sql/schemas/` on every
open, so a `CREATE TABLE IF NOT EXISTS` reaches an existing install
verbatim; the migration file the plan asked for would have been a
*second description of the same table*, which is the one thing the
checklist's third rule forbids. Column order and "no index on a
migrated column" do not apply either — nothing is being added to an
existing table, so the index lives beside its own `CREATE TABLE`.
- **The half that would have undone the feature is not in the plan.**
The startup soft scan decides "library unchanged" by comparing files
on disk against rows in the database. An excluded path is on disk and
deliberately not a row, so the two counts disagree *forever* and
every launch queues a full scan of the whole library. Both walks take
the exclusion set now. Nothing in any tier would have caught it: it
is not a wrong answer, it is a permanent, invisible re-scan.
- **…and neither is the queue.** Deleting an `audio_files` row cascades
to `queue_tracks`, so the queue's in-memory copy — and possibly the
playing track — goes stale. `RemoveLibrary` has had the
`CompactQueue` hook for exactly this since it was written; the
removal reuses it.
- **The plan says nothing about undo, and the operation needs one.** An
exclusion with no UI to clear it is a one-way door: the file is on
disk and the user cannot get it back. A full rescan clears the table,
which is the escape hatch until there is a list to manage. Recorded
rather than implied, because it is the difference between
"reversible" (shape A's stated advantage) and a claim.
- **A new table has a second gate nobody remembers.**
`backend/datamap` catalogues every table's Kind and Lifetime, and two
of its tests fail on a new one: `TestCatalogCoversSchema` for the
missing entry, then `TestAuthoredCascadesAreDeliberate` because an
*authored* table that cascades needs an argued exemption. Both are
right to ask; neither is mentioned in `references/schema-change.md`.
- **The copy was wrong in the first screenshot, and only there.** The
title was singular and the body said "**They** are removed" — the
message and impact strings were written for the multi-select case and
used for both. Nothing failed. Found by reading the PNG, which is now
the sixth regression in four plans that only a PNG has caught.
And one about the harness rather than the work: **a hook gets 30
seconds, not the test's timeout.** The e2e case's `afterAll` restore
passed in isolation and timed out in the full suite, where earlier
specs have staged an explore catalog and the restore takes longer than
the hook's default budget. `test.setTimeout()` inside the hook is what
raises it.
---
## Deliberately not in this plan