docs(plan): lock P11 11.H decision — additive EntryKey string, track-pattern, migration-time backfill
This commit is contained in:
@@ -242,7 +242,7 @@ Sequenced as **eight waves**; the critical path is `11.A → 11.B → 11.C → 1
|
||||
- **11.E — release-level Share.** `SharePopover` gains a release mode that copies `ReleaseRoutes.DetailHref(release)`; wire the Cut header Share to it. **Depends on 11.B** (resolver) + a release detail to share.
|
||||
- **11.F — queue model.** `IQueueService` above the single-slot player + one new player `TrackEnded` hook + player-bar skip controls. **Free-floating, can start cold day one.** Gates the Cuts "play album" affordance (11.A header Play). **Preload (§1.3 half b) stays OUT** — design the seam, defer the feature.
|
||||
- **11.G — release Description schema slice.** New `ReleaseEntity.Description` column + EF migration (**Daniel-gated apply**), `ReleaseDto` mirror, `TrackConverter` round-trip, write-path plumbing (`UpdateTrackMetadataRequest` + upload form + the unified services, threaded wherever `Genre` is), CMS `AlbumHeaderFields` multiline input (§3d). **Free-floating, can start cold day one** — the only gate is Daniel's migration go-ahead. The **detail-page render is NOT in this wave**: the Cut text block rides 11.A, the Session/Mix block is a small additive touch to those existing pages. Both degrade cleanly (null Description renders nothing), so render & schema can land in either order.
|
||||
- **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 — tracks already keep their int PK private and expose an app-minted GUID string). New `ReleaseEntity.PublicId` (`Guid`, unique index, minted at `FindOrCreateRelease`) + EF migration backfilling GUIDs for existing rows (**Daniel-gated apply**); `ReleaseDto.PublicId`; `TrackConverter` round-trip; **re-type the public addressing surface to `Guid`** — detail routes (`:long`→`:guid`), the `/tracks/{id}` redirect, `ReleaseRoutes.DetailHref`, `SharePopover.ReleaseId`, the public read path, and the public release API params (`GET api/release/{id}` + the `releaseId` track-page query). Internal FKs (track→release, satellite→release), the int PK, and the ApiKey-gated CMS endpoints **stay on the int**. **Depends on 11.B (landed), 11.C, 11.D, 11.E** — it sweeps the routes/resolver/share/cards those waves create or edit, so it is the **last** public-site wave (spec §3e.7). **Gating decision (Daniel, spec §3e.5(1)):** additive `PublicId` column (**recommended** — matches tracks, avoids forking `Cerebellum.BlazorBlocks.Models` whose `BaseEntity.Id` is hardwired `long`) vs. a true PK retype (recorded, declined — framework fork + full FK rewrite). Also Daniel-gated: existing-data conversion (recommend in-migration `gen_random_uuid()` backfill — the DB appears to hold real data, do **not** assume reset is safe), raw-GUID URL (recommended) vs. slug, and migration ordering after 11.G's snapshot.
|
||||
- **11.H — release GUID identifiers (terminal public-site wave).** Front the release `long` PK with an app-minted GUID-string `EntryKey` column — the **same pattern tracks use** (`TrackEntity.EntryKey` is `required string`, app-minted `Guid.NewGuid().ToString()`, keeping the int PK private). New `ReleaseEntity.EntryKey` (`string`, unique index, minted at `FindOrCreateRelease`) + EF migration that **backfills a GUID-string `EntryKey` for every existing release row at migration time** (**Daniel-gated apply**); `ReleaseDto.EntryKey`; `TrackConverter` round-trip; **re-type the public addressing surface from `long` to the `EntryKey` handle** — detail routes (`:long`→`{EntryKey}`), the `/tracks/{id}` redirect, `ReleaseRoutes.DetailHref`, `SharePopover.ReleaseId`, the public read path, and the public release API params (`GET api/release/{id}` + the `releaseId` track-page query). Internal FKs (track→release, satellite→release), the `long` int PK (unused by the app), and the ApiKey-gated CMS endpoints **stay on the int**. **Depends on 11.B (landed), 11.C, 11.D, 11.E** — it sweeps the routes/resolver/share/cards those waves create or edit, so it is the **last** public-site wave (spec §3e.7). **Gating decision (Daniel, spec §3e.5(1)) — RESOLVED (additive `EntryKey`, track-pattern):** additive app-level GUID-string `EntryKey` column matching tracks; the `long` PK stays DB-only and unused by the app; existing rows are backfilled at migration time (not a dev reset). Daniel's rationale (2026-06-16): "long at the DB level with an app-level guid `EntryKey` for the releases just like tracks; PK is not used by the app; migrate the existing data to provide the entry key at migration time." The true PK retype is **declined** (framework fork of `Cerebellum.BlazorBlocks.Models` — `BaseEntity.Id` hardwired `long` — plus full FK rewrite; recorded as considered-and-declined per file convention). Still open: raw-GUID URL (recommended) vs. slug, and migration ordering after 11.G's snapshot.
|
||||
|
||||
**Landed:** 11.A (2026-06-16); 11.F (2026-06-16); 11.G (2026-06-16); 11.B (2026-06-16); 11.C (2026-06-16); 11.E (2026-06-16). The §3.4 PlayAlbum→`IQueueService` seam (deferred in 11.A, awaiting 11.F) is now closed: `CutDetail.razor` consumes the cascaded `IQueueService` — header Play calls `Queue.PlayRelease(ViewModel.Tracks, 0)`, per-row play calls `Queue.PlayRelease(ViewModel.Tracks, index)`, currently-playing row toggles play/pause, null-safe fallback to `SelectTrackStreaming` when the queue cascade is absent (2026-06-16). Migration `20260616035252_AddReleaseDescription` authored but not yet applied (Daniel-gated). Tracks 11.D, 11.H remain open (11.H still gated behind 11.D).
|
||||
|
||||
|
||||
@@ -45,12 +45,15 @@ The nine:
|
||||
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).
|
||||
end-to-end. **The reframe (§3e), RESOLVED by Daniel 2026-06-16: 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. Releases get the *same member*: a new `ReleaseEntity.EntryKey`
|
||||
(`string`, matching tracks) backfilled at migration time; the `long` PK stays DB-only, unused by
|
||||
the app.** This is the chosen path (not just recommended) 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 decisions
|
||||
(gating one now resolved), 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
|
||||
@@ -692,15 +695,19 @@ and a Session/Mix touch.**
|
||||
> 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
|
||||
**RESOLVED (Daniel, 2026-06-16): do NOT change the release primary-key type. Front the existing
|
||||
`long` PK with an app-minted GUID *string* — a new `EntryKey` column, the *same member name and type
|
||||
tracks use* — and make *that* the only release identifier the public site and API expose. The int
|
||||
`Id` stays the internal PK, unused by the app; the GUID string becomes the addressable handle. This
|
||||
is exactly, mechanically, what tracks already do with `EntryKey`, and the chosen name/type mirror
|
||||
that for genuine consistency.** Daniel's words: *"long at the DB level with an app-level guid
|
||||
`EntryKey` for the releases just like tracks. PK is not used by the app. Be consistent with naming.
|
||||
Migrate the existing data to provide the entry key at migration time."* The brief's literal "replace
|
||||
all the int IDs with GUIDs from the entity up" was 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
|
||||
framework constraint — and the "similar to how tracks are keyed" steer was 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.
|
||||
declined alternative (a true PK swap) follow.
|
||||
|
||||
### 3e.1 How tracks are keyed today — the model to copy (read against live source, 2026-06-16)
|
||||
|
||||
@@ -714,9 +721,10 @@ A track has **two** identifiers, and only one is public:
|
||||
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.)
|
||||
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 `EntryKey` is purely an identifier. Daniel's call (2026-06-16) is to use
|
||||
the **same member name (`EntryKey`) and type (`string`)** anyway, for naming consistency — see §3e.4.)
|
||||
|
||||
### 3e.2 The framework constraint — why a true PK swap is not a local change
|
||||
|
||||
@@ -747,71 +755,81 @@ exactly why tracks solved the transparency problem with `EntryKey` rather than b
|
||||
|
||||
### 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.
|
||||
What the public-facing id touches today, and what each becomes under the **resolved additive
|
||||
`EntryKey` (GUID-string) column** approach. "→ `EntryKey`" means "switch from the int `Id` to the new
|
||||
app-level GUID-string handle"; "unchanged" means it stays on the internal int PK behind the ApiKey
|
||||
wall.
|
||||
|
||||
| Layer | Site | Today | Under recommendation |
|
||||
| Layer | Site | Today | Resolved (additive `EntryKey`, track-pattern) |
|
||||
|---|---|---|---|
|
||||
| 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`) |
|
||||
| Entity | `ReleaseEntity` | `Id : long` (PK) | **add** `EntryKey : string` (`required`, app-minted, unique index), mirroring `TrackEntity.EntryKey`; `Id` **unchanged** (DB-only, unused by app) |
|
||||
| EF config | `ReleaseConfiguration` | `id` PK + columns | **add** `entry_key` column (snake_case, like `TrackConfiguration`) + 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`) |
|
||||
| DTO | `ReleaseDto` | `Id : long` (from `BaseModel`) | **add** `EntryKey : string`; `Id` still present but not used in public links |
|
||||
| Converter | `TrackConverter.Convert(ReleaseEntity/ReleaseDto)` | maps `Id` | **add** `EntryKey` 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 | **`{EntryKey}` string route params** (matching `/track/{EntryKey}`); pages load by `EntryKey` |
|
||||
| Route resolver | `ReleaseRoutes.DetailHref(long id, ReleaseMedium)` + `(ReleaseDto)` overload | takes `long id` | **takes `string entryKey`** (overload reads `release.EntryKey`) — landed in 11.B, this wave re-types it (`ReleaseRoutes.cs:21-29`) |
|
||||
| Share | `SharePopover.ReleaseId` (11.E, `long?`) | `long?` | **`string?` `EntryKey`** — 11.E just added it; this wave re-types it |
|
||||
| Data service | `IReleaseDataService.GetById(long)` + the `releaseId`-filtered track page call | `long` | a **`GetByEntryKey(string)`** read path; the public track page filters by release `EntryKey` (or resolves `EntryKey`→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 | **`{entryKey}` string route / `string?` query** — public reads address by `EntryKey` (`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 |
|
||||
| JSON | `ReleaseDto` serialization | `Id` (number) | `EntryKey` serializes as a string — case-insensitive client deserialization already configured; verify the client reads `EntryKey` |
|
||||
| Rendered/parsed | Cut/Session/Mix detail page id parse; player-bar resolver; Archive/Cuts card hrefs | parse/compare `long` | carry/compare the `EntryKey` string at the public surface (no parse needed — string in, string out, like track routes) |
|
||||
|
||||
**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.
|
||||
**The honest scope line:** the `EntryKey` handle 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 `long` 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
|
||||
### 3e.4 EntryKey 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.)
|
||||
Track `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** — "just like tracks" 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 `EntryKey = Guid.NewGuid().ToString()` at release creation in that path — the
|
||||
identical call tracks make. **No DB default, no `HasDefaultValueSql`** — the column is app-populated
|
||||
and carries a unique index.
|
||||
|
||||
**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.)
|
||||
**Naming/type — RESOLVED (Daniel, 2026-06-16): `EntryKey`, `string`.** The earlier draft proposed a
|
||||
distinct `PublicId : Guid` column on the reasoning that a release has no vault entry, so a native
|
||||
`uuid` column would index better. Daniel overrode that in favor of **genuine consistency with tracks**:
|
||||
use the **same member name (`EntryKey`) and the same type (`string`)**, even though the release GUID
|
||||
has no vault job. Verified against live source — `TrackEntity.EntryKey` is `required string`
|
||||
(`DeepDrftModels/Entities/TrackEntity.cs:14`), column `entry_key` (snake_case, max 100, configured in
|
||||
`TrackConfiguration`), minted as `Guid.NewGuid().ToString()`. `ReleaseEntity.EntryKey` mirrors all of
|
||||
this: `required string`, `entry_key` column, app-minted GUID string, unique index. The minor
|
||||
indexing edge of a native `uuid` column is not worth diverging the two entities' identifier model.
|
||||
|
||||
### 3e.5 The four open decisions (Daniel's calls — recommendations given, none decided unilaterally)
|
||||
**Existing-data backfill — RESOLVED:** the migration mints a GUID-string `EntryKey` for every existing
|
||||
release row at migration time (the same `Guid.NewGuid().ToString()` shape), then marks the column
|
||||
non-null + unique. Not a dev reset. See §3e.5(2).
|
||||
|
||||
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.**
|
||||
### 3e.5 The four decisions (two now RESOLVED by Daniel 2026-06-16; two remain open)
|
||||
|
||||
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.)
|
||||
1. **PK strategy — additive GUID-string column vs. true PK retype. THE GATING DECISION — RESOLVED
|
||||
(additive `EntryKey`, track-pattern).** Daniel chose the **additive app-level `EntryKey` (`string`)
|
||||
column** (§3e.1–3e.2, §3e.4): it matches the track model exactly, avoids a framework fork, keeps the
|
||||
migration trivial (add a column + backfill GUID strings, no FK rewrites), and the `long` int PK
|
||||
stays DB-only/unused by the app. Daniel's words: *"long at the DB level with an app-level guid
|
||||
`EntryKey` for the releases just like tracks. PK is not used by the app. … Migrate the existing
|
||||
data to provide the entry key at migration time."* 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 — **declined** (recorded in §3e.6 per file
|
||||
convention). Nothing about this gating call remains open.
|
||||
|
||||
2. **Existing-data conversion strategy — RESOLVED (in-migration backfill at migration time).** Daniel:
|
||||
*"Migrate the existing data to provide the entry key at migration time."* The migration mints a
|
||||
GUID-string `EntryKey` for every existing release row (mirroring the track mint, `Guid.NewGuid()
|
||||
.ToString()`), then marks the column non-null + unique — **not** a dev-DB reset. (Mechanically this
|
||||
is the in-migration backfill the earlier draft recommended as option (a): 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 — i.e. a
|
||||
database with actual content. A Postgres `gen_random_uuid()` backfill cast to text, or an app-pass,
|
||||
both produce the GUID-string shape; staff-engineer picks the mechanism, but the *decision* — backfill
|
||||
at migration time, no reset — is settled.)
|
||||
|
||||
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
|
||||
@@ -822,8 +840,8 @@ string; a release has no such constraint, so a native `uuid` column is cleaner a
|
||||
**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** —
|
||||
4. **Migration apply is Daniel-gated** (consistent with 11.G). Authoring the migration (add `entry_key`,
|
||||
backfill GUID strings, 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
|
||||
@@ -831,9 +849,11 @@ string; a release has no such constraint, so a native `uuid` column is cleaner a
|
||||
**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)
|
||||
### 3e.6 The alternative — true PK retype (considered and DECLINED, Daniel 2026-06-16)
|
||||
|
||||
For completeness, the literal "change the PK to GUID" path, so the rejection is on record:
|
||||
For completeness, the literal "change the PK to GUID" path, so the rejection is on record (Daniel
|
||||
firmly declined this 2026-06-16 in favor of the additive `EntryKey` column — "PK is not used by the
|
||||
app"):
|
||||
|
||||
- `ReleaseEntity.Id` becomes `Guid` (requires a generic-keyed framework base, or a fork of
|
||||
`Cerebellum.BlazorBlocks.Models`).
|
||||
@@ -843,12 +863,12 @@ For completeness, the literal "change the PK to GUID" path, so the rejection is
|
||||
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).
|
||||
*Why declined:* it is a framework fork (or a confirmation-gated generic-key assumption) plus a
|
||||
full FK-rewrite migration, for an outcome the additive `EntryKey` 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. **Daniel chose §3e.1 (additive
|
||||
`EntryKey` column) on 2026-06-16; §3e.6 is recorded as the considered-and-declined literal reading** —
|
||||
kept visible per file convention in case the PK-retype question ever resurfaces.
|
||||
|
||||
### 3e.7 Sequencing — why 11.H must follow 11.B–11.E
|
||||
|
||||
@@ -856,15 +876,15 @@ This wave **sweeps the very files 11.B–11.E create or edit**, so authoring it
|
||||
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.H re-types both to the `string` `EntryKey` handle (`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.
|
||||
- **11.E** just added `SharePopover.ReleaseId` as `long?` — 11.H re-types it to `string?` (the
|
||||
`EntryKey` handle). **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.
|
||||
@@ -877,15 +897,18 @@ the `release` table / EF snapshot). 11.G is authored-not-applied; author 11.H's
|
||||
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.
|
||||
- `GET api/release/{entryKey}` 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 `EntryKey` (GUID string) after the (Daniel-applied)
|
||||
migration — backfilled at migration time.
|
||||
- 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.)
|
||||
the CMS still edits releases — none of which surfaces an `EntryKey` to a listener.
|
||||
- New releases created via the upload/`FindOrCreateRelease` path receive a freshly minted `EntryKey`
|
||||
(`Guid.NewGuid().ToString()`, the same call tracks make).
|
||||
- `ReleaseEntity.EntryKey` is `required string` and `ReleaseDto.EntryKey` is `string`, mirroring
|
||||
`TrackEntity.EntryKey` exactly (verified `required string` at `TrackEntity.cs:14`); the `long` PK is
|
||||
unchanged and unused by the app.
|
||||
|
||||
### 3e.9 Wave placement — its own wave (11.H), last on the public-site critical path
|
||||
|
||||
@@ -1155,19 +1178,23 @@ identifiers) re-types the public addressing surface that 11.B–11.E all build o
|
||||
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
|
||||
app-minted GUID-string `EntryKey` column — the **same member name and type tracks use** (§3e.1, §3e.4);
|
||||
make `EntryKey` the only release id the public site and public API expose. New `ReleaseEntity.EntryKey`
|
||||
(`required string`, unique index, app-minted `Guid.NewGuid().ToString()` at `FindOrCreateRelease`,
|
||||
mirroring `TrackEntity.EntryKey`) + EF migration that **backfills an `EntryKey` for every existing
|
||||
release row at migration time** (**Daniel-gated apply**); `ReleaseDto.EntryKey`; `TrackConverter`
|
||||
round-trip; **re-type the public addressing surface from `long` to the `EntryKey` handle**: detail
|
||||
routes (`/cuts|sessions|mixes/{id}` → `{EntryKey}` string params, like `/track/{EntryKey}`), 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.
|
||||
(`GetByEntryKey`, the `releaseId`-filtered track page), and the public release API params
|
||||
(`GET api/release/{id}` + `releaseId` query). Internal FKs (track→release, satellite→release), the
|
||||
`long` int PK (unused by the app), 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). **Gating decision RESOLVED (Daniel,
|
||||
2026-06-16): additive `EntryKey` (track-pattern); `long` PK unused by app; backfill at migration time**
|
||||
(§3e.5(1)–(2)). Migration ordered after 11.G's (§3e.5(4)). **Retyping the PK itself is DECLINED** —
|
||||
that is a framework fork (§3e.2, §3e.6); the additive `EntryKey` column achieves the transparency goal
|
||||
at the public surface, exactly as tracks do.
|
||||
|
||||
**Dependency shape:**
|
||||
|
||||
@@ -1251,19 +1278,23 @@ linear, and apply in author order (§3e.5(4)).
|
||||
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.
|
||||
7. **Release id — additive GUID column vs. true PK retype (§3e.5(1)). THE GATING DECISION for 11.H —
|
||||
RESOLVED (Daniel, 2026-06-16): additive `EntryKey` (track-pattern).** Releases get a new
|
||||
`ReleaseEntity.EntryKey` (`required string`, app-minted GUID) — the *same member name and type tracks
|
||||
use* — and the `long` PK stays DB-only, unused by the app. Daniel: *"long at the DB level with an
|
||||
app-level guid `EntryKey` for the releases just like tracks. PK is not used by the app."* It 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 **declined** — recorded as considered-and-declined per file
|
||||
convention (buys nothing the column doesn't, at framework-fork risk).
|
||||
*(Staff-engineer should still confirm the package is non-generic-keyed at implementation — §3e.2 note —
|
||||
though the decision stands regardless.)*
|
||||
8. **Existing-data conversion for 11.H (§3e.5(2)) — RESOLVED (backfill at migration time).** Daniel:
|
||||
*"Migrate the existing data to provide the entry key at migration time."* The migration mints a
|
||||
GUID-string `EntryKey` for every existing release row (the `Guid.NewGuid().ToString()` shape tracks
|
||||
use), then marks the column non-null + unique. **Not a dev-reset.** Staff-engineer picks the mechanism
|
||||
(in-migration SQL backfill cast to text vs. an app pass); the *decision* — backfill at migration time —
|
||||
is settled.
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user