docs(13-07): summary — music index + music.* surface complete, 23/23 green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-04 11:15:41 -04:00
co-authored by Claude Fable 5
parent d3b0ed8a1d
commit 17da7a7233
@@ -0,0 +1,161 @@
---
phase: 13-aiui-functional-conversational-node-control-and-content-surf
plan: 07
subsystem: music
tags: [music-library, index, rpc, lofty, incremental-refresh, atomic-write, rust]
# Dependency graph
requires:
- phase: 13-04
provides: "13-MUSIC-MODEL.md (D-13 decision), music/mod.rs entity types, music/tags.rs extract_tags with media-root confinement, lofty 0.24.0"
provides:
- "core/archipelago/src/music/index.rs — MusicIndex: reindex, refresh_incremental, load (NewerSchema refusal), save_atomic (temp+fsync+rename), group_albums/group_artists, ReindexState guard, ScanStats"
- "core/archipelago/src/music/mod.rs — media_roots(Config) (filebrowser/Music + purchased-content), LibrarySnapshot"
- "music.* RPC surface: music.list-albums, music.list-artists, music.list-tracks, music.status, music.reindex — one dispatcher.rs prefix arm"
affects: [13-11, 13-12, song-grid, peer-audio]
# Tech tracking
tech-stack:
added: []
patterns:
- "Atomic index persistence: serialize to sibling temp + fsync + rename — concurrent readers see complete old or complete new, never partial"
- "Forward-version refusal: load returns a distinct NewerSchema error and never overwrites the newer file; explicit reindex is the only rebuild path"
- "Scan guard: AtomicBool + RAII drop-release; duplicate scans report already-running with last stats instead of queueing"
- "RPC testability without RpcHandler: module-level dispatch fn parameterized on (state, data_dir, roots), thin RpcHandler method delegating"
key-files:
created:
- core/archipelago/src/music/index.rs
- core/archipelago/src/api/rpc/music.rs
modified:
- core/archipelago/src/music/mod.rs
- core/archipelago/src/api/rpc/dispatcher.rs
- core/archipelago/src/api/rpc/mod.rs
key-decisions:
- "media_roots = [data_dir/filebrowser/Music, data_dir/purchased-content] — both D-13 sources as local filesystem roots; per-root MusicSource assignment (purchased-content files map to Peer{onion} from their first path component)"
- "music.reindex accepts optional incremental:true routing to refresh_incremental, so the stays-fresh truth has a production caller (default remains the on-demand full rebuild)"
- "Albums ordered by (album_artist, album, min track year); tracks by (disc, track number, title); TrackId (source, path) is the final tiebreak everywhere"
patterns-established:
- "T-13-42 mitigation shape: never write the index in place — save_atomic is the only writer"
- "T-13-39 enforced twice: walker skips symlinks whose canonical target escapes the canonical root, and extract_tags re-checks confinement per file"
requirements-completed: [] # AIUI-03 spans multiple plans (13-11 SongGrid wiring still pending)
# Metrics
duration: ~3h45m wall clock (dominated by four full non-incremental archipelago compiles on the shared 4-core box; one 10-minute foreground cargo run was killed by the harness timeout and rerun)
completed: 2026-08-04
status: complete
---
# Phase 13 Plan 07: Music Library Index and music.* RPC Surface Summary
**Persisted, incrementally-refreshed music library over the D-13 model — atomic JSON index with schema-version refusal, symlink-confined scanning, derived album/artist grouping, and an authenticated five-method `music.*` surface behind a single dispatcher arm — 23/23 `music::` tests green.**
## Performance
- **Duration:** ~3h45m wall clock (~25 min of it authoring; the rest cargo compile time — four full `CARGO_INCREMENTAL=0` builds of the archipelago crate at ~10-14 min each on the live-node box)
- **Started:** 2026-08-04T11:27Z
- **Completed:** 2026-08-04T15:15Z
- **Tasks:** 2 (both auto/TDD)
- **Files modified:** 5
## Accomplishments
- `music/index.rs`: `reindex` walks the media roots, extracts tags per audio file via 13-04's `extract_tags`, and persists `data_dir/music/index.json`; `refresh_incremental` diffs `(path, mtime, size)` so unchanged files are never re-extracted, removes rows for vanished files (derived albums vanish with their last track), and preserves the lazily-backfilled `content_hash` column on unchanged rows.
- `save_atomic` (temp sibling + fsync + rename) makes the concurrent-read truth hold: a reader sees the complete previous index or the complete new one, never a torn file — verified by a 100-writer/200-reader interleaving test.
- `load` refuses `schema_version > MUSIC_SCHEMA_VERSION` with a distinct `NewerSchema` error and never overwrites the newer file; readers serve an empty library, and only an explicit reindex rebuilds (13-MUSIC-MODEL.md's downgrade contract, T-13-43).
- Symlinks whose canonical target escapes the canonical media roots are skipped, not followed (T-13-39) — confinement enforced in the walker *and* re-checked per file inside `extract_tags`.
- One comparator everywhere: albums by (album artist, album title, min year), tracks by (disc, track, title), with the `(source, path)` identity as final tiebreak — list ordering is stable across repeated calls.
- `music.*` RPC surface: five methods behind exactly one `m if m.starts_with("music.")` dispatcher arm (the 13-01 `assistant.` pattern), placed adjacent to the `content.*` block; envelopes carry `total` + `scanned_at`; `list-tracks` clamps `limit` to [1,500] (default 100); `music.reindex` spawns and returns immediately, refusing to duplicate a running scan (T-13-41).
- Nothing `music.*` in `UNAUTHENTICATED_METHODS` — the surface rides the existing session/CSRF/RBAC gate (T-13-40), asserted by `music_methods_require_session`.
## Task Commits
1. **Task 1: The index — scan, group, persist, stay fresh**`49687f7e` (feat) — `music/index.rs` + `music/mod.rs` (`media_roots`, `LibrarySnapshot`); 9 tests, one per behavior bullet
2. **Task 2: The music.* RPC surface**`d3b0ed8a` (feat) — `api/rpc/music.rs`, single dispatcher arm, `mod music;` declaration; 7 tests
## Files Created/Modified
- `core/archipelago/src/music/index.rs``MusicIndex`, `IndexEntry`, `ScanStats`, `IndexError::NewerSchema`, `reindex`, `refresh_incremental`, `load`, `save_atomic`, `group_albums`, `group_artists`, `ReindexState`/`ReindexGuard`, `shared_state`, `INDEX_FILENAME`
- `core/archipelago/src/music/mod.rs``pub mod index;`, `media_roots(&Config)`, `LibrarySnapshot`
- `core/archipelago/src/api/rpc/music.rs``handle_music` prefix sub-dispatcher + `handle_music_list_albums` / `list_artists` / `list_tracks` / `status` / `reindex`
- `core/archipelago/src/api/rpc/dispatcher.rs` — one `music.` prefix arm (the only registration point)
- `core/archipelago/src/api/rpc/mod.rs``mod music;` declaration (alphabetical block)
## Decisions Made
- **Media roots**: `filebrowser/Music` (OwnLibrary — peer audio purchases are also auto-filed here by `content.*`'s paid-download path, so they enter the library with real filenames) plus `purchased-content` (Peer byte cache; files under it map to `MusicSource::Peer{onion}` from their first path component). Note: purchased-content stores files extensionless today, so that root yields no tracks until files there carry audio extensions — peer catalog surfacing in `SongGrid` is 13-11's job via the existing `content.*` discovery, per 13-MUSIC-MODEL.md ("reuses content.*'s existing peer-audio discovery").
- **Grouping field names spot-checked against 13-MUSIC-MODEL.md** (plan acceptance): row key `TrackId{source, path}` (hybrid-identity) ✓; `content_hash` lazily-backfilled column preserved on unchanged rows, reset when bytes change ✓; albums derived at read time on `AlbumId{album_artist, album}`, artists on the `artist` tag — never persisted ✓; index at `data_dir/music/index.json`, pretty-printed JSON with top-level `schema_version` ✓; newer-version index treated as absent and never overwritten ✓.
- `MusicIndex::empty()` populates `scanned_at` with "now", so even a never-scanned library serves a timestamp — empty arrays + timestamp, never null, never an error.
## Deviations from Plan
### Auto-fixed / necessary additions
**1. [Rule 3 - Blocking] `mod music;` added to `api/rpc/mod.rs`**
- **Found during:** Task 2
- **Issue:** The plan's `files_modified` lists only `music.rs` + `dispatcher.rs`, but a new module must be declared in its parent.
- **Fix:** One line in the alphabetical `mod` block (between `monitoring` and `names`).
- **Commit:** `d3b0ed8a`
**2. [Rule 2 - Missing functionality] `music.reindex` gained an optional `incremental: true` param**
- **Found during:** Task 2 (`cargo build` bin target flagged `refresh_incremental`/`ScanMode::Incremental` as dead code — the freshness machinery had no production caller, making the "stays fresh without a full rebuild" truth test-only)
- **Fix:** `music.reindex {"incremental": true}` routes to `refresh_incremental`; default stays the on-demand full rebuild. Covered by `reindex_incremental_mode_refreshes_without_full_reextraction`. Also removed all three new dead-code warnings (bin builds with zero warnings from music files).
- **Commit:** `d3b0ed8a`
**3. [Rule 1 - Bug] `anyhow!` format-string brace escape**
- **Found during:** Task 2 first compile — `"expected { album, album_artist }"` parsed as format args.
- **Fix:** escaped to `{{ album, album_artist }}`. Caught before any commit.
### Process notes
- **TDD gate:** tests were authored first (one per behavior bullet) but each task landed as a single green commit rather than separate RED `test(...)` + GREEN `feat(...)` commits — this repo's CLAUDE.md hard rule ("commit each feature the moment it works — it compiles and its targeted tests pass") takes precedence over the generic RED-commit convention, matching how 13-04 Task 3 executed. See TDD Gate Compliance below.
- **Fixture reuse:** the compact FLAC byte-builder is duplicated into `index.rs` and `api/rpc/music.rs` test modules rather than refactoring `tags.rs`'s builders to be shared — `tags.rs` is outside this plan's file list. (An incidental `rustfmt` recursion into `tags.rs` was reverted to keep commits scoped.)
**Total deviations:** 2 necessary additions + 1 trivial compile fix. No scope changes.
## TDD Gate Compliance
- RED evidence exists as authored-first tests (16 new tests across the two tasks), but no standalone `test(...)` commits precede the `feat(...)` commits — CLAUDE.md's green-commit rule was applied deliberately (see Process notes). Gate sequence in git log is therefore `feat(49687f7e)``feat(d3b0ed8a)` with tests and implementation co-committed.
## Known Stubs
None — no placeholder text, no hardcoded empty values flowing to UI, no unwired data paths. (The `purchased-content` root legitimately yields zero tracks today because its files are extensionless content-ids; peer audio reaches `SongGrid` via 13-11's `content.*` path per the model doc, and audio purchases already land in `filebrowser/Music` with extensions.)
## Threat Flags
None — no security-relevant surface beyond the plan's threat model (T-13-39/40/41/42/43/44 all mitigated as specified; T-13-45 remains accepted/deferred to 13-12's `wrap_untrusted`, and nothing here places tag text in a model context).
## Issues Encountered
- **Foreground cargo timeout ceiling:** a full `CARGO_INCREMENTAL=0` test compile of the archipelago crate takes ~10-14 min on this box (live node sharing 4 cores), which exceeds the 10-minute foreground tool cap — one run was SIGTERM'd mid-compile and rerun. Workaround used thereafter: detached cargo writing to a log with a `.done` marker + foreground polling loops, keeping the session turn alive.
## Verification (plan-level)
- `CARGO_INCREMENTAL=0 cargo test --package archipelago music::`**`ok. 23 passed; 0 failed`** (9 index + 7 tags + 7 rpc)
- `CARGO_INCREMENTAL=0 cargo build --package archipelago` → exit 0 (`Finished dev profile`), zero warnings from music/rpc-music files
- `grep -c 'starts_with("music.")' dispatcher.rs`**1**
- `grep -n 'music\.' middleware.rs` → no match (nothing unauthenticated)
- `git ls-files core/archipelago | grep -ciE '\.(mp3|flac|m4a|ogg)$'` → 0 (no binary fixtures)
- Index field names match `13-MUSIC-MODEL.md` (spot check recorded under Decisions Made)
## User Setup Required
None — no external service configuration required.
## Next Phase Readiness
- 13-11 can wire `SongGrid` to `music.list-albums`/`music.list-tracks` (envelopes follow `content.*`'s items+metadata shape: arrays + `total` + `scanned_at`) and trigger `music.reindex` (full or incremental).
- No music tool was added to the assistant's curated registry — deliberately out of scope per the plan (track independence, D-13).
- T-13-45 (peer tag text as untrusted model input) remains 13-12's `wrap_untrusted` responsibility.
## Self-Check: PASSED
- FOUND: `core/archipelago/src/music/index.rs`, `core/archipelago/src/api/rpc/music.rs`, `core/archipelago/src/music/mod.rs` (updated), `.planning/phases/13-.../13-07-SUMMARY.md`
- FOUND commits: `49687f7e`, `d3b0ed8a`
---
*Phase: 13-aiui-functional-conversational-node-control-and-content-surf*
*Completed: 2026-08-04*