29 — Version-Scoped Approval
Active spec — decisions confirmed by the owner 2026-06-11; backend half implemented the same day. Governed by
00-ROADMAP.md. Answers the doc-24[P2]item "version-scoped approval" and the owner's question: how do approval elements relate to the correct record/data/element when a track has multiple versions?
The problem
track.releaseStatus is { title, version, cover } — three strings on the Track. The version dimension was never linked to any audio file, so the moment a track has 2+ versions, "Version: Approved" is ambiguous (approved which one?). Reviews happen against a specific listen; the data must record that.
The model (confirmed)
- Title + Cover stay track-level — they're about the track's name/artwork, not any version.
- Version approval lives on the File. Each audio version carries its own verdict:
File.reviewStatus('No status' | 'Under review' | 'Not approved' | 'Approved') +reviewStatusBy(user ref) +reviewStatusAt(date). The unit of approval is the exact file the reviewer was listening to. - Back-compat mirror:
track.releaseStatus.versionalways mirrors the final version'sreviewStatus. Web + legacy clients keep reading/writing the track-level field unchanged. - New upload → Under review. A new version becomes final on upload (existing behavior); its
reviewStatusstarts at Under review and the mirror resets with it — an "Approved" track correctly drops back into the review loop when fresh audio lands.
Backend (✅ implemented 2026-06-11)
| Piece | Where |
|---|---|
reviewStatus / reviewStatusBy / reviewStatusAt props | file.schema.ts (plain string to avoid file→track type dependency; default 'No status') |
PATCH /track/:trackId/file/:fileId/review-status { status } | track.controller.ts → track.service.updateFileReviewStatus |
Permission gate (owner/admin), file-belongs-to-track check, collaborator notification (V{n} of "{track}" set to {status}) | mirrors updateReleaseStatus |
Mirror to releaseStatus.version when the file is final (isFinalVersion or audioFinal match) | same service method |
Upload defaults: new version (and a new track's first audio) → Under review + mirror reset | createNewFile / createTrack |
Data backfill: 33 existing audio files — finals inherited their track's releaseStatus.version, others 'No status' (additive, safe for the not-yet-redeployed prod image) | one-off script, run 2026-06-11 |
iOS (✅ implemented 2026-06-11, same session — build green, pending visual QA)
MediaFiledecodesreviewStatus(permissive optional).- The player's Version chip (header
ApproveBar, newversionStatusOverride) binds tocurrentFile: shows the loaded version's ownreviewStatus; tapping PATCHes the per-file endpoint (PlayerViewModel.updateVersionReviewStatus→ refetch,currentIndexpreserved); switching versions switches the chip's value. Title/Cover chips keep the track-level PATCH. - Still open (cheap follow-ups): per-version status dot on the version-switcher
VersionCards; surfacereviewStatusBy/Atin the Portal ("Approved by Ana, Jun 7"). - ⚠️ The release/TestFlight build talks to the deployed API — the new endpoint only exists there after the next container deploy (doc 27 step 2; the Version chip will get a 404 on prod until then). Local dev works immediately.
Future (out of scope now)
Review history per file (multiple verdicts over time), per-dimension reviewers/quorums, and surfacing reviewStatusBy/At in the Portal ("Approved by Ana, Jun 7") — cheap UI add once the fields flow through.
Generalized 2026-06-13: these "future" items (history, per-dimension reviewers/quorums) are now owned by
32-APPROVAL-MODEL.md, which lifts this per-file pattern into a full dimensional approval model — approval on cover / lyrics / tracklist / master + a release-gate rollup, surfaced in Portals (doc 26), not only the Wave. This doc stays the source of truth for the audio-version dimension specifically.
Cross-references
- UX backlog item it resolves:
24-UX-BACKLOG.md(Approval & metadata) - File-granularity direction (sharing):
26-IDENTITY-SHARING-NAVIGATION.md - Deploy runbook (endpoint goes live with next redeploy):
27-TESTFLIGHT-BACKEND-DEPLOY.md