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 | 11 | execute | 4 |
|
|
true |
|
|
D-13 is explicit that the music library "lands as its own wave of plans inside Phase 13, not
blocking the rest — peer files, movies and conversational control ship on their own track and
the library lights up SongGrid when ready." This is that plan. Its depends_on points at the
music indexer and the content adapter; no plan on the control or content track depends on any
music plan, and neither does the phase-closing gate 13-15, so the track independence D-13
requires holds literally in the wave graph rather than only in prose.
That makes this a terminal plan: nothing lists it in depends_on, by design. It is not
orphaned — it owns AIUI-03's SongGrid and share-MIME deliverables, it lands at wave 4 well
ahead of the wave-8 gate, and 13-15 step 7b reads its summary as a best-effort input and records
the result. But if it slips, is red, or is deferred, 13-15 records that and the phase closes on
the control and content tracks anyway. That is the whole point of D-13.
The second half is a verified, directly-relevant landmine: neode-ui/src/components/cloud/ShareModal.vue
line 358 maps mp3, flac, ogg and wav and omits m4a, aac, opus and wma. Those four
therefore share as application/octet-stream, never route to the audio player, and auto-file to
Documents instead of Music. Shipping a music library while the share path still mis-types
the entire AAC family would be shipping a library that only half works.
Output: adaptLibraryTracks in the content adapter, the songs bucket fed from music.*, and
a corrected share MIME map.
<flagged_assumptions> None in this plan. </flagged_assumptions>
<artifacts_this_phase_produces> Symbols created by this plan:
neode-ui/src/composables/archyContentAdapter.ts:export function adaptLibraryTracks,export function adaptLibraryAlbums,export interface ArchyLibraryTrack,export interface ArchyLibraryAlbum/home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts:requestArchyLibrary
Changed, not created: ShareModal.vue's existing extension-to-MIME map gains four entries.
No new component, no new postMessage channel, and no change to SongGrid.vue.
</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-MUSIC-MODEL.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-06-SUMMARY.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-07-SUMMARY.md Task 1: Map the library onto the Song shape the grid already renders neode-ui/src/composables/archyContentAdapter.ts, neode-ui/src/composables/__tests__/archyContentAdapter.test.ts - A `music.list-tracks` record becomes a `Song` with title, artist, album and duration carried through from the extracted tags. - A track whose artist tag was absent maps to a display value derived from the album artist, or to an empty string — never to the literal `null` or `undefined`. - Album grouping preserves the index's deterministic order; calling the adapter twice on the same input yields the same order. - A track with no cover art maps with an absent `coverUrl`, and the grid's existing no-artwork state is what renders — no broken-image URL is emitted. - A paid or peer-sourced track carries a source entry distinguishing it from an own-library track, using the same three source literals 13-06 pinned. - No produced playback URL carries a credential in its query string. - An empty library produces an empty `songs` array, not `undefined`. - `neode-ui/src/composables/archyContentAdapter.ts` (13-06) — `adaptContentItems`, `classifyByMime`, `sortDeterministic`, and the three pinned source literals. **Extend this file's conventions; the library mapping is a sibling of the ContentItem mapping, not a replacement.** - `neode-ui/src/composables/__tests__/archyContentAdapter.test.ts` (13-06) — including the assertion that no adapter-produced URL matches a credential query parameter. The new mapping is held to the same assertion. - `/home/archipelago/Projects/AIUI/packages/core/src/types/content.ts` lines 44-60 — `Song` and `SongSource`, the exact target shape. **Read, never modify** (D-12). - `core/archipelago/src/api/rpc/music.rs` (13-07) — the `music.list-albums` / `music.list-tracks` response envelopes. - `.planning/phases/13-.../13-MUSIC-MODEL.md` — the entity model whose field names the adapter reads. - `neode-ui/src/composables/useAudioPlayer.ts` and `neode-ui/src/components/GlobalAudioPlayer.vue` — the singleton bottom-bar player. **Audio never opens the lightbox**; that rule is enforced in five existing call sites and the new path must not become a sixth exception. Add `adaptLibraryTracks` and `adaptLibraryAlbums` to `archyContentAdapter.ts`, mapping the `music.*` response records onto AIUI's `Song` shape. Reuse `sortDeterministic`'s comparator idea but honour the index's own ordering, which 13-07 already made stable — do not re-sort by a different key in the browser, or the grid and the RPC will disagree about what "first" means.Missing tag fields map to a display fallback (album artist, then empty string) rather than to a stringified null. A track with no cover art gets no coverUrl at all, so SongGrid's existing no-artwork state renders instead of a broken image — this matters because AIUI's cover-art sources are dev-server-only Vite middleware and are 404 on a node, which 13-06 already recorded as a known and accepted gap.
Playback URLs resolve through the existing content endpoints for own-library tracks and the existing Rust Range-streaming proxy for peer tracks. Do not build a URL with a credential in its query string — the same rule and the same test assertion as 13-06.
Feed the results into the existing generic content:push channel's songs bucket by adding a kind value; do not add a second channel and do not modify contextBroker.ts's transport. That is what 13-06's kind discriminator was for, and it is what keeps this plan's files_modified from colliding with the control track.
Extend archyContentAdapter.test.ts with a test per <behavior> bullet, including a repeat of the credential-in-URL assertion over the new mapping's output.
cd neode-ui && npx vitest run src/composables/tests/archyContentAdapter.test.ts
cd neode-ui && npx vitest run src/composables/tests/useAudioPlayer.test.ts
cd neode-ui && npx vue-tsc --noEmit
<acceptance_criteria>
cd neode-ui && npx vitest run src/composables/__tests__/archyContentAdapter.test.tsexits 0 with a test per<behavior>bullet and the existing 13-06 tests still passinggrep -q 'export function adaptLibraryTracks' neode-ui/src/composables/archyContentAdapter.tsgrep -cE '[?&](auth|token)=' neode-ui/src/composables/archyContentAdapter.tsreturns 0cd neode-ui && git diff --exit-code -- src/services/contextBroker.tsexits 0 — the transport was not touched; thekinddiscriminator absorbed the new bucketgit -C /home/archipelago/Projects/AIUI diff --exit-code -- packages/app/src/components/content/SongGrid.vue packages/core/src/types/content.tsexits 0cd neode-ui && npx vitest run src/composables/__tests__/useAudioPlayer.test.tsexits 0 — the audio-never-in-lightbox rule is still pinnedcd neode-ui && npx vue-tsc --noEmitexits 0 </acceptance_criteria> A prop-shaped mapping over a stable RPC surface; the source behind the grid stays swappable per D-12. Real indexed tracks render inSongGridthrough its unchanged props, with stable order, honest empty states, no broken cover images and no credential-bearing URL.
Cross-check the resulting map against classifyByMime in archyContentAdapter.ts and against the node-side auto-filing in content.rs, and make the three agree. If they disagree on any of the eight audio extensions, record which one is authoritative in the summary and align the other two to it — a browser that plays a file correctly while the node files it under Documents is the same class of bug in a new place.
Add a test asserting the mapping for all eight audio extensions plus one unknown extension, and one asserting that an audio MIME does not open the lightbox. Place it alongside the existing cloud-component tests, following neode-ui/src/composables/__tests__/useFileType.test.ts's fixture-table convention.
cd neode-ui && npx vitest run src/composables/tests/useAudioPlayer.test.ts
cd neode-ui && npx vitest run 2>&1 | tail -15
cd neode-ui && npx vue-tsc --noEmit
<acceptance_criteria>
grep -cE "m4a:|aac:|opus:|wma:" neode-ui/src/components/cloud/ShareModal.vuereturns 4grep -cE "mp3:|flac:|ogg:|wav:" neode-ui/src/components/cloud/ShareModal.vuereturns 4 — the existing entries survived- A new test asserts the MIME for all eight audio extensions and for one unknown extension, and it passes
- A test asserts an audio MIME does not open the lightbox, and it passes
cd neode-ui && npx vitest runexits 0 — the whole neode-ui suite is green- The summary records which of the three MIME maps (
ShareModal.vue,classifyByMime,content.rs) was taken as authoritative and confirms the other two agree on all eight extensions </acceptance_criteria> All eight common audio extensions get a real audio MIME on the share path, route to the bottom-bar player, and file to Music; unknown extensions still fall back rather than being guessed.
Add requestArchyLibrary(scope) to useArchy.ts as a sibling of 13-06's requestArchyContent, using the same bridge call with the library kind. Route its response through the existing setArchyContent so the songs bucket fills exactly the way the films bucket already does.
Do not modify SongGrid.vue, ContentGridView.vue or packages/core/src/types/content.ts — D-12 keeps AIUI's design exactly and only the data source changes. Do not revive ContentPanel.vue or any component that only it referenced.
Record honestly in the summary that album artwork is absent for library tracks on a node, because AIUI's artwork sources are dev-server-only Vite middleware, and that SongGrid renders its existing no-artwork state rather than a broken image.
Commit and push on development, staging explicitly by path.
cd /home/archipelago/Projects/AIUI/packages/app && npx vitest run
cd /home/archipelago/Projects/AIUI/packages/app && npx vue-tsc --noEmit
cd /home/archipelago/Projects/AIUI && git status --porcelain | grep -c . | grep -qx 0
<acceptance_criteria>
grep -q 'requestArchyLibrary' /home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.tsgrep -c 'ContentPanel' /home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.tsreturns 0git -C /home/archipelago/Projects/AIUI diff --exit-code HEAD~1 -- packages/app/src/components/content/ packages/core/src/types/content.tsexits 0cd /home/archipelago/Projects/AIUI/packages/app && npx vitest runexits 0cd /home/archipelago/Projects/AIUI/packages/app && npx vue-tsc --noEmitexits 0- The commit is pushed to
developmentand the working tree is clean </acceptance_criteria> AIUI requests the library over the same bridge it uses for content, andSongGridfills from real indexed tracks with no grid component changed.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| tag text → the browser DOM | Peer-authored ID3/Vorbis tag strings render as track titles and artist names |
| library records → iframe | Node data crossing into AIUI, gated on the media grant like all content |
| shared file MIME → player and filing | A wrong MIME misroutes both playback and storage location |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-13-70 | Tampering | Peer-authored tag text rendered as markup | medium | mitigate | Vue interpolation escapes by default; the adapter emits plain strings and no raw-HTML directive is introduced, matching 13-06 |
| T-13-71 | Information Disclosure | A credential in a track playback URL | high | mitigate | Same rule and same test assertion as 13-06: no credential query parameter is produced. Own tracks use the session-carrying content endpoints, peer tracks the existing Rust Range proxy |
| T-13-72 | Tampering | Browser and node MIME maps disagreeing, so a file plays right and files wrong | medium | mitigate | Task 2 cross-checks all three maps and records which is authoritative; the divergence is what produced the original m4a/aac/opus/wma bug |
| T-13-73 | Denial of Service | An unbounded library pulled into the browser in one push | low | mitigate | 13-07's limit clamp applies; the adapter consumes the paginated envelope rather than requesting everything |
| T-13-74 | Elevation of Privilege | Library records reaching the iframe without a media grant | high | mitigate | The existing content:push handler's permission check from 13-06 applies unchanged — this plan adds a kind, not a bypass |
| T-13-75 | Repudiation | Audio opening in the lightbox, breaking a rule enforced in five call sites | low | mitigate | useAudioPlayer.test.ts is re-run as an acceptance criterion, and Task 2 adds an explicit assertion |
| T-13-SC | Tampering | npm/pip/cargo installs | high | mitigate | Zero packages added in either repo. No install task, so no legitimacy checkpoint required |
| </threat_model> |
<success_criteria>
SongGrid renders the node's real library through unchanged props, the AAC family stops being
filed as Documents and stops missing the audio player, and the music track reached the UI
without a single control-track or content-track plan depending on it.
</success_criteria>