feat(69): auto-tuning backend — enumerate library metadata, preview + bulk-create channels (PR1) #379

Merged
timothy merged 11 commits from feat/69-auto-tuning into main 2026-07-16 22:35:11 +02:00
Owner

Backend for #69 auto-tuning — "install it and it builds a lineup for you." This is PR1 (backend only); the SPA wizard + live-E2E land in PR2, which is when #69 closes. Refs #69 (not fixes).

Design/plan: docs/superpowers/specs/2026-07-16-auto-tuning-design.md + docs/superpowers/plans/2026-07-16-auto-tuning-pr1-backend.md.

What it adds (3-axis MVP)

Three new endpoints under the frozen /api/v1, orchestrating existing primitives (#63 composite create, Smart Collections, #64 Channel Templates):

  • POST /api/v1/channels/auto-tune/preview — enumerates library metadata for the selected axes (TV Show, TV Genre, Movie Genre) via EF distinct+count queries and returns proposed channels (name, allocated number, item count, alreadyExists). No writes.
  • POST /api/v1/channels/auto-tune — per selected proposal: creates a SmartCollection (server-generated Lucene query from axis+value) then calls the existing CreateChannelFromLineup (#63) handler via ISender; returns a per-channel Created/Skipped/Failed result.

Key design decisions (see docs/decisions.md)

  • Enumerate via EF, persist via SmartCollection — exact counts for the preview; each channel is a live Lucene query so it keeps tracking the library as it grows (PseudoTV's regenerative intent, but non-destructive).
  • Additive & non-destructive — reserved starting channel number (skips taken), name-based dedup (alreadyExists de-selected by default), existing channels never mutated. Fixes PseudoTV's wipe-and-rebuild.
  • Server owns all query generation — the client passes {axis, value}, never a Lucene string.

Scope

Deferred to follow-ups (not this MVP): TV Network, Movie Studio, Mixed Genre, Music Genre, Smart-Collection→channel, Mixed Content; per-axis templates; even-show-distribution.

Testing / review

  • 16 new NUnit tests (axis map, number allocator, preview EF enumeration, bulk orchestration incl. rollback + name-length filtering) — all green; solution builds clean.
  • Built subagent-driven with per-task spec+quality review, then a cold-context whole-branch review (MERGEABLE-WITH-NITS). Two Important findings fixed + re-reviewed: (a) a null-channels 500 guard, (b) rollback of the just-created SmartCollection on a failed/skipped channel so re-runs stay clean. Plus a best-effort try/catch on that rollback.
  • OpenAPI (v1.json), endpoint-index.md, and the typed web client regenerated; check:api clean. docs/{api-conventions,domain-model,decisions}.md updated.

Known limitation (MVP)

A generated channel whose name collides with a pre-existing user SmartCollection of the same name reports Failed for that channel (documented in decisions.md). Residual: genre SelectMany/GroupBy enumeration is proven on SQLite (default); worth one smoke check before a MySQL deploy.

🤖 Generated with Claude Code

Backend for #69 **auto-tuning** — "install it and it builds a lineup for you." This is **PR1 (backend only)**; the SPA wizard + live-E2E land in **PR2**, which is when #69 closes. `Refs #69` (not `fixes`). Design/plan: `docs/superpowers/specs/2026-07-16-auto-tuning-design.md` + `docs/superpowers/plans/2026-07-16-auto-tuning-pr1-backend.md`. ## What it adds (3-axis MVP) Three new endpoints under the frozen `/api/v1`, orchestrating existing primitives (#63 composite create, Smart Collections, #64 Channel Templates): - `POST /api/v1/channels/auto-tune/preview` — enumerates library metadata for the selected axes (**TV Show**, **TV Genre**, **Movie Genre**) via EF distinct+count queries and returns proposed channels (name, allocated number, item count, `alreadyExists`). **No writes.** - `POST /api/v1/channels/auto-tune` — per selected proposal: creates a **SmartCollection** (server-generated Lucene query from `axis`+`value`) then calls the existing `CreateChannelFromLineup` (#63) handler via `ISender`; returns a per-channel **Created/Skipped/Failed** result. ## Key design decisions (see `docs/decisions.md`) - **Enumerate via EF, persist via SmartCollection** — exact counts for the preview; each channel is a live Lucene query so it keeps tracking the library as it grows (PseudoTV's regenerative intent, but non-destructive). - **Additive & non-destructive** — reserved starting channel number (skips taken), name-based dedup (`alreadyExists` de-selected by default), existing channels never mutated. Fixes PseudoTV's wipe-and-rebuild. - **Server owns all query generation** — the client passes `{axis, value}`, never a Lucene string. ## Scope Deferred to follow-ups (not this MVP): TV Network, Movie Studio, Mixed Genre, Music Genre, Smart-Collection→channel, Mixed Content; per-axis templates; even-show-distribution. ## Testing / review - **16 new NUnit tests** (axis map, number allocator, preview EF enumeration, bulk orchestration incl. rollback + name-length filtering) — all green; solution builds clean. - Built subagent-driven with per-task spec+quality review, then a **cold-context whole-branch review** (MERGEABLE-WITH-NITS). Two Important findings fixed + re-reviewed: (a) a null-`channels` 500 guard, (b) rollback of the just-created SmartCollection on a failed/skipped channel so re-runs stay clean. Plus a best-effort try/catch on that rollback. - OpenAPI (`v1.json`), `endpoint-index.md`, and the typed web client regenerated; `check:api` clean. `docs/{api-conventions,domain-model,decisions}.md` updated. ## Known limitation (MVP) A generated channel whose name collides with a **pre-existing user** SmartCollection of the same name reports `Failed` for that channel (documented in decisions.md). Residual: genre `SelectMany/GroupBy` enumeration is proven on SQLite (default); worth one smoke check before a MySQL deploy. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 11 commits 2026-07-16 22:21:48 +02:00
Design for #69 auto-tuning: enumerate library metadata (TV Show / TV
Genre / Movie Genre) -> preview -> select -> bulk-create channels.
Additive/non-destructive, enumerate-via-EF + persist-via-SmartCollection,
server-side query generation, reserve-block numbering.

Refs #69

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task-by-task TDD plan for the auto-tuning backend: axis map, number
allocator, preview query + EF enumeration, bulk-create orchestration,
REST endpoints, OpenAPI + docs.

Refs #69

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refs #69
Refs #69
Refs #69
Refs #69
Refs #69
Refs #69
- CreateAutoTunedChannelsRequest.ToCommand(): guard null Channels (was NREing on
  a request body that omits "channels", causing HTTP 500).
- CreateAutoTunedChannelsHandler.CreateOne: when CreateChannelFromLineup returns
  Left (Skipped/Failed), roll back the just-created SmartCollection via
  DeleteSmartCollection so retries don't fail on SmartCollection-name uniqueness.
  Best-effort; the delete result does not change the outcome.
- PreviewAutoTuneChannelsHandler: filter out proposals whose generated name
  exceeds the 50-char Channel.Name limit before number allocation, so numbers
  aren't wasted on proposals that can never be created.
- docs/superpowers/specs/2026-07-16-auto-tuning-design.md: fix field-name drift
  in JSON examples (proposedNumber -> number, error -> reason) to match the
  actual AutoTuneProposal/AutoTuneChannelOutcome DTOs.

Refs #69
DeleteSmartCollection rollback in CreateAutoTunedChannelsHandler.CreateOne
was called without exception handling; a transient infra exception during
the best-effort rollback would propagate and abort the whole batch,
contradicting the comment's stated intent. Wrap the send in try/catch so
an orphaned SmartCollection is the accepted degraded outcome instead.
chore(69): regenerate endpoint-index after rebase on origin/main
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m34s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 5m21s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 8m7s
938e4e0f50
Union of #340 IPTV-settings endpoints + auto-tune endpoints.

Refs #69
Author
Owner

Review-verdict: MERGEABLE @ 938e4e0f50

Review trail: subagent-driven build with per-task spec+quality reviews (all clean), then a cold-context whole-branch review of c8c8ee96 → MERGEABLE-WITH-NITS. Two Important findings fixed in fa445bbe (null-channels 500 guard; SmartCollection rollback on failed/skipped channel) + a best-effort try/catch in 5098c048; the fix commit was independently re-reviewed → MERGEABLE. The current head 938e4e0f adds only a rebase onto origin/main (hand-resolved append-only decisions.md, regenerated OpenAPI/endpoint-index.md) on top of the reviewed 5098c048 — no logic change; 16/16 tests green post-rebase, check:api clean.

Note: this PR is Refs #69 (backend PR1); #69 does not close until PR2 (SPA wizard + live-E2E). Merge is not gate-auto-granted here — it's a normal reviewed feature-branch merge once CI is green.

Review-verdict: MERGEABLE @ 938e4e0f505610a6c4def8e699dbd039cb6ed1a7 Review trail: subagent-driven build with per-task spec+quality reviews (all clean), then a cold-context whole-branch review of `c8c8ee96` → MERGEABLE-WITH-NITS. Two Important findings fixed in `fa445bbe` (null-`channels` 500 guard; SmartCollection rollback on failed/skipped channel) + a best-effort try/catch in `5098c048`; the fix commit was independently re-reviewed → MERGEABLE. The current head `938e4e0f` adds only a rebase onto origin/main (hand-resolved append-only `decisions.md`, regenerated OpenAPI/`endpoint-index.md`) on top of the reviewed `5098c048` — no logic change; 16/16 tests green post-rebase, `check:api` clean. Note: this PR is `Refs #69` (backend PR1); #69 does not close until PR2 (SPA wizard + live-E2E). Merge is not gate-auto-granted here — it's a normal reviewed feature-branch merge once CI is green.
timothy merged commit 331a427040 into main 2026-07-16 22:35:11 +02:00
timothy deleted branch feat/69-auto-tuning 2026-07-16 22:35:11 +02:00
Sign in to join this conversation.