20 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13-aiui-functional-conversational-node-control-and-content-surf | 04 | execute | 2 |
|
|
false |
|
|
This plan also clears the two gates that stand in front of any music code: the recorded decision
(REVERSIBILITY_GATES) and lofty's package legitimacy (13-RESEARCH.md marks it [ASSUMED]
because the automated package-legitimacy check seam was unavailable in the research session,
and its own fallback rule says an [ASSUMED] package's cargo add must be gated behind a
checkpoint:human-verify).
Wave note (D-13 independence). D-13 requires the music library to land as its own wave
"not blocking the rest" — and it does: no plan on the control or content track depends on any
plan in the music track. The edge here points the other way, and it is a file-serialization
fact rather than a logical coupling: core/archipelago/src/main.rs is the binary crate's only
module-declaration site, so mod assistant; (13-01) and mod music; (this plan) cannot be
written in the same wave. Nothing in this plan uses anything 13-01 produces.
Purpose: get the irreversible decision made while it is still cheap, and get the dependency audited before it is in the tree.
Output: 13-MUSIC-MODEL.md, lofty in Cargo.toml, and core/archipelago/src/music/.
<flagged_assumptions> None in this plan. </flagged_assumptions>
<artifacts_this_phase_produces> Symbols created by this plan:
core/archipelago/src/music/mod.rs:pub struct Track,pub struct Album,pub struct Artist,pub struct TrackId/AlbumId/ArtistId(or the identity scheme chosen at Task 1),pub enum MusicSource,const MUSIC_SCHEMA_VERSIONcore/archipelago/src/music/tags.rs:pub fn extract_tags,pub struct RawTags,fn fallback_from_filenamecore/archipelago/src/main.rs:mod music;core/archipelago/Cargo.toml:loftydependency- New file
.planning/phases/13-.../13-MUSIC-MODEL.md</artifacts_this_phase_produces>
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/STATE.md @CLAUDE.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-CONTEXT.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-RESEARCH.md Task 1: Decide the music entity model — one-way The album / artist / track entity model, the identity scheme that survives a file move or a retag, where the index lives on disk, and what a reindex path looks like when the schema changes. Also, within CONTEXT.md's "Claude's Discretion": whether the library indexes the node's own FileBrowser `Music` folder, peer audio, or both. D-13 rates this **one-way**: once nodes have indexed libraries, changing the entity model needs a reindex path, not just a code change. The three sub-decisions that are genuinely hard to walk back are (a) what a *track's stable identity* is, (b) whether an album is a first-class stored entity or derived at read time, and (c) the on-disk index format.Grounding for the developer:
- There is no music library domain in this codebase today — CONTEXT.md is explicit that the user chose "build a real library" over the narrower MIME-filtered-files option after being told this. There is nothing to migrate from, which is exactly why now is the cheap moment.
content_server.rs::load_catalogis the in-repo precedent for adata_dir-scoped catalog that is scanned and persisted;13-PATTERNS.mdassigns it as the analog formusic/index.rs.- A relevant landmine:
ShareModal.vue's mime map omitsm4a/aac/opus/wma, so those files today share asapplication/octet-stream, never reach the audio player, and are auto-filed toDocumentsinstead ofMusic. Whatever "the Music folder" means to the index must survive that (13-11 fixes the mime map). Track identity = content hash of the audio payload Survives renames, moves and retags. The same track shared by two peers deduplicates naturally. `content_hash.rs` already exists in-tree. Requires reading every byte of every file at index time — expensive on a large library on modest node hardware. A re-encode produces a different identity for the same recording. Track identity = (source, canonical path) Cheap — stat-only indexing, fast reindex, trivially incremental via mtime. A move or a rename orphans the row and any play counts or favourites attached to it. Two peers sharing the same album are two libraries, never one. Path is the row key; content hash is a lazily-computed dedupe column Fast first index, dedupe available when it is worth paying for, and the expensive column can be back-filled without a schema change. Two identity notions to keep straight; dedupe correctness depends on a back-fill that may lag. Albums/artists derived at read time from track tags (vs. stored as first-class rows) No album-identity problem at all; a retag just changes what the grouping produces. Least to migrate later. No place to hang album-level data (cover art path, review, purchase record) later without a schema change — which is the one-way cost this decision is about. Index format: a single JSON file under data_dir, like content_server.rs's catalog Matches the in-repo precedent exactly; human-inspectable; trivial backup/restore; no new dependency. Whole-file rewrite per update; poor above a few thousand tracks. Index format: SQLite under data_dir Incremental writes, real queries, scales past a large personal library. A new dependency that is NOT in 13-RESEARCH.md's Package Legitimacy Audit — adopting it requires its own audit and human-verify gate, which this phase has not budgeted. .planning/phases/13-.../13-MUSIC-MODEL.mdexists and states, each in one paragraph: the track identity scheme; whether albums and artists are stored or derived; the on-disk index path underdata_dirand its format; the sources indexed (ownMusicfolder, peer audio, or both); and the reindex path whenMUSIC_SCHEMA_VERSIONbumps- The file names a
MUSIC_SCHEMA_VERSIONstarting value and states what a node does on encountering an index written by a newer version - The file explicitly records which option ids above were chosen and one sentence on why the rejected ones were rejected </acceptance_criteria> Select one identity option, one album option and one index-format option (e.g. "hybrid-identity, derived-albums, index-format-json"), or describe a different model.
mod.rs declares the entity types exactly as decided in 13-MUSIC-MODEL.md: Track, Album, Artist (stored or derived per the decision), the identity newtypes, pub enum MusicSource { OwnLibrary, Peer { onion: String } } restricted to whatever Task 1 chose to index, and pub const MUSIC_SCHEMA_VERSION: u32 at the decided starting value. Every struct derives Serialize/Deserialize — the index is persisted, so these types are the migration surface and must be written once, carefully.
tags.rs exposes pub fn extract_tags(path: &Path, media_roots: &[PathBuf]) -> Result<RawTags>. It first canonicalizes path and refuses with a distinct error if the result is not under one of media_roots — an indexer that can be pointed at data_dir/secrets is a secret-exfiltration primitive, and this check runs before the file is opened, not after. It then uses lofty::read_from_path and the Accessor trait to pull title, artist, album, album artist, track, disc, year, and AudioFile::properties().duration(). RawTags carries Option<String>/Option<u32> fields plus a has_tags: bool. On a readable audio file with no tag block, populate title from the file stem via fallback_from_filename and set has_tags: false. On a file lofty cannot identify as audio, return Err with a variant the caller can distinguish from the untagged case.
Write the tests FIRST, one per bullet in <behavior>. Generate the fixture files programmatically into a tempfile::tempdir() using lofty's own writing API where it supports the format, rather than committing binary fixtures — a repo full of committed sample audio is a licensing problem and a review burden. For the not-audio case, write a text file with an .mp3 extension. For the path-traversal case, point at a temp path outside the roots. Name them music::tags::tests::mp3_id3v24_yields_full_record, ..::flac_vorbis_yields_full_record, ..::m4a_yields_full_record, ..::ogg_yields_full_record, ..::untagged_file_falls_back_to_filename_stem, ..::non_audio_returns_err_distinct_from_untagged, ..::path_outside_media_roots_is_refused.
cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago music:: 2>&1 | tail -20
grep -q '^mod music;' core/archipelago/src/main.rs
<acceptance_criteria>
grep -q '^mod music;' core/archipelago/src/main.rsgrep -q 'pub struct Track' core/archipelago/src/music/mod.rsandgrep -q 'MUSIC_SCHEMA_VERSION' core/archipelago/src/music/mod.rsgrep -q 'pub fn extract_tags' core/archipelago/src/music/tags.rscd core && cargo test --package archipelago music::exits 0 with all seven named tests passinggrep -q 'media_roots' core/archipelago/src/music/tags.rs— the root confinement is a parameter, not a constant a caller can bypassgit ls-files core/archipelago | grep -ciE '\.(mp3|flac|m4a|ogg)$'returns 0 — no binary audio fixtures were committed- The entity fields in
mod.rsmatch13-MUSIC-MODEL.md's decision (spot-check each name) </acceptance_criteria> The entity model and index format become a persisted data model once nodes index libraries; changing them afterwards needs a reindex path, not just a code change. Gated by Task 1'scheckpoint:decision, per CONTEXT.md D-13's own rating. Four real audio formats round-trip into a typed record, an untagged file still becomes a library entry, a non-audio file is a distinguishable error, and a path outside the media roots never gets opened.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| filesystem → indexer | Media files are attacker-influenceable (a peer chooses the filename and the tag contents of anything shared) |
| tag text → downstream context | Tag strings are peer-supplied text and will eventually reach the model context and the UI — D-10 territory |
| crates.io → the tree | A new third-party parser handling untrusted binary input |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-13-20 | Information Disclosure | Indexer pointed at data_dir/secrets or another sensitive path |
high | mitigate | extract_tags canonicalizes and confines to media_roots before opening the file; asserted by path_outside_media_roots_is_refused. The roots are a parameter, not a constant |
| T-13-21 | Denial of Service | Malformed/hostile audio file crashes or hangs the parser | medium | mitigate | lofty errors are returned as Err, never unwrapped; a non-audio file is a normal error path, asserted by non_audio_returns_err_distinct_from_untagged. No panic path is introduced |
| T-13-22 | Tampering | Peer-authored tag text treated as trusted once it is "structured data" | high | mitigate | Deferred by design to 13-12's wrap_untrusted boundary: RawTags fields are plain Option<String> carrying no trust, and nothing in this plan puts them in a model context. Recorded here so the assumption is explicit rather than implied |
| T-13-23 | Elevation of Privilege | Music entity model later needs a field that only exists on a stored album, forcing an on-disk migration | medium | mitigate | This is the one-way cost D-13 names. Mitigated by making it a checkpoint:decision and by MUSIC_SCHEMA_VERSION + a written reindex path, not by trying to guess right |
| T-13-SC | Tampering | npm/pip/cargo installs | high | mitigate | lofty is [ASSUMED] in 13-RESEARCH.md's Package Legitimacy Audit. Task 2 is a checkpoint:human-verify with gate="blocking-human" before cargo add, per the audit's own fallback rule. Not auto-approvable. cargo tree -i lofty is reviewed for unexpected transitive networking deps |
| </threat_model> |
<success_criteria>
The irreversible half of D-13 is a written, developer-made decision rather than an emergent
property of the first implementation; lofty entered the tree through a human legitimacy gate;
and tag extraction handles the four formats a real library contains, including the M4A/AAC
family the current share path mis-handles.
</success_criteria>