From bca4660a9579e3b312f0a2dab3017f568a24b6ca Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 4 Aug 2026 05:08:29 -0400 Subject: [PATCH] docs(13-04): record D-13 one-way music entity model decision Task 1 checkpoint:decision resolved by operator: hybrid-identity (path row key, lazily-backfilled content-hash dedupe column), derived-albums (computed at read time from track tags, not stored rows), a single JSON index at data_dir/music/index.json matching content_server.rs's load_catalog precedent, and both own-library + peer sources indexed. MUSIC_SCHEMA_VERSION starts at 1; a newer-version index on an older binary is treated as absent rather than reinterpreted or overwritten. Co-Authored-By: Claude Fable 5 --- .../13-MUSIC-MODEL.md | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 .planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-MUSIC-MODEL.md diff --git a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-MUSIC-MODEL.md b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-MUSIC-MODEL.md new file mode 100644 index 00000000..f0ac8d2f --- /dev/null +++ b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-MUSIC-MODEL.md @@ -0,0 +1,89 @@ +# 13-04 Task 1: Music Entity Model — One-Way Decision (D-13) + +**Decided:** 2026-08-04, by the operator, at the `checkpoint:decision` gate in `13-04-PLAN.md` +Task 1. This is the one-way half of D-13: the album/artist/track entity model and its on-disk +index format, decided once, before any node indexes a library, per CONTEXT.md's own rating +("a persisted data model with a migration cost once nodes have indexed libraries; changing the +entity model afterwards needs a reindex path, not just a code change"). + +## Track identity: hybrid-identity + +A track's stable row key is `(source, canonical path)` — cheap, stat-only indexing, trivially +incremental via mtime, matching `content_server.rs::load_catalog`'s existing scan/persist shape. +A content hash (reusing `content_hash.rs`'s `sha256_hex`/`blake3_hex`) is a **lazily-computed +dedupe column** on the same row: populated by a background backfill pass, not required at +first-index time. This gives a fast first index on modest node hardware while leaving a path to +real dedupe (the same track shared by two peers, or present twice on one node) once the hash +column catches up. + +**Rejected:** `content-hash-identity` (track identity = content hash of the audio payload) — +requires reading every byte of every file at index time, which is too expensive on a large +library on modest node hardware, and a re-encode of the same recording would produce a different +identity, defeating the purpose. `path-identity` (track identity = `(source, canonical path)` +alone, no hash column ever) — a file move or rename orphans the row and any play counts or +favourites attached to it, and two peers sharing the same album stay two separate libraries +forever with no dedup path. + +## Albums/artists: derived-albums + +Albums and artists are **not** stored as first-class rows. They are computed at read time by +grouping the track index on `(album, album_artist)` / `artist` tag fields. There is no +album-identity or album-merge problem to solve, and a retag just changes what the grouping +produces on the next read — nothing to migrate. + +**Rejected:** storing albums/artists as first-class rows — this creates an album-identity and +merge problem (what makes two rows "the same album"?) and adds migration surface for a benefit +(a place to hang album-level extras like cover art path, a review, a purchase record) that isn't +needed yet and can be added later via a schema bump + reindex, which is exactly the reindex path +this document defines below. + +## Index format: index-format-json + +The index is a single JSON file under `data_dir`, at `data_dir/music/index.json`, matching +`content_server.rs::load_catalog`'s `data_dir.join(CATALOG_FILE)` precedent exactly (JSON file +under a subdirectory, loaded via `serde_json::from_str`, defaulted on missing/corrupt, saved via +`serde_json::to_string_pretty` + `fs::write`). Human-inspectable, trivial to back up, and adds no +new dependency. + +**Rejected:** `index-format-sqlite` — a new dependency that fell outside this plan's Package +Legitimacy Audit (13-RESEARCH.md's audit covers `lofty` only), which would need its own +legitimacy-review gate that this phase has not budgeted. Whole-file JSON rewrite cost is accepted +as a tradeoff for a personal-library-scale index; if a library later grows past what whole-file +rewrites can serve comfortably, that's a `MUSIC_SCHEMA_VERSION` bump away, not a blocker today. + +## Sources indexed: both + +The music library indexes **both** the node's own FileBrowser `Music` folder (`MusicSource::OwnLibrary`) +and peer-shared audio reachable through the existing content/peer-proxy subsystem +(`MusicSource::Peer { onion: String }`). This matches 13-11's plan to surface peer content in +`SongGrid` alongside the node's own library, and reuses `content.*`'s existing peer-audio +discovery rather than building a second one. Indexing "the Music folder" must survive the +`ShareModal.vue` mime-map landmine noted in `13-CONTEXT.md` (m4a/aac/opus/wma currently +mis-share as `application/octet-stream` and get auto-filed to `Documents`) — 13-11 fixes the +mime map; this index's own tag-extraction path (13-04 Task 3) does not depend on that fix, since +it reads file contents directly, not the share-time MIME guess. + +## Schema version and the reindex path + +`MUSIC_SCHEMA_VERSION` starts at **`1`**. It is written into `data_dir/music/index.json` as a +top-level field alongside the track/album data. + +**On load, if the on-disk index's `MUSIC_SCHEMA_VERSION` is older than the running binary's +constant:** the node runs a migration step for each version delta before use (a no-op for +version 1, since there is no prior version). This is the routine, expected case as the schema +evolves. + +**On load, if the on-disk index's `MUSIC_SCHEMA_VERSION` is *newer* than the running binary's +constant** (an older binary encountering an index written by a newer version — e.g. after a +downgrade, or a shared `data_dir` touched by a newer node): the node does **not** attempt to +read or reinterpret the newer-format data. It logs a warning naming both versions, treats the +index as absent (starts from an empty in-memory index), and does **not** overwrite the on-disk +file until a reindex is explicitly triggered — this avoids a downgraded node silently truncating +or corrupting an index a newer node will read again later. A full reindex (rescan `OwnLibrary` +and re-request peer catalogs) is the standard recovery path whenever `MUSIC_SCHEMA_VERSION` +changes in either direction; because albums/artists are derived rather than stored, a reindex +only needs to rebuild the track rows, not reconcile any stored album/artist state. + +## Summary of chosen option ids + +`hybrid-identity`, `derived-albums`, `index-format-json`, sources = both own-library and peer.