Files
archy/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-11-PLAN.md
T

260 lines
20 KiB
Markdown
Raw Normal View History

2026-08-12 10:55:49 +00:00
---
phase: 13-aiui-functional-conversational-node-control-and-content-surf
plan: 11
type: execute
wave: 4
depends_on: ["13-07", "13-06"]
files_modified:
- neode-ui/src/composables/archyContentAdapter.ts
- neode-ui/src/composables/__tests__/archyContentAdapter.test.ts
- neode-ui/src/components/cloud/ShareModal.vue
- /home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts
autonomous: true
requirements: [AIUI-03]
must_haves:
truths:
- "AIUI's SongGrid shows the node's real music library — albums, artists and tracks from the index, not a MIME-filtered folder listing (D-13, D-12)"
- "An .m4a, .aac, .opus or .wma file shared from the cloud view gets a real audio MIME type, routes to the global bottom-bar player, and auto-files to Music instead of Documents"
- "Audio opens in the global bottom-bar player and never in the lightbox — the rule enforced across five existing call sites is not broken by the new path"
- "The music track reached the UI without any control-track or content-track plan depending on a music plan, and without the phase-closing gate 13-15 depending on one either (D-13)"
artifacts:
- path: "neode-ui/src/composables/archyContentAdapter.ts"
provides: "music.* records mapped onto AIUI's Song/Album shape, alongside the existing ContentItem mapping"
contains: "adaptLibraryTracks"
key_links:
- from: "neode-ui/src/composables/archyContentAdapter.ts"
to: "core/archipelago/src/api/rpc/music.rs"
via: "music.list-albums / music.list-tracks feed the songs bucket of the existing content:push channel"
pattern: "music\\.list-"
- from: "neode-ui/src/components/cloud/ShareModal.vue"
to: "neode-ui/src/composables/useAudioPlayer.ts"
via: "a correct audio MIME on m4a/aac/opus/wma is what routes the file to the bottom-bar player"
pattern: "audio/"
---
<objective>
Light up `SongGrid` from the real library, and fix the share-path bug that would otherwise make
the library's most common formats second-class everywhere else.
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.
</objective>
<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>
<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
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Map the library onto the Song shape the grid already renders</name>
<files>neode-ui/src/composables/archyContentAdapter.ts, neode-ui/src/composables/__tests__/archyContentAdapter.test.ts</files>
<behavior>
- 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`.
</behavior>
<read_first>
- `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.
</read_first>
<action>
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.
</action>
<verify>
<automated>cd neode-ui &amp;&amp; npx vitest run src/composables/__tests__/archyContentAdapter.test.ts</automated>
<automated>cd neode-ui &amp;&amp; npx vitest run src/composables/__tests__/useAudioPlayer.test.ts</automated>
<automated>cd neode-ui &amp;&amp; npx vue-tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- `cd neode-ui && npx vitest run src/composables/__tests__/archyContentAdapter.test.ts` exits 0 with a test per `<behavior>` bullet and the existing 13-06 tests still passing
- `grep -q 'export function adaptLibraryTracks' neode-ui/src/composables/archyContentAdapter.ts`
- `grep -cE '[?&](auth|token)=' neode-ui/src/composables/archyContentAdapter.ts` returns 0
- `cd neode-ui && git diff --exit-code -- src/services/contextBroker.ts` exits 0 — the transport was not touched; the `kind` discriminator absorbed the new bucket
- `git -C /home/archipelago/Projects/AIUI diff --exit-code -- packages/app/src/components/content/SongGrid.vue packages/core/src/types/content.ts` exits 0
- `cd neode-ui && npx vitest run src/composables/__tests__/useAudioPlayer.test.ts` exits 0 — the audio-never-in-lightbox rule is still pinned
- `cd neode-ui && npx vue-tsc --noEmit` exits 0
</acceptance_criteria>
<reversibility rating="reversible">A prop-shaped mapping over a stable RPC surface; the source behind the grid stays swappable per D-12.</reversibility>
<done>Real indexed tracks render in `SongGrid` through its unchanged props, with stable order, honest empty states, no broken cover images and no credential-bearing URL.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Four missing audio types — the AAC family stops being filed as Documents</name>
<files>neode-ui/src/components/cloud/ShareModal.vue</files>
<behavior>
- Sharing a `.m4a` produces an audio MIME type, not the generic binary type.
- The same holds for `.aac`, `.opus` and `.wma`.
- A shared file with an audio MIME routes to the global bottom-bar player and does not open the lightbox.
- A shared file with an audio MIME auto-files to Music rather than Documents.
- Existing behaviour for `.mp3`, `.flac`, `.ogg` and `.wav` is unchanged.
- An unknown extension still falls back to the generic binary type — the fix adds entries, it does not guess.
</behavior>
<read_first>
- `neode-ui/src/components/cloud/ShareModal.vue` around line 358 — the extension-to-MIME map that today lists exactly four audio extensions. Read the surrounding function to see how the fallback works before adding entries.
- `neode-ui/src/composables/useAudioPlayer.ts` and `neode-ui/src/components/GlobalAudioPlayer.vue` — how a MIME type routes a file to the bottom-bar player, and the five call sites that enforce audio-never-in-lightbox.
- `core/archipelago/src/api/rpc/content.rs` around line 668 — the node-side MIME auto-filing that decides Music vs Documents. The browser-side map must agree with it, or a file will play correctly and file wrongly.
- `neode-ui/src/composables/archyContentAdapter.ts``classifyByMime` from 13-06 already handles these four extensions. **Keep the two lists consistent**; a divergence here is exactly how this bug survived the first time.
</read_first>
<action>
Add the four missing entries to the extension-to-MIME map in `ShareModal.vue`: the AAC-in-MP4 container extension, raw AAC, Opus, and Windows Media Audio, each mapped to its correct audio MIME type. Leave the existing four entries and the generic fallback exactly as they are — this fix adds coverage, it does not change the fallback strategy and it does not guess at unknown extensions.
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.
</action>
<verify>
<automated>cd neode-ui &amp;&amp; npx vitest run src/composables/__tests__/useAudioPlayer.test.ts</automated>
<automated>cd neode-ui &amp;&amp; npx vitest run 2>&amp;1 | tail -15</automated>
<automated>cd neode-ui &amp;&amp; npx vue-tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- `grep -cE "m4a:|aac:|opus:|wma:" neode-ui/src/components/cloud/ShareModal.vue` returns 4
- `grep -cE "mp3:|flac:|ogg:|wav:" neode-ui/src/components/cloud/ShareModal.vue` returns 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 run` exits 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>
<done>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.</done>
</task>
<task type="auto">
<name>Task 3: AIUI asks for the library the same way it asks for content</name>
<files>/home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts</files>
<read_first>
- `/home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts``requestArchyContent` from 13-06 and the `archyBridge.requestContext` convention it mirrors. **Add a sibling; do not invent a fourth transport convention.**
- `/home/archipelago/Projects/AIUI/packages/app/src/composables/useContentPanel.ts``setArchyContent` and `archyContentActive` from 13-06; the `songs` bucket is what this feeds.
- `/home/archipelago/Projects/AIUI/packages/app/src/pages/ChatPage.vue` — the live render tree through `ContentGridView`. **`ContentPanel.vue` is dead code and must not be built through.**
</read_first>
<action>
Work in `/home/archipelago/Projects/AIUI` on branch `development`.
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.
</action>
<verify>
<automated>cd /home/archipelago/Projects/AIUI/packages/app &amp;&amp; npx vitest run</automated>
<automated>cd /home/archipelago/Projects/AIUI/packages/app &amp;&amp; npx vue-tsc --noEmit</automated>
<automated>cd /home/archipelago/Projects/AIUI &amp;&amp; git status --porcelain | grep -c . | grep -qx 0</automated>
</verify>
<acceptance_criteria>
- `grep -q 'requestArchyLibrary' /home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts`
- `grep -c 'ContentPanel' /home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts` returns 0
- `git -C /home/archipelago/Projects/AIUI diff --exit-code HEAD~1 -- packages/app/src/components/content/ packages/core/src/types/content.ts` exits 0
- `cd /home/archipelago/Projects/AIUI/packages/app && npx vitest run` exits 0
- `cd /home/archipelago/Projects/AIUI/packages/app && npx vue-tsc --noEmit` exits 0
- The commit is pushed to `development` and the working tree is clean
</acceptance_criteria>
<done>AIUI requests the library over the same bridge it uses for content, and `SongGrid` fills from real indexed tracks with no grid component changed.</done>
</task>
</tasks>
<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>
<verification>
- `cd neode-ui && npx vitest run` green (whole suite, including `archyContentAdapter.test.ts` and `useAudioPlayer.test.ts`)
- `cd neode-ui && git diff --exit-code -- src/services/contextBroker.ts` exits 0
- `cd /home/archipelago/Projects/AIUI/packages/app && npx vitest run && npx vue-tsc --noEmit` green
- All eight audio extensions map to an audio MIME across `ShareModal.vue`, `classifyByMime` and `content.rs`
</verification>
<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>
<output>
Create `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-11-SUMMARY.md` when done
</output>