# Deliberation: general — Claude Opus 4.6 **Date**: 2026-03-18 12:17 **Model**: Claude Opus 4.6 **Type**: general **Artifact**: ## Cloud Download for Export — Draft Proposal ### Problem Cloud tracks cannot be included in DAW ex --- # Requirements Critique: Cloud Download for Export ## Is This Solving the Right Problem? **Yes, but the problem statement undersells the complexity at the data layer.** The proposal correctly identifies the user-facing gap (cloud tracks skipped in export), but the hardest question isn't the download mechanics — it's the model boundary between `ChadTrack` (transient Codable struct) and `Track` (SwiftData `@Model`). The proposal's own "Open questions" section flags this, which is good. But this isn't an open question — **it's the critical prerequisite that determines the entire shape of the solution**. You cannot defer it. Let me explain why. --- ## The ChadTrack → Track Boundary Is the Real Problem The user-provided context says: > Cloud tracks are currently transient (ChadTrack is Codable struct, not SwiftData) > But Track @Model has cloud fields: isCloud, cloudStreamPath, cloudTrackId This creates two possible states of the world: 1. **When a user adds a cloud track to a playlist, a `Track` @Model is created with `isCloud=true`.** If this is true, the export pipeline already has a `Track` to work with, and you just need to fill in the file. The proposal's architecture is roughly correct. 2. **Cloud tracks in playlists are held as `ChadTrack` references that never become `Track` @Models.** If this is true, the export pipeline will never encounter them — `PlaylistEntry → Track` won't yield them at all, and the `guard track.hasLocalFile else { continue }` isn't even the relevant skip point. **They're invisible to export, not just skipped.** **The proposal assumes scenario (1) without confirming it.** If scenario (2) is the reality, the fix isn't "add a DownloadService" — it's "materialize ChadTrack into Track when adding to a playlist" plus download. That's a fundamentally different scope. **Action required:** Before any implementation work, trace the actual code path for "user adds cloud track to playlist." Does a `Track` @Model get created? If not, the proposal needs a significant addition. --- ## User Journey Gaps ### What happens if the download fails mid-export? Both prior reviews flagged this. The proposal still doesn't answer it. This isn't optional — it's a UX contract: - **Option A: Fail the entire export.** Simple, safe, but frustrating if 1 of 30 tracks fails. - **Option B: Export with missing tracks, report which ones failed.** More complex but more useful for DAW workflows where partial sessions are common. - **Option C: Export what you can, substitute silence/placeholder for failures.** DAW-friendly but surprising. **Recommendation:** Option B. DAW users are accustomed to "missing media" dialogs. Export should succeed with a manifest of missing tracks. ### The "just-in-time download during export" UX is awkward The proposal has download happening *inside* the export flow. Consider what this looks like: 1. User clicks "Export" 2. Progress bar appears... but nothing is being exported. Files are downloading. 3. Downloads finish. Now exporting starts. 4. Export finishes. This is confusing. The user didn't ask to download — they asked to export. Two things are happening under one progress indicator. **Better UX options:** - **Pre-flight check:** Before export starts, show a dialog: "3 cloud tracks need to be downloaded (45 MB). Download and export?" with a cancel option. Then show download progress separately from export progress. - **Background pre-cache:** Download cloud tracks lazily when they're added to a playlist (not at export time). Export just uses the cache. **[CHECK THIS]** — this may be over-engineering for MVP but it eliminates the export-time surprise entirely. ### Standalone download (right-click → Download) The proposal asks whether to include this. **Don't, for v1.** It introduces UI surface, state management (what does a "downloaded" cloud track look like in the UI?), cache invalidation questions, and user expectations about persistence. Keep it export-scoped. --- ## Architecture Feedback ### Cache path design ``` ~/Library/Caches/MixBoard/CloudTracks/{trackId}.{ext} ``` **Problem 1: File extension.** Where does `{ext}` come from? The context says `cloudStreamPath` exists — if it's a URL path like `/tracks/123/audio.wav`, you can extract the extension. If it's `/api/stream/123`, you can't. The proposal doesn't address this. Prior reviews flagged it. **You need to handle this explicitly:** - Try `cloudStreamPath` path extension first - Fall back to `Content-Type` / `Content-Disposition` from the HTTP response - If neither works, fail with a clear error — don't guess **Problem 2: Cache invalidation.** `~/Library/Caches/` can be purged by macOS at any time. This is fine if you treat it as a true cache (re-download if missing). But if you store the path in `Track.localCachePath` (as proposed), you now have a stale reference. **Don't persist the cache path on the model.** Instead, have `DownloadService` check for the file at the deterministic path at call time — the path is derivable from `trackId` + extension, so you don't need to store it. ### DownloadService API ```swift download(track: ChadTrack, apiClient: ChadMusicAPIClient) async throws -> URL ``` **[CHECK THIS]**: The parameter type is `ChadTrack`, but the export pipeline works with `Track` (@Model). If scenario (1) above is true, you'll need to either: - Accept `Track` and extract cloud fields from it, or - Extract `ChadTrack`-equivalent data from `Track` before calling This is a direct consequence of the unresolved model boundary. Don't design the API until you know which type you're working with at the call site. ### Async in a synchronous pipeline The proposal flags this as an open question. The answer is straightforward: `MixExporter.copyAudioFiles` needs to become `async`. If it's currently called from a `Task` or background queue, this is a small change. If it's called synchronously from a SwiftUI action, you need a `Task { }` wrapper. Check the current call site. ### Concurrency limits Both prior reviews flagged this. **Add `maxConcurrentDownloads` (e.g., 3-4).** Use `TaskGroup` with a semaphore pattern or a custom `AsyncStream`-based throttle. Unbounded concurrent downloads to the same server will either get rate-limited, OOM on large files, or both. --- ## What's Missing From the Proposal 1. **Auth headers on download requests.** The context confirms `ChadMusicAPIClient` has `authHeaders`. The `DownloadService` must apply them to the `URLRequest`. The proposal doesn't mention this. Even if the server currently serves unauthenticated streams, **always send auth** — server policy can change. 2. **Disk space check.** Before downloading N tracks for export, estimate total size (if the API provides track file sizes) and compare to available disk space. A 2GB playlist download into a nearly-full disk is a bad time. 3. **Cleanup policy.** When are cached files deleted? Options: after export completes, after N days, never (let macOS manage `~/Library/Caches/`). The proposal is silent on this. For MVP, "let macOS manage it" is fine, but document it. 4. **Cancellation.** User should be able to cancel the download/export. With `async/await` and `Task`, this means checking `Task.isCancelled` and supporting `task.cancel()`. The proposal doesn't mention it. --- ## Hallucination Check - `MixExporter.copyAudioFiles` — referenced in user context ✓ - `ChadMusicAPIClient.streamURL(for:)` — referenced in user context ✓ - `ChadMusicAPIClient.authHeaders` — referenced in user context ✓ - `~/Library/Caches/MixBoard/CloudTracks/` — proposed path, reasonable for macOS convention ✓ - `ChadTrack` as Codable struct — referenced in user context ✓ - `Track` as SwiftData @Model — referenced in user context ✓ - `UploadService` as existing pattern — referenced in user context ✓ - "~150 lines" estimate — **[UNVERIFIED]**, plausible but meaningless at this stage No hallucinations detected. All entity names trace back to user-provided context. --- ## Summary Verdict The proposal has the right instinct but is premature. **The model boundary question (ChadTrack vs Track in playlists) must be resolved first** — it determines whether this is a 1-service addition or a 2-layer refactor. The download mechanics are relatively straightforward once you know what type you're downloading *for*.
Session State **Blocking findings**: - **Ambiguous requirements**: ChadTrack → Track materialization path unresolved. Must confirm whether cloud tracks added to playlists become Track @Models before designing DownloadService API or export integration. - **Ambiguous requirements**: No failure policy for failed downloads during export (fail all vs. partial export). Carried forward from prior reviews, still unaddressed. - **Performance**: No concurrency limits on batch downloads (carried forward, still unaddressed). **Advisory findings**: - Don't persist `localCachePath` on Track model; derive it from trackId + extension at call time to avoid stale references. - Add pre-flight dialog before export-time downloads ("3 tracks, 45 MB — proceed?") rather than silent just-in-time download. - File extension must be resolved from cloudStreamPath, then Content-Type/Content-Disposition fallback, then fail — don't guess. - Always send authHeaders on download requests regardless of current server policy. - Add disk space pre-check before batch download. - Add cancellation support (Task.isCancelled). - Define cleanup policy for cached files (MVP: let macOS manage ~/Library/Caches/). - Don't add standalone download UI in v1. - DownloadService parameter type (ChadTrack vs Track) depends on blocking finding #1. **Decisions**: - Recommended: Export-scoped MVP only, no standalone download, no model schema change. - Recommended: Option B failure policy (export succeeds with missing-track manifest). **Open Questions**: - Does adding a cloud track to a playlist create a Track @Model with isCloud=true? (BLOCKING) - Is `MixExporter.copyAudioFiles` called from an async context already? (Determines difficulty of async conversion) - Does `cloudStreamPath` contain a file extension, or is it an opaque API path? - Does the Chad Music API provide file size metadata for tracks? **Key Entities**: - `MixExporter.copyAudioFiles` (existing, needs modification) - `DownloadService` (proposed, new) - `ChadMusicAPIClient` (existing — `streamURL(for:)`, `authHeaders`) - `Track` (SwiftData @Model with isCloud, cloudStreamPath, cloudTrackId) - `ChadTrack` (transient Codable struct) - `PlaylistEntry` (SwiftData, relates to Track) - `UploadService` (existing pattern reference) - `~/Library/Caches/MixBoard/CloudTracks/` (proposed cache location)