docs(plan): add Phase 11 commitment 9 (release GUID identifiers, wave 11.H)
This commit is contained in:
@@ -221,7 +221,7 @@ Heat→intensity and collision soft↔hard transfer functions are **staff-engine
|
|||||||
|
|
||||||
## Phase 11 — Public Site Enhancements
|
## Phase 11 — Public Site Enhancements
|
||||||
|
|
||||||
The next pass over the public listening surface, after Phase 9 + Wave 8 moved the site to release-cardinal browse (`/archive`) and per-medium detail. The spine of the phase: **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). **Eight Daniel commitments** (the original four plus four added 2026-06-15 when he resolved the open questions and expanded scope): (1) a Cuts detail page `/cuts/{id}`; (2) the player-bar release-title resolves medium → dedicated detail page; (3) retire the **whole** track-cardinal stack **and** normalize release-card rendering into shared components; (4) encode Archive filters in the URL; (5) explicit track ordinal editable from the CMS; (6) release-level Share; (7) a play-queue system (absorbs the queue half of §1.3); (8) a release **Description** field — multiline free-text on the base release (all media), edited from the CMS add/edit forms and rendered as a text block on every detail page. Full design, framing corrections, wave decomposition, gap analysis: `product-notes/phase-11-public-site-enhancements.md`.
|
The next pass over the public listening surface, after Phase 9 + Wave 8 moved the site to release-cardinal browse (`/archive`) and per-medium detail. The spine of the phase: **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). **Nine Daniel commitments** (the original four, plus four added 2026-06-15 when he resolved the open questions and expanded scope, plus a ninth added 2026-06-16): (1) a Cuts detail page `/cuts/{id}`; (2) the player-bar release-title resolves medium → dedicated detail page; (3) retire the **whole** track-cardinal stack **and** normalize release-card rendering into shared components; (4) encode Archive filters in the URL; (5) explicit track ordinal editable from the CMS; (6) release-level Share; (7) a play-queue system (absorbs the queue half of §1.3); (8) a release **Description** field — multiline free-text on the base release (all media), edited from the CMS add/edit forms and rendered as a text block on every detail page; (9) **release GUID identifiers** — front the release's transparent sequential int PK with an opaque app-minted GUID handle (the track-`EntryKey` model), swept across every public addressing site. Full design, framing corrections, wave decomposition, gap analysis: `product-notes/phase-11-public-site-enhancements.md`.
|
||||||
|
|
||||||
**State it inherits (verified 2026-06-15).** `/sessions/{id}` and `/mixes/{id}` detail pages **exist and are mature** (both inherit `ReleaseDetailBase`'s prerender bridge; `MixDetail` composes `ReleaseDetailScaffold`, `SessionDetail` deliberately diverges). `/archive` is **already** a release-cardinal searchable browser (search + medium + genre). `ReleaseGallery` is the shared release-card grid — but **only Sessions/Mixes use it**; Archive and Cuts re-implement equivalent card markup inline. The real gaps: **Cuts have no single-release detail page** (`/cuts` cards open `/tracks?album={title}`), and **`/archive` holds its filters in component fields, not the URL**. The queue/playlist **does not exist** (single-slot player).
|
**State it inherits (verified 2026-06-15).** `/sessions/{id}` and `/mixes/{id}` detail pages **exist and are mature** (both inherit `ReleaseDetailBase`'s prerender bridge; `MixDetail` composes `ReleaseDetailScaffold`, `SessionDetail` deliberately diverges). `/archive` is **already** a release-cardinal searchable browser (search + medium + genre). `ReleaseGallery` is the shared release-card grid — but **only Sessions/Mixes use it**; Archive and Cuts re-implement equivalent card markup inline. The real gaps: **Cuts have no single-release detail page** (`/cuts` cards open `/tracks?album={title}`), and **`/archive` holds its filters in component fields, not the URL**. The queue/playlist **does not exist** (single-slot player).
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ The next pass over the public listening surface, after Phase 9 + Wave 8 moved th
|
|||||||
|
|
||||||
**Design discipline.** The medium→route resolver is **one table** (`ReleaseRoutes.DetailHref`) consumed by the player bar, Archive, and Cuts cards. The shared `ReleaseGallery` becomes the **one** release-card grid across all four browse surfaces (Archive/Cuts fold in via a new per-card `HrefResolver`), not three inline copies (memory *One source, multiple views*). The `/cuts/{id}` page composes `ReleaseDetailScaffold` via a generalized `Header` slot + a `BodyContent` slot for the track list — **not** a boolean layout flag (Phase 9 §5.3). The queue is a separate `IQueueService` orchestrating above the single-slot player (strong steer; final call staff-engineer's). Header Play binds to a single handler that swaps single-track → `QueueService.PlayRelease` with no page change (memory *Design for adaptability up front*).
|
**Design discipline.** The medium→route resolver is **one table** (`ReleaseRoutes.DetailHref`) consumed by the player bar, Archive, and Cuts cards. The shared `ReleaseGallery` becomes the **one** release-card grid across all four browse surfaces (Archive/Cuts fold in via a new per-card `HrefResolver`), not three inline copies (memory *One source, multiple views*). The `/cuts/{id}` page composes `ReleaseDetailScaffold` via a generalized `Header` slot + a `BodyContent` slot for the track list — **not** a boolean layout flag (Phase 9 §5.3). The queue is a separate `IQueueService` orchestrating above the single-slot player (strong steer; final call staff-engineer's). Header Play binds to a single handler that swaps single-track → `QueueService.PlayRelease` with no page change (memory *Design for adaptability up front*).
|
||||||
|
|
||||||
Sequenced as **seven waves**; the critical path is `11.A → 11.B → 11.C`, with 11.D / 11.E / 11.F / 11.G hanging off it.
|
Sequenced as **eight waves**; the critical path is `11.A → 11.B → 11.C → 11.H`, with 11.D / 11.E / 11.F / 11.G hanging off the front and 11.H sitting at the tail (it re-types the public addressing surface that 11.B–11.E build on).
|
||||||
|
|
||||||
- **11.A — `/cuts/{id}` album-detail page.** Left header (name, artist, genre, year, Play + Share), right cover with theme border, ordered track list (by `TrackNumber`) with per-row play, header Play. New `CutDetailViewModel`; reuses `GetById` + the `releaseId`-filtered track page (both exist). Ordinal is a **verification** (§3a), not a dependency. Header/row Play consume 11.F when present, else degrade to single-track (§3.4 seam). **Load-bearing prerequisite for 11.B's Cut resolution.**
|
- **11.A — `/cuts/{id}` album-detail page.** Left header (name, artist, genre, year, Play + Share), right cover with theme border, ordered track list (by `TrackNumber`) with per-row play, header Play. New `CutDetailViewModel`; reuses `GetById` + the `releaseId`-filtered track page (both exist). Ordinal is a **verification** (§3a), not a dependency. Header/row Play consume 11.F when present, else degrade to single-track (§3.4 seam). **Load-bearing prerequisite for 11.B's Cut resolution.**
|
||||||
- **11.B — `ReleaseRoutes` resolver + repoint.** Promote `ArchiveView.DetailHref` to a shared `ReleaseRoutes.DetailHref`; Cut resolves to `/cuts/{id}` (needs 11.A); repoint player-bar title (→ release), Archive cards, `AlbumsView` cards; thin `/tracks/{id}` redirect page. **Depends on 11.A.**
|
- **11.B — `ReleaseRoutes` resolver + repoint.** Promote `ArchiveView.DetailHref` to a shared `ReleaseRoutes.DetailHref`; Cut resolves to `/cuts/{id}` (needs 11.A); repoint player-bar title (→ release), Archive cards, `AlbumsView` cards; thin `/tracks/{id}` redirect page. **Depends on 11.A.**
|
||||||
@@ -242,10 +242,11 @@ Sequenced as **seven waves**; the critical path is `11.A → 11.B → 11.C`, wit
|
|||||||
- **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.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.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.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.
|
||||||
|
|
||||||
**Landed:** 11.A (2026-06-16); 11.F (2026-06-16); 11.G (2026-06-16); 11.B (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.C, 11.D, 11.E remain open.
|
**Landed:** 11.A (2026-06-16); 11.F (2026-06-16); 11.G (2026-06-16); 11.B (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.C, 11.D, 11.E, 11.H remain open (11.H is gated behind 11.C/11.D/11.E and carries the §3e.5(1) PK-strategy decision).
|
||||||
|
|
||||||
**Dependency shape:** `11.A → 11.B → 11.C`; `11.B → 11.E`; **11.D, 11.F, 11.G parallel** (11.D coordinates with 11.C on `ArchiveView`; 11.F's "play album" is consumed by 11.A; 11.G's Description render rides 11.A + a Session/Mix touch, degrading on null). The cold-start items are **11.A**, **11.F**, and **11.G** — kick 11.A + 11.F off first so "play album" works on first ship of the Cut page; 11.G runs alongside on its own track.
|
**Dependency shape:** `11.A → 11.B → 11.C → 11.H`; `11.B → 11.E`; **11.D, 11.F, 11.G parallel** (11.D coordinates with 11.C on `ArchiveView`; 11.F's "play album" is consumed by 11.A; 11.G's Description render rides 11.A + a Session/Mix touch, degrading on null). **11.H is terminal** — it re-types the public release-addressing surface (routes, `ReleaseRoutes`, `SharePopover`, cards, public API params) that 11.B–11.E create/edit, so it follows all of them; its migration is authored after 11.G's so the EF snapshot stays linear. The cold-start items are **11.A**, **11.F**, and **11.G** — kick 11.A + 11.F off first so "play album" works on first ship of the Cut page; 11.G runs alongside on its own track; 11.H waits for the addressing surface to settle.
|
||||||
|
|
||||||
**Resolved by Daniel (2026-06-15), kept visible per file convention:** player-bar title → release detail (was OQ1); track ordinal in scope **and already built** (was OQ4, reversed then found done); retire the **whole** track-cardinal stack (was OQ5, full cut chosen); release-level Share in scope; play-queue in scope (queue half of §1.3 absorbed; preload half stays deferred); release **Description** field in scope (commitment 8 — a real new column, lands as schema slice 11.G with the render on 11.A + a Session/Mix touch). **Still open (spec §7.2):** `/cuts/{id}` scaffold strategy (generalized `Header` slot — recommended — vs. bespoke); Cut header affordance idiom (icon vs. labeled buttons); queue architecture (separate `IQueueService` — strong steer; staff-engineer's final call); whether release-share keeps "Embed player" (recommend copy-link-only); Description render plain-text vs. markdown (recommend plain text + preserved line breaks for v1) and column max-length (recommend 2000–4000); `/genres` fate (out of scope, flag as adjacent).
|
**Resolved by Daniel (2026-06-15), kept visible per file convention:** player-bar title → release detail (was OQ1); track ordinal in scope **and already built** (was OQ4, reversed then found done); retire the **whole** track-cardinal stack (was OQ5, full cut chosen); release-level Share in scope; play-queue in scope (queue half of §1.3 absorbed; preload half stays deferred); release **Description** field in scope (commitment 8 — a real new column, lands as schema slice 11.G with the render on 11.A + a Session/Mix touch). **Still open (spec §7.2):** `/cuts/{id}` scaffold strategy (generalized `Header` slot — recommended — vs. bespoke); Cut header affordance idiom (icon vs. labeled buttons); queue architecture (separate `IQueueService` — strong steer; staff-engineer's final call); whether release-share keeps "Embed player" (recommend copy-link-only); Description render plain-text vs. markdown (recommend plain text + preserved line breaks for v1) and column max-length (recommend 2000–4000); `/genres` fate (out of scope, flag as adjacent).
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Phase 11 — Public Site Enhancements
|
# Phase 11 — Public Site Enhancements
|
||||||
|
|
||||||
Status: spec / design. Author: product-designer. Date: 2026-06-15 (revised same day after Daniel
|
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
|
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 —
|
**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
|
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
|
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,
|
scope to **eight**; on 2026-06-16 he added a **ninth** — switch releases to GUID identifiers so the
|
||||||
make every navigation an addressable, shareable URL, and make the album a first-class playable
|
id is no longer a transparent sequential integer. They share one spine: **make the release the
|
||||||
object** (ordered, queue-able, shareable) — and now, **a release that can describe itself** in prose.
|
cardinal unit of the public site, make every navigation an addressable, shareable URL, and make the
|
||||||
The eight:
|
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.
|
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.
|
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
|
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
|
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.
|
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
|
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
|
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
|
## 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),
|
**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,
|
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
|
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
|
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
|
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
|
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).
|
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:**
|
**Dependency shape:**
|
||||||
|
|
||||||
```
|
```
|
||||||
11.A ──► 11.B ──► 11.C
|
11.A ──► 11.B ──► 11.C ─┐
|
||||||
└────► 11.E (also needs a release detail to share)
|
└────► 11.E ─┤
|
||||||
11.D (free-floating; coordinate with 11.C on ArchiveView)
|
11.D (free-floating; coordinate with 11.C on ArchiveView) ─┤
|
||||||
11.F (free-floating cold start; 11.A's "play album" consumes it)
|
├──► 11.H (re-types the public
|
||||||
11.G (free-floating cold start; gated only by Daniel's migration go-ahead.
|
11.F (free-floating cold start; 11.A's "play album" uses it)│ addressing surface that
|
||||||
Detail-page render rides 11.A + a Session/Mix touch — degrades on null,
|
11.G (free-floating cold start; Daniel-gated migration. │ 11.B–11.E build on; terminal
|
||||||
so render & schema can land in either order)
|
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,
|
**Critical path:** `11.A → 11.B → 11.C → 11.H`. **11.D, 11.E, 11.F, 11.G hang off the front** (11.E
|
||||||
11.F, 11.G fully parallel). The "can start immediately" items are **11.A**, **11.F**, and **11.G** —
|
after 11.B; 11.D, 11.F, 11.G fully parallel), but **11.H sits at the tail** — it depends on 11.B
|
||||||
kicking 11.A + 11.F off first shortens the wall-clock to a usable Cut page (page + queue arrive
|
(landed), 11.C, 11.D, and 11.E because it re-types the routes/resolver/share/cards those waves touch.
|
||||||
together, so "play album" works on first ship of 11.A rather than as a later retrofit); 11.G runs
|
The "can start immediately" items are still **11.A**, **11.F**, and **11.G**; **11.H starts last**, once
|
||||||
alongside on its own track, surfacing the Description on the detail pages as it lands.
|
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):**
|
> **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
|
> - **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
|
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
|
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.
|
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)
|
### 7.3 Small things to get right (not decisions — implementer notes)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user