docs(plan): add Phase 11 commitment 9 (release GUID identifiers, wave 11.H)
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# Phase 11 — Public Site Enhancements
|
||||
|
||||
Status: spec / design. Author: product-designer. Date: 2026-06-15 (revised same day after Daniel
|
||||
resolved the open questions and expanded scope). **Plan only — no code edits made by this doc.**
|
||||
resolved the open questions and expanded scope); **2026-06-16 — added commitment 9 (release GUID
|
||||
identifiers; §3e, wave 11.H).** **Plan only — no code edits made by this doc.**
|
||||
|
||||
Cross-references: `PLAN.md §11` (the concise phase entry), `PLAN.md §1.3` (preload/queue — now
|
||||
**absorbed into this phase**), `product-notes/phase-9-release-medium-types.md` (the medium model —
|
||||
@@ -21,10 +22,12 @@ demoted from the nav (route kept). That work landed and is stable on dev (2026-0
|
||||
|
||||
Phase 11 is the **next coherent pass over the public listening surface**. Daniel's hands-on use
|
||||
surfaced an initial four commitments; on 2026-06-15 he resolved the open questions and expanded the
|
||||
scope to **eight**. They share one spine: **make the release the cardinal unit of the public site,
|
||||
make every navigation an addressable, shareable URL, and make the album a first-class playable
|
||||
object** (ordered, queue-able, shareable) — and now, **a release that can describe itself** in prose.
|
||||
The eight:
|
||||
scope to **eight**; on 2026-06-16 he added a **ninth** — switch releases to GUID identifiers so the
|
||||
id is no longer a transparent sequential integer. They share one spine: **make the release the
|
||||
cardinal unit of the public site, make every navigation an addressable, shareable URL, and make the
|
||||
album a first-class playable object** (ordered, queue-able, shareable) — and now, **a release that
|
||||
can describe itself** in prose and **identify itself by an opaque, non-enumerable handle**.
|
||||
The nine:
|
||||
|
||||
1. **Cuts detail page** (`/cuts/{id}`) — structural; the phase's center of gravity.
|
||||
2. **Player-bar release-title → release detail** via a medium→route resolver — structural.
|
||||
@@ -39,6 +42,15 @@ The eight:
|
||||
8. **Release Description field** — **new scope (2026-06-15)**; a multiline free-text field on the
|
||||
base release (all media), edited from the CMS add/edit forms and rendered as a text block on every
|
||||
release detail page. Unlike commitment 5, this **is** a genuinely new column + migration. See §3d.
|
||||
9. **Release GUID identifiers** — **new scope (2026-06-16)**; the release id Daniel sees in URLs and
|
||||
the API is a transparent sequential integer (`/cuts/1`, `/cuts/2`, …) that leaks catalogue size
|
||||
and ordering. Switch releases to a GUID identifier "similar to how tracks are keyed," swept
|
||||
end-to-end. **The reframe (§3e): tracks did *not* change their PK type — they front the int PK
|
||||
with an app-minted GUID *string* (`EntryKey`) as the public handle and keep the int `Id` private.
|
||||
That same move is the recommendation here, because the int PK is `BaseEntity.Id` from the
|
||||
`Cerebellum.BlazorBlocks.Models` framework and is hardwired to `long` across `Repository<>` /
|
||||
`Manager<>`.** See §3e for the surface map, the framework constraint, the four open decisions, and
|
||||
the sequencing (this wave **must follow 11.B–11.E** — it sweeps the files they create/edit).
|
||||
|
||||
This is not a greenfield phase — most of the scaffolding it needs already exists (the medium browse
|
||||
pages already share a `ReleaseGallery` card component; the detail pages already share
|
||||
@@ -674,6 +686,218 @@ and a Session/Mix touch.**
|
||||
|
||||
---
|
||||
|
||||
## 3e. Commitment 9 — release GUID identifiers (the transparency reframe)
|
||||
|
||||
> **Daniel, verbatim (2026-06-16):** "The integer IDs for releases are too transparent — switch to
|
||||
> GUID identifiers for releases instead of the int IDs, similar to how tracks are keyed. Implement
|
||||
> this from the entity up and replace all the int IDs with the GUIDs."
|
||||
|
||||
**Recommendation: do NOT change the release primary-key type. Front the existing `long` PK with an
|
||||
app-minted GUID *string* — a new `PublicId` column — and make *that* the only release identifier the
|
||||
public site and API expose. The int `Id` stays the internal PK; the GUID becomes the addressable
|
||||
handle. This is exactly, mechanically, what tracks already do with `EntryKey`.** The brief's literal
|
||||
"replace all the int IDs with GUIDs from the entity up" is the right *intent* (kill the transparent
|
||||
sequential id in the URL) but, taken as "change the PK type to `Guid`," it collides head-on with a
|
||||
framework constraint — and the "similar to how tracks are keyed" steer is the clue that the
|
||||
non-colliding path is the intended one. The reframe is the headline; the surface map and the
|
||||
alternative (a true PK swap, if Daniel wants it despite the cost) follow.
|
||||
|
||||
### 3e.1 How tracks are keyed today — the model to copy (read against live source, 2026-06-16)
|
||||
|
||||
A track has **two** identifiers, and only one is public:
|
||||
|
||||
| Identifier | Type | Origin | Exposure | Evidence |
|
||||
|---|---|---|---|---|
|
||||
| `TrackEntity.Id` | `long` (framework `BaseEntity.Id`) | DB identity column (`bigint`, `IdentityByDefaultColumn`) | **Private.** Used only by ApiKey-gated CMS ops (`PUT/DELETE api/track/{id:long}`). Never in a public URL. | `TrackConfiguration.cs:18`, `NormalizeReleaseTrack.cs:20` |
|
||||
| `TrackEntity.EntryKey` | `string` | **App-minted at creation** — `Guid.NewGuid().ToString()` | **Public.** The track-detail route is `/track/{EntryKey}`; the streaming/waveform/by-key endpoints are all `EntryKey`-addressed. | `TrackContentService.cs:55`, `TrackConfiguration.cs:23` |
|
||||
|
||||
So tracks never put their sequential int in front of a listener. The opaque GUID string *is* the
|
||||
public handle; the int PK is an implementation detail behind the ApiKey wall. **"Similar to how
|
||||
tracks are keyed" therefore reads most faithfully as: give releases their own opaque app-minted GUID
|
||||
string handle and address the public site by it — not as "retype the PK."** (Note one divergence: a
|
||||
track's `EntryKey` doubles as the FileDatabase vault entry id, so it has a second job. A release has
|
||||
no vault entry, so its GUID is purely an identifier — see the column-name note in §3e.4.)
|
||||
|
||||
### 3e.2 The framework constraint — why a true PK swap is not a local change
|
||||
|
||||
The release PK is **not defined in this repo.** `ReleaseEntity : BaseEntity, IEntity` inherits `Id`
|
||||
from `Cerebellum.BlazorBlocks.Models` (NuGet package, version 10.3.30 — ships as a compiled
|
||||
`Models.dll`, no source). The same package supplies `Repository<DeepDrftContext, ReleaseEntity>` and
|
||||
`Manager<…>`, whose CRUD surface (`GetByIdAsync`, `Delete`, etc.) is typed on the base `Id`. Three
|
||||
hard signals that the framework hardwires `Id` to `long`:
|
||||
|
||||
1. **`TrackManager.cs:307`** — comment: *"Delete(long) → Result is inherited from `Manager<>` and
|
||||
satisfies `ITrackService.Delete`."* The inherited delete takes `long`.
|
||||
2. **`ReleaseManager.GetByIdAsync(long id)`** and **`SetSessionHeroImageAsync(long releaseId)`** etc.
|
||||
are all typed `long` (`ReleaseManager.cs:81/97/110/130`).
|
||||
3. **The migration** creates `id` as `bigint` / `IdentityByDefaultColumn` (`NormalizeReleaseTrack.cs:20`).
|
||||
|
||||
Unless the package exposes a *generic-keyed* base (e.g. `BaseEntity<TKey>` / `Repository<…, TKey>`)
|
||||
— which the consuming code shows no sign of, and which would itself be a breaking generic-arity change
|
||||
across `TrackEntity`, `SessionMetadata`, `MixMetadata`, and every `Manager<>`/`Repository<>` derived
|
||||
type — **changing `ReleaseEntity.Id` to `Guid` is a framework fork, not an entity edit.** This is the
|
||||
load-bearing reason the recommendation is the additive GUID *column*, not a PK retype. It is also
|
||||
exactly why tracks solved the transparency problem with `EntryKey` rather than by retyping their own PK.
|
||||
|
||||
> **One thing to verify before finalizing the wave (staff-engineer, at implementation):** decompile or
|
||||
> inspect `Cerebellum.BlazorBlocks.Models` 10.3.30 to confirm `BaseEntity.Id`/`BaseModel.Id` are
|
||||
> non-generic `long`. If the package *does* offer a generic key, the PK-swap alternative (§3e.6)
|
||||
> becomes viable and the decision in §3e.5(1) reopens. The recommendation assumes the non-generic case
|
||||
> the consuming code strongly implies.
|
||||
|
||||
### 3e.3 The cross-stack surface map (every place a release id appears)
|
||||
|
||||
What the public-facing id touches today, and what each becomes under the **recommended `PublicId`
|
||||
column** approach. "→ GUID" means "switch from the int `Id` to the new GUID handle"; "unchanged"
|
||||
means it stays on the internal int PK behind the ApiKey wall.
|
||||
|
||||
| Layer | Site | Today | Under recommendation |
|
||||
|---|---|---|---|
|
||||
| Entity | `ReleaseEntity` | `Id : long` (PK) | **add** `PublicId : Guid` (app-minted, unique index); `Id` **unchanged** |
|
||||
| EF config | `ReleaseConfiguration` | `id` PK + columns | **add** `public_id` column + unique index; PK unchanged (`ReleaseConfiguration.cs:9-93`) |
|
||||
| Satellites | `SessionMetadata.ReleaseId`, `MixMetadata.ReleaseId` | `long` FK to release PK | **unchanged** — internal FK stays on the int PK (1:1 join is server-side, never exposed) |
|
||||
| Track FK | `TrackEntity.ReleaseId` | `long?` FK | **unchanged** — internal join (`TrackConfiguration.cs:42-50`) |
|
||||
| DTO | `ReleaseDto` | `Id : long` (from `BaseModel`) | **add** `PublicId : Guid`; `Id` still present but not used in public links |
|
||||
| Converter | `TrackConverter.Convert(ReleaseEntity/ReleaseDto)` | maps `Id` | **add** `PublicId` to both maps (`TrackConverter.cs:19-67`) |
|
||||
| Detail routes | `/cuts/{id:long}`, `/sessions/{id:long}`, `/mixes/{id:long}`, `/tracks/{Id:long}` redirect | `:long` route constraints | **`:guid`** constraints; pages load by `PublicId` |
|
||||
| Route resolver | `ReleaseRoutes.DetailHref(long id, ReleaseMedium)` + `(ReleaseDto)` overload | takes `long id` | **takes `Guid publicId`** (overload reads `release.PublicId`) — landed in 11.B, this wave re-types it (`ReleaseRoutes.cs:21-29`) |
|
||||
| Share | `SharePopover.ReleaseId` (11.E, `long?`) | `long?` | **`Guid?`** — 11.E just added it; this wave re-types it |
|
||||
| Data service | `IReleaseDataService.GetById(long)` + the `releaseId`-filtered track page call | `long` | a **`GetByPublicId(Guid)`** read path; the public track page filters by `PublicId` (or resolves PublicId→int server-side) |
|
||||
| **Public** API | `GET api/release/{id:long}`, `…/{id:long}/mix/waveform`, the `releaseId` query on `GET api/track/page` | `:long` route / `long?` query | **`:guid`** / `Guid?` — public reads address by `PublicId` (`ReleaseController.cs:73/111/134/165`, `TrackController` `releaseId`) |
|
||||
| **CMS/ApiKey** API | `DELETE api/track/release/{id:long}`, `POST …/mix/waveform`, `…/session/hero-image` | `:long` | **judgment call (§3e.5(3))** — these sit behind the ApiKey wall and are not the transparency target. Recommend leaving them on the int PK to keep the blast radius at the public surface; the CMS already holds the full `ReleaseDto`. |
|
||||
| JSON | `ReleaseDto` serialization | `Id` (number) | `PublicId` serializes as a GUID string — case-insensitive client deserialization already configured; verify the client reads `PublicId` |
|
||||
| Rendered/parsed | Cut/Session/Mix detail page id parse; player-bar resolver; Archive/Cuts card hrefs | parse/compare `long` | parse/compare `Guid` at the public surface |
|
||||
|
||||
**The honest scope line:** the GUID reaches *every public addressing site* (routes, resolver, share,
|
||||
the public read path, the public-facing API params) and **stops at the ApiKey wall** — the internal
|
||||
FKs (`TrackEntity.ReleaseId`, the satellite `ReleaseId`s), the PK, and the CMS-only endpoints stay on
|
||||
the int. This is the minimal surface that satisfies "the id Daniel sees is no longer transparent"
|
||||
while honoring the framework constraint and matching the track model exactly.
|
||||
|
||||
### 3e.4 GUID generation site — app-side, matching tracks
|
||||
|
||||
`EntryKey` is minted **app-side** (`Guid.NewGuid().ToString()` in `TrackContentService`), not by a DB
|
||||
default. Two reasons to follow that here rather than a Postgres `gen_random_uuid()` server default:
|
||||
(1) **consistency** — "similar to how tracks are keyed" includes *where the value is born*; (2) the
|
||||
release is created in app code (the `FindOrCreateRelease` path the upload flow runs), so the mint site
|
||||
is already in hand. Set `PublicId = Guid.NewGuid()` at release creation in that path. **No DB default,
|
||||
no `HasDefaultValueSql`** — the column is app-populated and carries a unique index. (If Daniel prefers
|
||||
a DB default for defense-in-depth, `gen_random_uuid()` via Npgsql is available, but it diverges from
|
||||
the track model for no real gain — recommend app-side.)
|
||||
|
||||
**Column-name note:** tracks call this `EntryKey` because it doubles as the vault entry id. A release
|
||||
GUID has no vault job, so name it for what it is — recommend **`PublicId`** (`public_id` column),
|
||||
`Guid` type, not a stringified GUID. (Tracks store a *string* because the FileDatabase entry id is a
|
||||
string; a release has no such constraint, so a native `uuid` column is cleaner and indexes better.)
|
||||
|
||||
### 3e.5 The four open decisions (Daniel's calls — recommendations given, none decided unilaterally)
|
||||
|
||||
1. **PK strategy — additive GUID column vs. true PK retype. THE GATING DECISION.** *Recommend the
|
||||
additive `PublicId` column* (§3e.1–3e.2): it matches the track model, avoids a framework fork, keeps
|
||||
the migration trivial (add a column + backfill GUIDs, no FK rewrites), and the int PK keeps serving
|
||||
the internal joins it already serves well. The true PK retype (§3e.6) is the literal reading of the
|
||||
brief but costs a framework fork (or confirmation the package is generic-keyed) **and** a full FK
|
||||
rewrite across two satellites + the track table. **This is the fork the whole wave hinges on —
|
||||
Daniel must pick before anything is authored.**
|
||||
|
||||
2. **Existing-data conversion strategy.** Under the recommended column approach this is small but real:
|
||||
**mint a GUID for every existing release row.** Either (a) a data-migration step (`UPDATE release
|
||||
SET public_id = gen_random_uuid() WHERE public_id IS NULL` inside the same migration, then mark the
|
||||
column non-null + unique — Postgres has `gen_random_uuid()` so the backfill needs no app pass), or
|
||||
(b) if there is **no production data to preserve**, a clean dev-DB reset. *What I can infer about
|
||||
live data:* the schema carries real migration history (eight applied migrations, a normalization
|
||||
data-migration that back-filled releases from tracks, a unique-title-artist constraint with a
|
||||
conflict-recovery path) — this reads like a database with **actual content**, not a throwaway. So
|
||||
*recommend (a) the in-migration backfill* and do **not** assume a reset is safe. **Confirm with
|
||||
Daniel whether the target DB holds data worth preserving** — that single fact decides (a) vs. (b).
|
||||
(Under the PK-retype alternative §3e.6, this decision is far heavier — it becomes a full FK
|
||||
rewrite — which is another reason to prefer the column approach.)
|
||||
|
||||
3. **Public URL exposure — raw GUID vs. slug/short-id.** Daniel's stated motivation is "int IDs too
|
||||
transparent," and a raw GUID in the URL (`/cuts/9f8a…`) fully satisfies that — it leaks neither
|
||||
count nor order. A slug (`/cuts/midnight-drift`) or short-id would be prettier but is a **separate
|
||||
feature** (needs a slug column, collision handling, and a slugify step) and is *not* what the brief
|
||||
asked for. *Recommend the raw GUID in the URL for this wave* (it is the direct, track-consistent
|
||||
answer — `/track/{EntryKey}` is already a raw GUID string and nobody has asked to prettify it), and
|
||||
**flag the slug as an adjacent future option** if Daniel later wants shareable-pretty URLs. Naming
|
||||
it so the door stays open without widening this wave.
|
||||
|
||||
4. **Migration apply is Daniel-gated** (consistent with 11.G). Authoring the migration (add `public_id`,
|
||||
backfill, unique index, non-null) is **in scope**; running `dotnet ef database update` is **not** —
|
||||
it applies on Daniel's go, exactly as `20260616035252_AddReleaseDescription` (11.G) is staged but
|
||||
not applied. **Coordinate the two migrations:** 11.G's `AddReleaseDescription` is authored-not-applied
|
||||
and also touches the `release` table; whichever applies second must be generated *after* the first so
|
||||
the EF model snapshot is linear (no divergent-snapshot conflict). Recommend authoring 11.H's migration
|
||||
**on top of** 11.G's (i.e. after 11.G is in the tree), and applying them in author order. Surface
|
||||
this ordering to Daniel as part of the migration go-ahead.
|
||||
|
||||
### 3e.6 The alternative considered — true PK retype (recorded, not recommended)
|
||||
|
||||
For completeness, the literal "change the PK to GUID" path, so the rejection is on record:
|
||||
|
||||
- `ReleaseEntity.Id` becomes `Guid` (requires a generic-keyed framework base, or a fork of
|
||||
`Cerebellum.BlazorBlocks.Models`).
|
||||
- `TrackEntity.ReleaseId`, `SessionMetadata.ReleaseId`, `MixMetadata.ReleaseId` all become `Guid` /
|
||||
`Guid?` FKs; the migration mints a GUID per release and **rewrites every FK value** to match
|
||||
(a multi-table data migration, ordered: mint release GUIDs → rewrite track FKs → rewrite satellite
|
||||
FKs → swap PK).
|
||||
- Every `Manager<>`/`Repository<>` `long`-typed CRUD call across tracks *and* releases shifts to `Guid`.
|
||||
|
||||
*Why rejected:* it is a framework fork (or a confirmation-gated generic-key assumption) plus a
|
||||
full FK-rewrite migration, for an outcome the additive column achieves at the public surface with a
|
||||
single new column and a backfill — and the additive approach is *what tracks already do*. The PK retype
|
||||
buys nothing the column doesn't, at materially higher risk. **Recommend §3e.1 (column); record §3e.6 as
|
||||
the considered-and-declined literal reading** in case Daniel specifically wants the PK itself retyped
|
||||
(in which case decision 3e.5(1) flips and the wave grows substantially).
|
||||
|
||||
### 3e.7 Sequencing — why 11.H must follow 11.B–11.E
|
||||
|
||||
This wave **sweeps the very files 11.B–11.E create or edit**, so authoring it concurrently would
|
||||
guarantee conflicts:
|
||||
|
||||
- **11.B** created `ReleaseRoutes.DetailHref(long id, …)` and the `/tracks/{Id:long}` redirect page —
|
||||
11.H re-types both to `Guid` (`ReleaseRoutes.cs`, `TrackRedirect.razor`). *(11.B is landed
|
||||
2026-06-16, so this dependency is already satisfied.)*
|
||||
- **11.C** folds Archive + Cuts cards into `ReleaseGallery` with the `HrefResolver` that calls
|
||||
`ReleaseRoutes.DetailHref` — 11.H changes the id type flowing through that resolver. Sweeping the
|
||||
card markup while 11.C is mid-flight would collide. **11.C must land first.**
|
||||
- **11.D** edits `ArchiveView` (filters → URL); 11.H touches the same Archive card hrefs. **Sequence
|
||||
after 11.D** (or fold carefully — but cleaner to follow).
|
||||
- **11.E** just added `SharePopover.ReleaseId` as `long?` — 11.H re-types it to `Guid?`. **Must follow
|
||||
11.E** or the type churns twice.
|
||||
|
||||
So the dependency is: **11.H follows 11.B (done), 11.C, 11.D, and 11.E.** It is the *last* public-site
|
||||
wave of the phase by construction — it re-types the addressing surface that every prior wave builds on.
|
||||
It is **independent of 11.G** except for the migration-ordering coordination in §3e.5(4) (both touch
|
||||
the `release` table / EF snapshot). 11.G is authored-not-applied; author 11.H's migration after it.
|
||||
|
||||
### 3e.8 Observable acceptance criteria
|
||||
|
||||
- A release detail URL is `/cuts/{guid}` (e.g. `/cuts/9f8a3c2e-…`) — **no sequential integer appears
|
||||
in any public release URL**; `/cuts/1` no longer resolves a release.
|
||||
- The player-bar release-title click, Archive cards, and Cuts cards all navigate to the GUID URL
|
||||
(via the re-typed `ReleaseRoutes.DetailHref`).
|
||||
- `GET api/release/{guid}` returns the release; the legacy `GET api/release/{int}` no longer serves the
|
||||
public read path (CMS/ApiKey endpoints' id type per decision 3e.5(3)).
|
||||
- Every existing release row has a non-null, unique `PublicId` after the (Daniel-applied) migration.
|
||||
- Release-level Share (11.E) copies a GUID URL.
|
||||
- Internal joins are unaffected: tracks still resolve to their release, satellites still 1:1-join,
|
||||
the CMS still edits releases — none of which surfaces a GUID to a listener.
|
||||
- New releases created via the upload/`FindOrCreateRelease` path receive a freshly minted `PublicId`.
|
||||
- (If decision 3e.5(1) chose the PK retype instead: the above, plus `ReleaseEntity.Id` is `Guid` and
|
||||
every FK column is GUID — a different acceptance surface; the spec assumes the recommended column.)
|
||||
|
||||
### 3e.9 Wave placement — its own wave (11.H), last on the public-site critical path
|
||||
|
||||
11.H is a **standalone wave**: it is a cross-cutting re-type of the public release-addressing surface
|
||||
plus a Daniel-gated migration, sharing files with (and therefore gated behind) 11.B–11.E. It is not a
|
||||
graft onto another wave — it touches the entity, the DTO, the converter, the routes, the resolver, the
|
||||
share control, the public API params, and the public read path in one coherent sweep. Like 11.G it
|
||||
carries a Daniel-gated migration; unlike 11.G it is **not** free-floating — its file overlap makes it
|
||||
the terminal public-site wave. See §6 for the wave entry and the updated dependency shape.
|
||||
|
||||
---
|
||||
|
||||
## 4. Requirement 3 — full stack retirement + shared release-card normalization
|
||||
|
||||
**DECIDED (2026-06-15):** retire the **whole** track-cardinal stack (not just the `?album` branch),
|
||||
@@ -849,9 +1073,10 @@ seed-from-URL step just has to run before the restore decision (as §5.2 specifi
|
||||
|
||||
Sequenced so the structural chain (Cuts page → resolver → repoint → retire/normalize) is honored,
|
||||
and the genuinely independent tracks (Archive URL; the queue model; the Description schema slice) can
|
||||
run in parallel. Eight commitments, **seven waves**. Two work items can start cold on day one: the
|
||||
run in parallel. Nine commitments, **eight waves**. Two work items can start cold on day one: the
|
||||
queue (11.F, gate for the Cuts "play album" affordance) and the Description schema slice (11.G, gated
|
||||
only by Daniel's migration go-ahead).
|
||||
only by Daniel's migration go-ahead). One wave is **terminal by construction**: 11.H (release GUID
|
||||
identifiers) re-types the public addressing surface that 11.B–11.E all build on, so it follows them.
|
||||
|
||||
```
|
||||
┌──────────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐
|
||||
@@ -929,24 +1154,42 @@ only by Daniel's migration go-ahead).
|
||||
is NOT in this wave** — the Cut text block rides 11.A; the Session/Mix text block is a small additive
|
||||
touch to those existing pages. Both degrade cleanly (a null Description renders nothing), so the
|
||||
render can land before or after 11.G applies (§3d.4–3d.5).
|
||||
- **11.H — release GUID identifiers (terminal public-site wave).** Front the release `long` PK with an
|
||||
app-minted `PublicId` GUID column (the track-`EntryKey` model — §3e.1); make `PublicId` the only
|
||||
release id the public site and public API expose. New `ReleaseEntity.PublicId` (`Guid`, unique index,
|
||||
app-minted at `FindOrCreateRelease`) + EF migration with a GUID backfill for existing rows
|
||||
(**Daniel-gated apply**); `ReleaseDto.PublicId`; `TrackConverter` round-trip; **re-type the public
|
||||
addressing surface to `Guid`**: detail routes (`/cuts|sessions|mixes/{id}` → `:guid`), the
|
||||
`/tracks/{id}` redirect, `ReleaseRoutes.DetailHref`, `SharePopover.ReleaseId`, the public read path
|
||||
(`GetByPublicId`, the `releaseId`-filtered track page), and the public release API params
|
||||
(`GET api/release/{id}` + `releaseId` query). Internal FKs (track→release, satellite→release), the int
|
||||
PK, and the ApiKey-gated CMS endpoints **stay on the int** (§3e.3, decision 3e.5(3)). **Depends on
|
||||
11.B (landed), 11.C, 11.D, 11.E** — it sweeps the files those waves create/edit, so it is the *last*
|
||||
public-site wave (§3e.7). Carries the gating PK-strategy decision (§3e.5(1)) and a migration
|
||||
ordered after 11.G's (§3e.5(4)). **NOT recommended: retyping the PK itself** — that is a framework
|
||||
fork (§3e.2, §3e.6); the column approach achieves the transparency goal at the public surface.
|
||||
|
||||
**Dependency shape:**
|
||||
|
||||
```
|
||||
11.A ──► 11.B ──► 11.C
|
||||
└────► 11.E (also needs a release detail to share)
|
||||
11.D (free-floating; coordinate with 11.C on ArchiveView)
|
||||
11.F (free-floating cold start; 11.A's "play album" consumes it)
|
||||
11.G (free-floating cold start; gated only by Daniel's migration go-ahead.
|
||||
Detail-page render rides 11.A + a Session/Mix touch — degrades on null,
|
||||
so render & schema can land in either order)
|
||||
11.A ──► 11.B ──► 11.C ─┐
|
||||
└────► 11.E ─┤
|
||||
11.D (free-floating; coordinate with 11.C on ArchiveView) ─┤
|
||||
├──► 11.H (re-types the public
|
||||
11.F (free-floating cold start; 11.A's "play album" uses it)│ addressing surface that
|
||||
11.G (free-floating cold start; Daniel-gated migration. │ 11.B–11.E build on; terminal
|
||||
Render rides 11.A + a Session/Mix touch — degrades │ public-site wave. Migration
|
||||
on null, so render & schema land in either order) │ ordered after 11.G's snapshot.)
|
||||
─┘
|
||||
```
|
||||
|
||||
**Critical path:** `11.A → 11.B → 11.C`. **11.D, 11.E, 11.F, 11.G hang off it** (11.E after 11.B; 11.D,
|
||||
11.F, 11.G fully parallel). The "can start immediately" items are **11.A**, **11.F**, and **11.G** —
|
||||
kicking 11.A + 11.F off first shortens the wall-clock to a usable Cut page (page + queue arrive
|
||||
together, so "play album" works on first ship of 11.A rather than as a later retrofit); 11.G runs
|
||||
alongside on its own track, surfacing the Description on the detail pages as it lands.
|
||||
**Critical path:** `11.A → 11.B → 11.C → 11.H`. **11.D, 11.E, 11.F, 11.G hang off the front** (11.E
|
||||
after 11.B; 11.D, 11.F, 11.G fully parallel), but **11.H sits at the tail** — it depends on 11.B
|
||||
(landed), 11.C, 11.D, and 11.E because it re-types the routes/resolver/share/cards those waves touch.
|
||||
The "can start immediately" items are still **11.A**, **11.F**, and **11.G**; **11.H starts last**, once
|
||||
the public addressing surface it rewrites has stopped moving. 11.G and 11.H both carry Daniel-gated
|
||||
migrations against the `release` table — author 11.H's *after* 11.G's so the EF model snapshot stays
|
||||
linear, and apply in author order (§3e.5(4)).
|
||||
|
||||
> **Honest dependency notes (the brief asked to keep these straight):**
|
||||
> - **Ordinal does *not* gate a wave.** §3a showed `TrackNumber` already exists end-to-end; the
|
||||
@@ -1008,6 +1251,28 @@ alongside on its own track, surfacing the Description on the detail pages as it
|
||||
breaks (`white-space: pre-line`) for v1 — no markdown dependency, matches "free-text field."* Trivial
|
||||
to revisit if Daniel wants formatting. Also minor: the `HasMaxLength` ceiling for the column (§3d.3
|
||||
step 1) — recommend generous (2000–4000) for paragraph prose; not a decision, just pick and note.
|
||||
7. **Release id — additive GUID column vs. true PK retype (§3e.5(1)). THE GATING DECISION for 11.H.**
|
||||
*Recommend the additive `PublicId` GUID column* (the track-`EntryKey` model): it matches "similar to
|
||||
how tracks are keyed," avoids forking the `Cerebellum.BlazorBlocks.Models` framework (whose `BaseEntity.Id`
|
||||
the consuming code shows is hardwired `long`), and keeps the migration to a column + backfill rather
|
||||
than a full FK rewrite. The literal "retype the PK to GUID" (§3e.6) is recorded as considered-and-declined
|
||||
— it buys nothing the column doesn't, at framework-fork risk. **Daniel must pick before 11.H is authored.**
|
||||
*(Staff-engineer should confirm the package is non-generic-keyed at implementation — §3e.2 note.)*
|
||||
8. **Existing-data conversion for 11.H (§3e.5(2)).** Under the column approach: backfill a GUID into every
|
||||
existing release row. *Recommend an in-migration `gen_random_uuid()` backfill* (no app pass needed) —
|
||||
the schema's migration history (eight migrations, a back-fill normalization, a unique constraint with
|
||||
conflict recovery) reads like a DB with **real content**, so do *not* assume a dev-reset is safe.
|
||||
**Confirm with Daniel whether the target DB holds data worth preserving** — that fact decides backfill
|
||||
vs. reset.
|
||||
9. **Public URL form for 11.H (§3e.5(3)).** Raw GUID in the URL (`/cuts/{guid}`) vs. a slug/short-id.
|
||||
*Recommend the raw GUID* — it directly satisfies "int IDs too transparent" and matches `/track/{EntryKey}`
|
||||
(already a raw GUID string). A slug is a separate feature (slug column + collision handling); *flag as
|
||||
an adjacent future option* if Daniel later wants pretty shareable URLs.
|
||||
10. **CMS/ApiKey endpoint id type for 11.H (§3e.3, §3e.5(3)).** The ApiKey-gated release endpoints
|
||||
(`DELETE api/track/release/{id}`, the mix-waveform / session-hero POSTs) are behind the auth wall and
|
||||
are *not* the transparency target. *Recommend leaving them on the int PK* to keep 11.H's blast radius at
|
||||
the public surface (the CMS already holds the full `ReleaseDto`). Flag — retyping them too is defensible
|
||||
but widens the wave for no transparency gain.
|
||||
|
||||
### 7.3 Small things to get right (not decisions — implementer notes)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user