feat(414): stamp immutable Channel.Origin (auto-tuned vs user-created) and surface it #575

Merged
timothy merged 1 commits from feat/414-channel-origin into main 2026-07-23 20:12:26 +02:00
Owner

Closes #414.

What

An honest, immutable creation-provenance marker distinguishing auto-tuned from user-created channels.

  • New ChannelOrigin { Unknown = 0, UserCreated = 1, AutoTuned = 2 } + Channel.Origin column (dual-provider EF migration Add_Channel_Origin, default 0 = Unknown).
  • Stamped once at insert: AutoTuned in CreateChannelFromLineupHandler.BuildChannel (the single primitive that bulk auto-tune CreateAutoTunedChannelsHandler delegates to), UserCreated in CreateChannelHandler. Never mutated on edit — so "auto-generated then user-edited" stays AutoTuned.
  • Surfaced as a raw origin on ChannelResponseModel; the SPA badges only AutoTuned (absence ⇒ user-created or legacy).

Design decisions (see docs/decisions.mdchannel.origin-marker)

  • Immutable provenance, not a mutable "still managed" flag — deliberately avoids reviving the fragile "detect when it's been edited away" heuristic #414 rejected. A future "diverged from its auto-tune template" signal belongs to the #383/#384 auto-tune arc, not this column (mirrors the api.channel-health-signal "raw fact, not a policy enum" reasoning).
  • Unknown = 0 is the honest legacy default — pre-migration rows are not back-filled (inferring origin from the "Channel Lineups" playlist group is the rejected heuristic). Unknown/UserCreated both render unbadged.
  • Enum, not bool IsAutoTuned — so a future origin (e.g. Imported) is additive without a wire-contract break.

Empty-schedule / broken-source fault detection remain deferred to #415.

Done-when

  • Channel carries an honest origin marker, stamped at creation by the auto-tune path
  • Dual-provider migration (SQLite + MySql) via scripts/add-migration.sh
  • Surfaced on ChannelResponseModel + rendered in the channels list
  • Defined semantics for "auto-generated then user-edited" (stays AutoTuned; immutable provenance)
  • docs/decisions.md + docs/domain-model.md updated
  • adversarial review passed — clean, no findings

Verification

  • dotnet build ErsatzTV.sln clean; 1833 .NET tests + 939 web tests pass; decisions-validate OK; format/BOM gates clean.
  • Independent cold-context review: clean (migration symmetry, immutability under edit, positional-record integrity, non-nullable wire contract all verified).
  • Live-E2E (write→read round-trip via scripts/e2e-local.sh): POST /api/v1/channelsGET /api/v1/channels returns origin: "UserCreated"; migration applied at boot and the seed channel reads origin: "Unknown" (materialized through the read-path Map projection — no lazy-Map blind spot).
  • design-system/ Channels prototype mirrored to Claude Design in-session.

🤖 Generated with Claude Code

Closes #414. ## What An honest, **immutable creation-provenance** marker distinguishing auto-tuned from user-created channels. - New `ChannelOrigin { Unknown = 0, UserCreated = 1, AutoTuned = 2 }` + `Channel.Origin` column (dual-provider EF migration `Add_Channel_Origin`, default `0 = Unknown`). - Stamped **once at insert**: `AutoTuned` in `CreateChannelFromLineupHandler.BuildChannel` (the single primitive that bulk auto-tune `CreateAutoTunedChannelsHandler` delegates to), `UserCreated` in `CreateChannelHandler`. Never mutated on edit — so "auto-generated then user-edited" stays `AutoTuned`. - Surfaced as a raw `origin` on `ChannelResponseModel`; the SPA badges only `AutoTuned` (absence ⇒ user-created or legacy). ## Design decisions (see `docs/decisions.md` → `channel.origin-marker`) - **Immutable provenance, not a mutable "still managed" flag** — deliberately avoids reviving the fragile "detect when it's been edited away" heuristic #414 rejected. A future "diverged from its auto-tune template" signal belongs to the #383/#384 auto-tune arc, not this column (mirrors the `api.channel-health-signal` "raw fact, not a policy enum" reasoning). - **`Unknown = 0` is the honest legacy default** — pre-migration rows are **not** back-filled (inferring origin from the `"Channel Lineups"` playlist group is the rejected heuristic). `Unknown`/`UserCreated` both render unbadged. - **Enum, not `bool IsAutoTuned`** — so a future origin (e.g. `Imported`) is additive without a wire-contract break. Empty-schedule / broken-source fault detection remain deferred to #415. ## Done-when - [x] `Channel` carries an honest origin marker, stamped at creation by the auto-tune path - [x] Dual-provider migration (SQLite + MySql) via `scripts/add-migration.sh` - [x] Surfaced on `ChannelResponseModel` + rendered in the channels list - [x] Defined semantics for "auto-generated then user-edited" (stays `AutoTuned`; immutable provenance) - [x] `docs/decisions.md` + `docs/domain-model.md` updated - [x] adversarial review passed — clean, no findings ## Verification - `dotnet build ErsatzTV.sln` clean; **1833** .NET tests + **939** web tests pass; `decisions-validate` OK; format/BOM gates clean. - Independent cold-context review: **clean** (migration symmetry, immutability under edit, positional-record integrity, non-nullable wire contract all verified). - **Live-E2E** (write→read round-trip via `scripts/e2e-local.sh`): `POST /api/v1/channels` → `GET /api/v1/channels` returns `origin: "UserCreated"`; migration applied at boot and the seed channel reads `origin: "Unknown"` (materialized through the read-path `Map` projection — no lazy-Map blind spot). - `design-system/` Channels prototype mirrored to Claude Design in-session. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 1 commit 2026-07-23 19:18:21 +02:00
feat(414): stamp immutable Channel.Origin (auto-tuned vs user-created) and surface it
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 15s
PR Gates / Docs update reminder (pull_request) Successful in 15s
PR Gates / decisions lifecycle (pull_request) Successful in 24s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m26s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m39s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m25s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 19m37s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
015d416604
Add a `ChannelOrigin { Unknown, UserCreated, AutoTuned }` enum + `Channel.Origin`
column (dual-provider migration, default 0 = Unknown for un-back-filled legacy
rows). Stamped once at insert — `AutoTuned` in `CreateChannelFromLineupHandler`
(the single primitive bulk auto-tune delegates to), `UserCreated` in
`CreateChannelHandler` — and never mutated on edit, so "auto-generated then
user-edited" stays AutoTuned. Surfaced as a raw `origin` on `ChannelResponseModel`;
the SPA badges only AutoTuned (absence = user-created or legacy).

