docs: library comments say what the scanner and resolver do, not how they came to be (#6277)
release / govulncheck (push) Successful in 20s
release / web (push) Successful in 1m20s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 5m15s
release / android (push) Successful in 5m26s
release / Build signed APK (releases and dev) (push) Successful in 5m45s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped

Rewrites the internal/library comment blocks to rule 32. Task, milestone,
decision and rule ids are gone; the incidents behind them are restated as
the constraint they left (why a file is deleted before its row, why the
missing mark is a mark, why duplicates block on duration). Counts and
album names from one library are dropped from production comments; test
comments keep the names their fixtures use. The tagReadVersion log keeps
each version's before and after, without the measurements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 12:27:34 -04:00
co-authored by Claude Opus 5.5
parent 08a2c06b85
commit 7e6228c13f
66 changed files with 578 additions and 564 deletions
+23 -25
View File
@@ -22,14 +22,13 @@ type trackReconciler interface {
InsertLibraryChanges(ctx context.Context, arg dbq.InsertLibraryChangesParams) error
}
// Reconcile marks tracks whose files have disappeared (#2523).
// Reconcile marks tracks whose files have disappeared.
//
// Why this exists: nothing in Minstrel used to notice a deleted file. The walk
// only visits paths that exist, so a row whose file is gone was never scanned,
// never errored, never counted — permanently invisible. The watcher ignores
// removals by design (see classifyEvent), and the safety-net scan is the same
// walk, so it covers additions only. Rows accumulated forever, kept being
// offered to recommendations, and failed at playback.
// The walk only visits paths that exist, so without this a row whose file is
// gone would never be scanned, never error, never be counted. The watcher
// ignores removals by design (see classifyEvent), and the safety-net scan is
// the same walk, so it covers additions only. Unmarked, such rows would keep
// being offered to recommendations and fail at playback.
//
// Why it MARKS rather than deletes: a missing file is a claim about the
// filesystem, and the filesystem lies transiently — an unmounted volume, a
@@ -62,9 +61,9 @@ func (s *Scanner) reconcileMissing(
if err := s.verifyRootsPresent(); err != nil {
return err
}
// Roots resolved but the walk found nothing. Either the library is genuinely
// empty — in which case there is nothing to reconcile — or the mount is
// hollow. Both mean: don't act.
// Roots resolved but the walk found nothing. Either the library is
// genuinely empty — in which case there is nothing to reconcile — or the
// mount is hollow. Both mean: don't act.
if len(seen) == 0 {
return errors.New("walk saw no audio files; refusing to reconcile")
}
@@ -123,9 +122,8 @@ func (s *Scanner) reconcileMissing(
return fmt.Errorf("mark tracks missing: %w", err)
}
stats.Missing = int(n)
// Warn, not Info: every one of these is a library entry the operator
// probably didn't intend to lose, and the only place it surfaces today is
// this line.
// Warn, not Info: every one of these is a library entry an admin probably
// didn't intend to lose.
s.logger.Warn("library scan: tracks marked missing (files not found)",
"count", n, "library_total", len(rows))
// Admins are told after the scan has adopted moves and folded
@@ -135,13 +133,13 @@ func (s *Scanner) reconcileMissing(
}
// logTrackChanges tells the delta sync that these tracks changed, so clients
// pick up the missing mark (#2704).
// pick up the missing mark.
//
// Without this the mark was invisible to every client: MarkTracksMissing and
// ClearTracksMissing are plain UPDATEs, and /api/library/sync is a change-log
// feed — a row that never produces a change is never re-sent, so a client
// would keep its stale copy until an unrelated edit touched the track or the
// cursor fell out of the retention window.
// Without this the mark would be invisible to every client: MarkTracksMissing
// and ClearTracksMissing are plain UPDATEs, and /api/library/sync is a
// change-log feed — a row that never produces a change is never re-sent, so a
// client would keep its stale copy until an unrelated edit touched the track or
// the cursor fell out of the retention window.
//
// Logged BEFORE the mutation, deliberately, which is the opposite of the
// scanner's log-after-success pattern. The two failure modes are not
@@ -161,7 +159,7 @@ func logTrackChanges(ctx context.Context, q trackReconciler, ids []pgtype.UUID)
}
// OpUpsert, not OpDelete: the track still exists and keeps its history —
// only its playability changed. A delete would tell clients to drop the
// row, which is the behaviour #2704 deliberately rejected.
// row, and a missing file is expected to come back.
return q.InsertLibraryChanges(ctx, dbq.InsertLibraryChangesParams{
EntityType: string(syncpkg.EntityTrack),
EntityIds: strIDs,
@@ -169,11 +167,11 @@ func logTrackChanges(ctx context.Context, q trackReconciler, ids []pgtype.UUID)
})
}
// verifyRootsPresent is the first and most important guard. If a configured root
// doesn't resolve to a readable directory, the walk beneath it found nothing and
// every row under it would look deleted. An unmounted media volume is the
// obvious case, and it is common enough — a container restart racing its volume
// mount does exactly this.
// verifyRootsPresent is the first and most important guard. If a configured
// root doesn't resolve to a readable directory, the walk beneath it found
// nothing and every row under it would look deleted. An unmounted media volume
// is the obvious case, and it is common enough — a container restart racing its
// volume mount does exactly this.
func (s *Scanner) verifyRootsPresent() error {
roots := s.Roots()
if len(roots) == 0 {