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
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)
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.
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)
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>
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #414.
What
An honest, immutable creation-provenance marker distinguishing auto-tuned from user-created channels.
ChannelOrigin { Unknown = 0, UserCreated = 1, AutoTuned = 2 }+Channel.Origincolumn (dual-provider EF migrationAdd_Channel_Origin, default0 = Unknown).AutoTunedinCreateChannelFromLineupHandler.BuildChannel(the single primitive that bulk auto-tuneCreateAutoTunedChannelsHandlerdelegates to),UserCreatedinCreateChannelHandler. Never mutated on edit — so "auto-generated then user-edited" staysAutoTuned.originonChannelResponseModel; the SPA badges onlyAutoTuned(absence ⇒ user-created or legacy).Design decisions (see
docs/decisions.md→channel.origin-marker)api.channel-health-signal"raw fact, not a policy enum" reasoning).Unknown = 0is the honest legacy default — pre-migration rows are not back-filled (inferring origin from the"Channel Lineups"playlist group is the rejected heuristic).Unknown/UserCreatedboth render unbadged.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
Channelcarries an honest origin marker, stamped at creation by the auto-tune pathscripts/add-migration.shChannelResponseModel+ rendered in the channels listAutoTuned; immutable provenance)docs/decisions.md+docs/domain-model.mdupdatedVerification
dotnet build ErsatzTV.slnclean; 1833 .NET tests + 939 web tests pass;decisions-validateOK; format/BOM gates clean.scripts/e2e-local.sh):POST /api/v1/channels→GET /api/v1/channelsreturnsorigin: "UserCreated"; migration applied at boot and the seed channel readsorigin: "Unknown"(materialized through the read-pathMapprojection — no lazy-Map blind spot).design-system/Channels prototype mirrored to Claude Design in-session.🤖 Generated with Claude Code
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>Review-verdict: MERGEABLE @
015d416604Independent cold-context review (fresh agent, no implementation role) found no issues at confidence ≥80. Verified against spec:
Origin(int, not-null, default 0) with a clean symmetricDropColumnDown; snapshots + Designer files consistent, no model drift.UserCreated/AutoTunedat the only two persisted insert sites; bulk auto-tune funnels through the sameCreateChannelFromLineupprimitive;DbInitializerseed + 3 in-memory preview channels legitimately unstamped (never persisted).UpdateChannelHandlernever reads/writesOrigin— it survives an edit untouched.Mapper.ProjectToResponseModel/OpenApiSerializerContractTests.origininrequired), 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.