30 KiB
PLAN.md — DeepDrftHome forward roadmap
Forward-looking roadmap. Sits alongside CONTEXT.md (architecture orientation) and COMPLETED.md (history). Per CONTEXT.md §6, items move from here to COMPLETED.md when work lands; do not delete completed entries.
Organised by theme, not by date. Themes are roughly ordered by current product weight, not commitment. Nothing here carries a timeline unless it explicitly says so.
0. Baseline — what just landed
A two-part audit (design + streaming) ran on 2026-05-17 and the fixes for Critical, Major, and Minor findings are now on dev. The remainder of this plan assumes that baseline. In summary the audit-pass fixed:
- Index concurrency —
VaultIndexDirectoryno longer drops the lock before its async disk write; the index file can no longer be clobbered by interleaved writers. - Repository semantics —
TrackRepository.Updatenow fails-fast when anIdis not found instead of silently issuing anINSERT. - Streaming Criticals — concurrent-seek race in the client, dirty trailing bytes leaking out of the
ArrayPool-rented buffer, final-tail audio dropped at EOF below the minimum decode frame, and the assumption that the first network chunk contains the whole WAV header. - 17 design and streaming Majors/Minors across all eight projects — format-validation alignment between processor/offset/decoder,
IAsyncDisposableon the player provider, cancellation tokens threaded through the HTTP path, structured logging into the FileDatabase subsystem, sort-sentinel cleanup, sundry DRY/SRP tightenings.
What this means for the roadmap: the streaming substrate is solid. Future work can build on top of it rather than around it. The remaining items in TODO-V2.md that did not land are deferred as features, not bugs — they are captured below under Phase 1.
Phase 1 — Streaming features deferred from the audit
These were flagged during the audit but classified as feature work, not defect fixes. They are listed in rough order of user-visible impact.
1.3 Preload / prefetch of the next track
- What: No mechanism to begin the next track's stream during the tail of the current. Each play is a cold fetch.
- Why it matters: Prerequisite for both crossfade (1.4) and gapless (1.5). Also a perceived-latency win on its own — track-change feels instant when the bytes are already in flight.
- Shape: A second
HttpClientrequest kicked off when the current track passes a configurable threshold (e.g. last 10 seconds). Bytes accumulate into a stagedStreamDecoderinstance rather than the live one. Promotion to "current" happens at end-of-stream or on user-selected next. - Prerequisite: Requires a notion of "next track" — today the player only knows the current one. That implies either a playlist/queue model in
IPlayerServiceor a passive "what was the next row in the gallery" inference. - Open question: Does a queue model belong in
IPlayerService, or is the player a single-slot device that a futurePlaylistServiceorchestrates above? Worth a design note before implementation. Capture in product notes when picked up.
1.4 Crossfade
- What: Smooth A→B transition with overlapping fade-out / fade-in.
- Why it matters: DJ/mix aesthetic that fits the DeepDrft collective's electronic-music context. Distinguishing UX from generic "next track."
- Shape: Architecturally two simultaneous
PlaybackSchedulerinstances suffice — each owns its own gain node, crossfaded viaGainNode.gain.linearRampToValueAtTime. The wiring is the work, not the audio graph itself. - Prerequisite: 1.3 (Preload) — there is nothing to fade into without prefetch.
1.5 Gapless playback
- What: Eliminate the inter-track silence that exists today.
- Why it matters: Important for live-set rips, mix tapes, anything authored to flow continuously.
- Shape: The decoder must be able to start the next track's first buffer scheduled exactly at the end of the current one's last buffer (sample-accurate, not wall-clock). With
PlaybackScheduler's existing 500 ms lookahead this is mechanically achievable — the next track's firstAudioBufferSourceNode.start(t)is set to the previous track's end time. - Prerequisite: 1.3 (Preload). Also needs to play nicely with 1.2 because gapless across formats is hard (encoder padding/priming on MP3 in particular).
- Constraint: Truly sample-accurate gapless requires knowing the priming/padding sample counts of the source format. Out of scope for WAV-only; revisit when format diversity lands.
1.6 Track-skip on error
- What: A failed
processStreamingChunkaborts the entire load with no recovery path. - Why it matters: One corrupt frame at byte 4M of a 100 MB stream currently means the listener loses the entire track. Should at minimum surface a clear error and (optionally) skip past the bad region.
- Shape: Two-level response.
- Cheap: catch in the streaming loop, surface a user-visible error, advance the gallery to the next track if a queue exists.
- Richer: byte-scan forward to the next valid frame header for the format and resume. Format-dependent — only worth doing once 1.2 lands.
1.7 Safari compatibility
- What: Two known Safari edge cases.
webkitAudioContext.close()is async-but-not-Promise on older Safari (≤ ~14);awaitresolves immediately and the nextinitialize()can run against a not-yet-closed context.- iOS Safari < 15 had streaming-fetch quirks;
HttpCompletionOption.ResponseHeadersReadbehaviour is not guaranteed there.
- Why it matters: Real listener share. iOS in particular is a primary listening surface for music.
- Shape: For the
close()race — detectwebkitAudioContextand pollstate === "closed"with a short timeout instead of trusting theawait. For the fetch quirks — first decide the minimum supported iOS version; if pre-15 is in scope, fall back to a non-streaming fetch path and accept the latency. - Open question: What's the floor? Decide before designing the fallback. iOS 15+ as the floor would let us drop the second concern entirely.
Phase 2 — Product surface: gallery, browsing, ingestion
These follow from CONTEXT.md §5. Direction is strongly implied but no specific UI has been committed.
Phase 6 — CMS Enhancements (Completed)
See COMPLETED.md for Phase 6 (§6.1, §6.3) and entity-prep (§6.2 model layer) which landed on dev in June 2026.
6.2 Card-contextual filtering of the Tracks page — [superseded by §8]
- What: Make the Album and Genre dashboard cards navigate into a filtered
/tracksview (e.g. clicking an album card shows only that album's tracks), rather than the unfiltered table. - Why: Turns the dashboard from a read-only summary into a navigation hub — the natural next step once the cards exist.
- Why deferred: The dashboard cards aggregate across all albums/genres — there is no single album/genre to filter to from a top-level count card. Meaningful per-album/per-genre navigation needs an intermediate browse surface (a list of albums, a list of genres) for the admin to pick from — i.e. it's really a CMS analogue of the public
AlbumsView/GenresView, not a property of the summary cards. That's a larger surface than the dashboard itself and shouldn't be smuggled in. TheGET api/track/pageendpoint already acceptsalbum=andgenre=query filters, so the API substrate is ready; the missing piece is the CMS browse UI and the filter plumbing inTrackList. - Superseded: §8 (CMS Track Browser) builds exactly the intermediate browse surface this item was waiting on — Album Mode and Genre Mode are the CMS analogue of
AlbumsView/GenresView, and the filter plumbing intoGetPagedAsyncis part of §8's data contract. This item folds into §8; do not implement it separately.
Phase 3 — New content kinds
3.1 Live / session content
- What: The home page advertises "Live Sessions" and "Video Content (coming soon)". No data model exists for these.
- Why it matters: Honour the home page copy. Also differentiates the site from a generic track gallery — live sessions and video are the collective's authored output.
- Shape: Speculative; no commitment yet.
- Likely new entity table(s) sibling to
TrackEntity(SessionEntity,VideoEntity?) — or a polymorphicMediaEntitywith discriminator. The choice affects how much code inTrackService/TrackControllercan be reused. - New vault type(s).
MediaVaultType.Mediaexists and is the obvious home for video; sessions are probably stillAudio. - New routes, new UI surfaces, new player considerations (video has its own playback element and does not go through the WAV decoder).
- Likely new entity table(s) sibling to
- Prerequisite: Probably 2.1 (vault wiring proof) and a decision on the entity model before any code lands.
[speculative]— direction inferred from home-page copy, not a Daniel-confirmed commitment.
Phase 4 — Infrastructure / delivery
4.3 Dual-write rollback / dead-letter log
- What: If content-side write succeeds and SQL-side write fails, audio is orphaned in the vault. No compensating mechanism exists.
- Why it matters: A latent data-integrity issue. Materially riskier once web upload (2.4) exists.
- Shape: Audit suggested a
DeadLetterLogrecording orphanedentryKeys for a periodic maintenance pass. Lighter than full transactional rollback (which the dual-database split fundamentally cannot give us). - Prerequisite: None. Worth landing alongside or just before 2.4.
Phase 5 — Documentation backlog
5.1 Folder-level CLAUDE.md sweep
- What: Eight folder-level
CLAUDE.mdfiles need writing/rewriting per the brief inDOC_PLAN.md. Five are rewrites (drift from the.NET 10upgrade and structural moves); three are new (DeepDrftWeb.Services,DeepDrftContent.Services— the two libraries where most domain logic now lives — plus the open question onDeepDrftContent.Services/FileDatabase/README.md). - Why it matters: The agent guidance files are how every future implementer (human or agent) gets oriented in a directory. They are currently misleading in ways that will cause wrong assumptions on first contact — claiming
.NET 9, referencingMediaPaththat has beenEntryKeyfor two migrations, describing aFileDatabase/tree insideDeepDrftContentthat has moved out, and missing entirely for the two*.Serviceslibraries. - Shape: Doc-keeper executes against
DOC_PLAN.md. Order of operations and the per-folder briefs are already specified there. - Prerequisite: None. Can run fully in parallel with any feature work.
- Constraint: Wait on Daniel for the
DeepDrftContent.Services/FileDatabase/README.mdjudgement call before that file changes (retire, keep + refresh, or replace with a CLAUDE.md). The other seven can proceed without that decision.
Phase 7 — Shared UI Components
Reusable presentational components in DeepDrftShared.Client (the RCL consumed by both the public site and the CMS). Distinct from the player stack and CMS surfaces — these are host-agnostic building blocks both apps compose.
Phase 8 — CMS Track Browser
Three browse modes for the CMS /tracks page — Track, Album, Genre — selected by a toggle, each deep-linkable so the public home page can link straight into a mode. One view-model (DI-scoped, matching the TracksViewModel pattern) feeds all three views; the divergence is in rendering, not data paths (per the standing "same data, different uses" preference). This supersedes the deferred §6.2 — Album and Genre modes are the intermediate browse surface that item was waiting on. Full spec: product-notes/phase-8-cms-track-browser.md (normalization gate, component decomposition, VM design, URL scheme, data contracts, open questions).
§8.0 landed on 2026-06-11 — a breaking TrackEntity normalization has been completed and is stable on dev. §8.1–§8.5 are now unblocked. The Waveform Pre-Processing tab is removed, folded into an in-grid status column + per-row/page-level generate actions (see §8.2).
A small set of items that are real but don't fit a phase yet. Surface them when they become relevant rather than committing now.
- Identity / accounts. Currently no user concept. Needed before web upload (2.4); also a precondition for favourites, listening history, per-user playlists. Decide the shape before any of those lands.
[speculative]until Daniel signals interest. ITrackServiceinterface. Audit-suggested. Low value today (one consumer pair); higher value when the test surface expands beyond FileDatabase.- Test coverage outside FileDatabase. Tests today cover the FileDatabase subsystem comprehensively and nothing else. As features in Phases 1–4 land, test scope should expand — at minimum
WavOffsetService,AudioProcessor,TrackService(both sides), and the streaming player services. Not a phase of its own; an attached cost to feature work.
Phase 9 — Release Medium Types
Releases gain a top-level medium discriminator above the existing ReleaseType. Three media: Studio CUTS (Cut — the only medium that uses Single/EP/Album), Live SESSIONS (Session — a single live track with a distinct hero image), DJ MIXES (Mix — a single long track with a preprocessed high-resolution waveform datum). This touches the data model, the API, the CMS, and the public site.
The public home page already carries the three-medium framing as editorial cards (Studio / Live / DJ Mix — COMPLETED.md §8.6, landed 2026-06-12), but those cards have no destinations and nothing below the copy layer knows what a medium is. Phase 9 makes the medium real and gives those cards somewhere to point.
Architectural spine — discriminator enum + optional metadata table. ReleaseMedium is a plain enum column on ReleaseEntity. A medium that needs data beyond the base release (Session's hero image, Mix's waveform datum) gets its own 1:1 metadata table; a medium that needs nothing extra (Cut) is the base ReleaseEntity. This is Open/Closed at the schema level — a future medium (e.g. Video, §3.1) adds an enum value and optionally one metadata table, and changes zero existing tables. The alternatives (one wide nullable table; an EF type hierarchy) both collapse to the god-table the Phase 8 normalization moved away from — rejected. Full design, contracts, and the SOLID rationale: product-notes/phase-9-release-medium-types.md.
Design discipline throughout: extension, not modification. Where a per-medium mapping is unavoidable (card → browser, medium → API projection, medium → detail hero), keep it in one table per concern — never a scattered three-arm switch. Drive CMS cards and nav sub-items off Enum.GetValues<ReleaseMedium>() + a display-metadata lookup, so a new medium surfaces automatically.
The ReleaseType-only-for-Cut invariant. Single/EP/Album is meaningful only when Medium == Cut. Enforce as a domain rule (service layer ignores/resets ReleaseType for non-Cut; CMS hides the field unless Cut; ReleaseDto.ReleaseType is nullable, nulled at the single entity→DTO mapping point for non-Cut so one producer enforces and no consumer needs the rule), not a DB constraint — by choice, not necessity: EF Core supports check constraints first-class (HasCheckConstraint, versioned in migrations, Npgsql-supported), but the invariant is advisory ("meaningless," not "invalid") and the read model enforces it at one point. The column stays on ReleaseEntity as a named exception to the metadata-table pattern: a CutMetadata table was considered and rejected because the /cuts hot path reads ReleaseType on every card and Phase 8 §8.0 just landed the column (see spec §1). Future media must not copy this — the default remains the metadata table.
Sequenced as four waves. Wave 1 is a prerequisite for everything; within Waves 2–4 the lettered tracks are parallel.
Dependency summary: 1 → 2 → 3 → 4. Wave 4 (public site) can begin once Wave 2's api/release family is stable; both Wave 4 build and acceptance are independent of Wave 3 (CMS) — the body-less POST api/release/{id}/mix/waveform trigger (9.2.B) can seed real waveform datum for acceptance testing without any CMS in existence, and hero images seed via a script against 9.2.B likewise.
9.5 Wave 5 — Gap Cleanup
Waves 1–4 are on dev. This wave fixes functional gaps discovered in the landed code: one disclosed by the Wave 3 engineer (medium is never written through the upload path), two structural issues flagged by review (fragile track resolution in the detail VM, browser duplication), and one nav gap (/tracks is unreachable from the public menu). Items are ordered: A–B are blockers (data correctness); C–E are correctness/nav gaps; F is a structural debt item worth landing when the browsers next need editing.
9.5.A — Medium write path: POST api/track/upload
- What: The
POST api/track/uploadendpoint accepts nomediumform field.CmsTrackService.UploadTrackAsyncalready sendsmediumin the multipart body (a forward-compatible no-op left by the Wave 3 engineer), but the API ignores it. Every uploaded release is created withMedium = Cutregardless of the CMS form selection. Sessions and Mixes uploaded through the CMS are silently mis-typed at the database level. - Why: This is the primary functional gap of the phase. A mix uploaded as
Cutdoes not appear in the/mixesbrowser, does not trigger waveform generation on the correct release, and the public/mixes/{id}detail page will never find it. The bug is silent — no error surfaces; the track uploads cleanly into the wrong category. - Shape: Three layers, each minimal:
TrackController.UploadTrack— add[FromForm] string? mediumparameter. Parse it withEnum.TryParse<ReleaseMedium>(same defensive pattern asreleaseType, defaulting toCutwith a logged warning on unrecognised values). Pass the parsed value intoUnifiedTrackService.UploadAsync.UnifiedTrackService.UploadAsync— addReleaseMedium mediumparameter. Include it in theReleaseDtopassed toFindOrCreateRelease(the DTO already has theMediumfield; it is simply not populated today).FindOrCreateReleasefind-path: When the release already exists, the returned row'sMediumis not updated to match the upload's intent. This is correct behaviour for the first track — the release was created with the right medium. It is potentially wrong for subsequent tracks uploaded to the same release with a corrected medium. No change required here: medium is a release-level property, and the first upload is authoritative. Document this explicitly in the service comment so future engineers do not try to "fix" it.
- Acceptance criteria: A Session upload from the CMS creates (or links to) a release with
Medium == Session; a Mix upload creates a release withMedium == Mix; a Cut upload is unchanged. TheGET api/release?medium=sessionendpoint returns the Session release immediately after upload with no manual migration. - Open question: Should the upload path update an existing release's medium when it differs? Recommend no — a release's medium is set on creation and should not silently change on a subsequent track add. If an admin needs to change a release's medium, that is an edit operation (9.5.B). Capture this as a comment in the service, not a policy decision to re-open here.
9.5.B — Medium write path: PUT api/track/meta
- What:
UpdateTrackMetadataRequestcarries noMediumfield.PUT api/track/meta/{id}can updateReleaseTypeon a release but cannot changeMedium.CmsTrackService.UpdateAsyncsends nomediumfield. An admin who uploads a Session asCut(due to the pre-9.5.A bug, or a future form mistake) has no way to correct the medium through the CMS after the fact. - Why: Without an edit path, the only remediation is a direct DB update or a delete-and-re-upload. Both are bad. The edit path should be complete.
- Shape:
UpdateTrackMetadataRequest— addReleaseMedium? Medium(nullable: null = no change, matching theReleaseType?pattern already on the request).TrackController.UpdateMeta— applyrequest.Mediumtorelease.Mediumwhen non-null, alongside the existingReleaseTypeconditional (the same six-line pattern at line 394–395 of the controller).CmsTrackService.UpdateAsync— addReleaseMedium? medium = nullparameter, include in the JSON body.ICmsTrackService— update the interface signature to match.TrackEdit.razor/BatchEdit.razor— wire theMediumFieldsselector (already present for upload viaBatchUpload) into the edit submit path, passing the selected medium.
- Acceptance criteria: An admin can open an existing release in
TrackEditorBatchEdit, change the medium selector, submit, and the release'sMediumcolumn updates in the DB. The browsers (CmsAlbumBrowser,CmsSessionBrowser,CmsMixBrowser) reflect the new medium after the edit. - Constraint: The
ReleaseType-only-for-Cutinvariant: when medium changes away fromCut, the controller should null (or ignore)ReleaseTypeon the release — the same enforcement theTrackConverteralready applies on the read path. Mirror that logic on the write path: ifrequest.Mediumis non-null and notCut, resetrelease.ReleaseType = ReleaseType.Single(the DB-level default) rather than leaving a stale studio-format value.
9.5.C — ReleaseDetailViewModel: replace fragile album-title track resolution
- What:
ReleaseDetailViewModel.Loadresolves the playable track for a Session or Mix detail page by calling_trackData.GetPage(pageNumber: 1, pageSize: 1, album: release.Title). This is a string join on album title. If two releases share the same title (different artists — e.g., both have an untitled mix), the wrong track is returned. More fundamentally, filtering by album title relies on theRelease.Titlematching what was stored as the album string at upload time — a join that is fragile once releases can be renamed via the edit path (9.5.B). - Why: The correct join is by
releaseId, not album title. The track-page endpoint already supportsalbum=filtering; it needs an additionalreleaseId=filter, or the public API needs aGET api/track/by-release/{releaseId}endpoint. This is a correctness issue, not a cosmetic one — a collision silently plays the wrong track. - Shape (recommended): Add a
releaseIdquery parameter toGET api/track/pageinTrackControllerand thread it throughITrackService.GetPaged→TrackRepository.GetPagedFilteredAsyncas an additionalWHERE release_id = @releaseIdpredicate.TrackFiltergains along? ReleaseIdfield.ReleaseDetailViewModel.Loadthen callsGetPage(pageNumber: 1, pageSize: 1, releaseId: release.Id)— an exact join, no title string. The publicIReleaseDataServiceandReleaseClientDataServicedo not need changes if the track page is called directly viaITrackDataService. - Acceptance criteria:
/sessions/{id}and/mixes/{id}resolve their playable track byreleaseId, not by album title string. Two releases with identical titles return their own correct tracks on their respective detail pages. - Open question: Should
TrackFilter.ReleaseIdbe exposed on the public unauthenticatedGET api/track/pageendpoint? Yes — it is a read-only filter on public data, same posture asalbum=andgenre=. No auth change.
9.5.D — Public nav: /tracks route unreachable
- What:
Pages.MenuPages(the public nav model) contains ARCHIVE (with sub-items /cuts, /sessions, /mixes) and Genres./tracks(the original track gallery atTracksView.razor) is not in the nav. The route is still live — typing/tracksin the address bar works — but there is no menu entry, no link from any existing page, and no redirect from any of the new medium surfaces. - Why it matters: The track gallery is a useful surface (flat cross-medium search, grid/list toggle, genre/album filter). Removing it from the nav without a replacement or deliberate deprecation is a nav gap. A listener who does not know about
/cutshas no way to discover the flat track list. - Shape (three options — pick one):
- Option A (recommended): Add
/tracksback to the nav. Add a "Tracks" entry (flat, no children) toPages.MenuPagesalongside ARCHIVE and Genres. Zero risk; the page exists and works. Honest about what the site offers. - Option B: Retire
/tracksexplicitly. Add a redirect from/tracks→/cuts(or/archive) and removeTracksView.razor. Requires confirming that/cutsis a complete replacement (it is not —/cutsshows only Cut releases;/tracksis a flat cross-medium list). Not recommended unless Daniel confirms the gallery is intentionally retired. - Option C: Make ARCHIVE the gallery. Repurpose
/archivefrom the current three-card overview to the flat track gallery. Feels wrong —/archiveis already a meaningful overview page, not a gallery.
- Option A (recommended): Add
- Recommendation: Option A. The track gallery is valuable and distinct from the medium-specific browsers. Add "Tracks" to
Pages.MenuPages. If Daniel later wants to retire the gallery, that is a separate explicit decision with a redirect. Do not silently leave a useful route off the nav. - Acceptance criteria:
/tracksappears in the public navigation menu. Desktop and mobile nav both link to it. Existing functionality ofTracksViewis unchanged.
9.5.E — CmsSessionBrowser and CmsMixBrowser: missing Edit row action
- What: The Wave 3 spec for 9.3.B says "row Edit + hero-image management" for the Session browser, and the Mix browser should similarly have an edit affordance. The landed
CmsSessionBrowserandCmsMixBrowserprovide the medium-specific action (hero upload / waveform generate) but no Edit button linking to the standard release edit page (/tracks/album/{name}/editviaBatchEdit). - Why: Without the Edit button, an admin cannot rename a session, change its artist, update its genre, or swap its cover art from the browser. The only path is navigating to
/tracks, finding the session track, and editing it from there — which itself is now off the nav (9.5.D). - Shape: Add a
MudButton(orMudIconButton) per row linking to/tracks/album/@Uri.EscapeDataString(context.Release.Title)/editin both browsers, matching theCmsAlbumBrowserpattern. No new components or endpoints. - Acceptance criteria: Each row in
CmsSessionBrowserandCmsMixBrowserhas an Edit button that navigates toBatchEditfor that release. The edit page loads the release's tracks and release-level fields correctly.
9.5.F — CmsSessionBrowser / CmsMixBrowser structural duplication (DRY debt)
- What: Both browsers share an identical structural skeleton: a
LoadAsyncmethod with_loading/_rowsfields, anOnInitializedAsync→LoadAsynccall, aThumbUrlstatic helper, snackbar error handling, and aMudTablewith cover-thumbnail + title + artist columns. Only the per-row action column and the row model differ. This is copy-paste, not composition. The Phase 9 intro promises "a new medium is one entry, one file" — with this structure, a new medium browser is instead two files of boilerplate plus one file of new logic. - Why: Manageable now at three media, but violates the open/closed discipline the phase established. The right fix is a
MediumBrowserBaseabstract base (or a parameterizedCmsMediumBrowsercomponent with an action-column slot), reducing each browser to its medium-specific action markup only. - Shape: Extract a
CmsMediumBrowserBaseclass (analogous toMediumBrowseBaseon the public site) carrying:_loading,_rows,OnInitializedAsync,LoadAsync,ThumbUrl. Subclasses supply theReleaseMediumand the per-row action column. The table structure (cover, title, artist, actions) is rendered in the base or via a sharedCmsMediumTableRazor component with anActionContentRenderFragmentparameter. A new medium browser is then a subclass that overrides the medium enum and implements the action fragment. - Acceptance criteria:
CmsSessionBrowserandCmsMixBrowserno longer duplicateLoadAsync/ThumbUrl/ the error-snackbar pattern. A third medium browser (hypothetical) would require only the medium-specific action markup, with zero structural boilerplate. - Note: This is structural debt, not a functional gap. Mark
[nice-to-have]if Wave 5 is time-boxed. The functional items (A–E) are the priority; F can defer to Wave 6 if needed.
Dependency summary for Wave 5: A and B are independent of each other (parallel tracks) and are the highest priority — both are data-correctness blockers for Session/Mix releases created since Wave 3 landed. C depends on A and B being stable (so the detail VM resolves tracks for correctly-typed releases). D and E are independent nav/UI fixes. F is independent structural debt.
Working with this file
- Add items by extending an existing phase first; only create a new phase when the addition genuinely doesn't fit any of 1–5. Phase numbers are organisational, not sequencing.
- When something lands, move it to
COMPLETED.mdrather than deleting it. Keep the original "What / Why / Shape" body intact so the history reads as a record of the decision, not just the outcome. - Mark genuinely uncertain items
[speculative]so future readers can tell what is direction vs. commitment. - Open questions belong in the item that raises them, not in a separate "questions" list — they expire when the item does.