This is #72 scope item (a), deferred in `api.channel-health-signal` because no
honest signal existed (`ChannelPlayoutSource.Generated` also tags SPA-created
blanks; a "Channel Lineups" playlist-group join is a heuristic that breaks on edit).
The fix is a dedicated column — a fact, not a derivation.

Decision: docs/decisions.md `channel.origin-marker`; docs/domain-model.md updated;
design-system Channels prototype mirrored.

Verified: 1833 .NET + 939 web tests; independent review clean; live-E2E write→read
round-trip (POST channel → GET returns origin=UserCreated; seed row=Unknown).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Owner

Review-verdict: MERGEABLE @ 015d416604

Independent cold-context review (fresh agent, no implementation role) found no issues at confidence ≥80. Verified against spec:

  • Both provider migrations add exactly Origin (int, not-null, default 0) with a clean symmetric DropColumn Down; snapshots + Designer files consistent, no model drift.
  • Stamp completeness: UserCreated/AutoTuned at the only two persisted insert sites; bulk auto-tune funnels through the same CreateChannelFromLineup primitive; DbInitializer seed + 3 in-memory preview channels legitimately unstamped (never persisted).
  • Immutability: UpdateChannelHandler never reads/writes Origin — it survives an edit untouched.
  • Positional-record integrity across DTO / Mapper.ProjectToResponseModel / OpenApiSerializerContractTests.
  • Non-nullable wire contract (origin in required), correct catalog/docs sync.

Plus live-E2E write→read round-trip confirmed origin: "UserCreated" on create and "Unknown" on the seed row through the real read-path projection.

Review-verdict: MERGEABLE @ 015d416604ed0fc5cad0af8f503aedc2f1e0c56a Independent cold-context review (fresh agent, no implementation role) found **no issues** at confidence ≥80. Verified against spec: - Both provider migrations add exactly `Origin` (int, not-null, default 0) with a clean symmetric `DropColumn` Down; snapshots + Designer files consistent, no model drift. - Stamp completeness: `UserCreated`/`AutoTuned` at the only two persisted insert sites; bulk auto-tune funnels through the same `CreateChannelFromLineup` primitive; `DbInitializer` seed + 3 in-memory preview channels legitimately unstamped (never persisted). - Immutability: `UpdateChannelHandler` never reads/writes `Origin` — it survives an edit untouched. - Positional-record integrity across DTO / `Mapper.ProjectToResponseModel` / `OpenApiSerializerContractTests`. - Non-nullable wire contract (`origin` in `required`), correct catalog/docs sync. Plus live-E2E write→read round-trip confirmed `origin: "UserCreated"` on create and `"Unknown"` on the seed row through the real read-path projection.
timothy merged commit 8b9a7ed541 into main 2026-07-23 20:12:26 +02:00
timothy deleted branch feat/414-channel-origin 2026-07-23 20:12:26 +02:00
Sign in to join this conversation.