Skip to content

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.version always mirrors the final version's reviewStatus. 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 reviewStatus starts 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)

PieceWhere
reviewStatus / reviewStatusBy / reviewStatusAt propsfile.schema.ts (plain string to avoid file→track type dependency; default 'No status')
PATCH /track/:trackId/file/:fileId/review-status { status }track.controller.tstrack.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 resetcreateNewFile / 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)

  • MediaFile decodes reviewStatus (permissive optional).
  • The player's Version chip (header ApproveBar, new versionStatusOverride) binds to currentFile: shows the loaded version's own reviewStatus; tapping PATCHes the per-file endpoint (PlayerViewModel.updateVersionReviewStatus → refetch, currentIndex preserved); 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; surface reviewStatusBy/At in 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

Ctrl-Audio Platform Documentation