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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bf9b4e6ad3
commit
bca4660a95
+89
@@ -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.
|
||||
Reference in New Issue
Block a user