Files
deepdrft/PLAN.md
T
daniel-c-harvey 05130aaed2
Deploy DeepDrftAPI / Build, Publish & Bundle (push) Successful in 2m10s
Deploy DeepDrftManager / Build & Publish (push) Successful in 1m25s
Deploy DeepDrftPublic / Build & Publish (push) Successful in 4m3s
Deploy DeepDrftAPI / Deploy (push) Successful in 1m33s
Deploy DeepDrftManager / Deploy (push) Successful in 1m29s
Deploy DeepDrftPublic / Deploy (push) Successful in 1m30s
docs: record Wave 8 tracks 8.C and 8.E landed; Phase 9 gate met
2026-06-13 22:43:53 -04:00

22 KiB
Raw Blame History

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 concurrencyVaultIndexDirectory no longer drops the lock before its async disk write; the index file can no longer be clobbered by interleaved writers.
  • Repository semanticsTrackRepository.Update now fails-fast when an Id is not found instead of silently issuing an INSERT.
  • 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, IAsyncDisposable on 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 HttpClient request kicked off when the current track passes a configurable threshold (e.g. last 10 seconds). Bytes accumulate into a staged StreamDecoder instance 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 IPlayerService or 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 future PlaylistService orchestrates 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 PlaybackScheduler instances suffice — each owns its own gain node, crossfaded via GainNode.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 first AudioBufferSourceNode.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 processStreamingChunk aborts 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); await resolves immediately and the next initialize() can run against a not-yet-closed context.
    • iOS Safari < 15 had streaming-fetch quirks; HttpCompletionOption.ResponseHeadersRead behaviour 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 — detect webkitAudioContext and poll state === "closed" with a short timeout instead of trusting the await. 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.

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 /tracks view (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. The GET api/track/page endpoint already accepts album= and genre= query filters, so the API substrate is ready; the missing piece is the CMS browse UI and the filter plumbing in TrackList.
  • 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 into GetPagedAsync is 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 polymorphic MediaEntity with discriminator. The choice affects how much code in TrackService / TrackController can be reused.
    • New vault type(s). MediaVaultType.Media exists and is the obvious home for video; sessions are probably still Audio.
    • New routes, new UI surfaces, new player considerations (video has its own playback element and does not go through the WAV decoder).
  • 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 DeadLetterLog recording orphaned entryKeys 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.md files need writing/rewriting per the brief in DOC_PLAN.md. Five are rewrites (drift from the .NET 10 upgrade and structural moves); three are new (DeepDrftWeb.Services, DeepDrftContent.Services — the two libraries where most domain logic now lives — plus the open question on DeepDrftContent.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, referencing MediaPath that has been EntryKey for two migrations, describing a FileDatabase/ tree inside DeepDrftContent that has moved out, and missing entirely for the two *.Services libraries.
  • 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.md judgement 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.
  • ITrackService interface. 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 14 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 24 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.

Waves 17 are landed (COMPLETED.md §9). Wave 6 closes two functional gaps a post-landing smoke-test survey surfaced — surfaces the medium taxonomy did not reach, not regressions. Wave 7 hardens the single-track-per-medium rule from a CMS-form convention into a real domain invariant — the one place the medium taxonomy is declared but not enforced below the UI.

9.8 Wave 8 — Remediation (from Daniel's Phase-9 testing pass)

Daniel tested the landed Phase 9 surface end-to-end and produced a punch-list of corrections before the phase is called complete. These are not new features — they are the gap between what the Wave 17 specs built and what hands-on use wants. The theme is the same one Phase 9 has carried throughout: the medium taxonomy reaching every surface it should, and the browse surfaces matching the mental model rather than the implementation's first cut.

Two surfaces dominate: the CMS Release Archive (the card-grid landing is the wrong shape — Daniel wants medium tabs, not navigate-away cards) and the public Archive (the three-card overview is dead weight; the searchable all-releases view is the archive — release-cardinal, decided). The Mix Visualizer redesign (8.K) is pulled out of Phase-9-completion scope — Phase 9 closes without it — but is now documented in full: the interview ran and product-notes/phase-9-mix-visualizer-redesign.md is a finished, implementation-ready design spec for a post-Phase-9 wave.

Open questions resolved (Daniel, 2026-06-13): 8.H is decided H2 (a new release-cardinal searchable browser at /archive; cascade: /tracks demoted from nav, route kept; mobile ARCHIVE → the browser; three-card overview fully retired); 8.I drops GENRES from the nav only (route kept); 8.F makes the Session hero optional-but-warn-if-missing; 8.E defaults the ALL-tab Add Track to Cut with the medium selector staying user-changeable. A new track 8.L consolidates the release-name/track-name pair into a single name for single-track media (derived track name kept synced, decided), and 8.M (split off 8.L) retires the legacy TrackNew/TrackEdit forms by folding them into the batch forms to reduce code surface.

Full track decomposition, acceptance criteria, and parallel/dependent analysis: product-notes/phase-9-wave-8-remediation.md. The tracks in brief:

CMS (DeepDrftManager):

  • 8.M — Retire the legacy single-track forms; consolidate onto the batch forms. Fold TrackNew (/tracks/new) and TrackEdit (/tracks/{Id:long}) into BatchUpload/BatchEdit's single-track branch and retire them, reducing duplicate form surface (Daniel, 2026-06-13: "consolidate the forms and reduce the code surface"). Feasibility: retirement-with-reconciliation. TrackNew is a clean retirement (no live inbound link — every Add-Track points at /tracks/upload). TrackEdit requires reconciling an addressing-model gap: it is addressed by track id while BatchEdit is addressed by release title and loads the whole release — so the single-Cut-track edit from Track mode's per-row Edit (CmsTrackGrid/tracks/{id}, the one live inbound link) needs a decision (open the parent release, or address a single track within the batch edit). Architectural — staff-engineer scope (route-map change + two component removals + navigation-model decision). Not a Phase-9-completion gate; sequence after 8.L. (Independent of 8.L for build.)

Public site (DeepDrftPublic.Client):

Mix Visualizer — out of Phase-9-completion scope:

  • 8.K — Mix Visualizer redesign. [post-Phase-9, design-complete]. A windowed, playback-coupled, bottom-to-top scrolling waveform showing the currently-playing region; zoom couples to apparent scroll speed (Guitar-Hero model, anchored at 1 quarter note @ 180 BPM = 333 ms visible at max zoom); lava-lamp aesthetic (theme-aware gradients, glassy), strictly read-only; standard Canvas/WebGL, no tricks, well-commented. The datum analysis recommends switching the Mix loudness profile from a fixed 2048 buckets to constant-time-resolution capture (~333 samples/sec) so long mixes aren't under-sampled at max zoom. Phase 9 closes without this; it runs as a post-Phase-9 wave, dispatchable straight from the finished spec: product-notes/phase-9-mix-visualizer-redesign.md.

Dependency shape: 8.B is the foundation for the CMS tab work (8.A consumes the shared grid; 8.C/8.E layer on once 8.A lands). 8.L follows 8.G and coordinates with 8.E/8.F (same forms). 8.M (legacy-form retirement) follows 8.L and is architectural (route map + addressing decision). On the public side, 8.H (decided H2 — the new release-cardinal archive) gates 8.I. Phase 9 completion gate (8.A8.J + 8.L) is now fully met — the remediation wave is complete except the two explicitly-out-of-gate items (8.K post-Phase-9 visualizer; 8.M legacy-form retirement follow-on). Landed tracks (2026-06-13): 8.A, 8.B, 8.C, 8.D, 8.E, 8.F, 8.G, 8.H, 8.I, 8.J, 8.L.

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 15. Phase numbers are organisational, not sequencing.
  • When something lands, move it to COMPLETED.md rather 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.