feat(425): per-source rotation weights + query corrections for auto-tune channels #439

Merged
timothy merged 2 commits from feat/425-autotune-weighted-sources into main 2026-07-18 04:29:17 +02:00
Owner

Fixes #425.

Per-source rotation weights (weighted round-robin, e.g. 3x Show A / 1x Show B) + query corrections (exclude / add-untagged) for auto-tune channels, supplied at bulk-create time via an optional sources: [{sourceId, weight, excluded}] on POST /api/v1/channels/auto-tune.

Design (Option A, reuse #70): a customized channel is backed by a system-owned MultiCollection of per-source SmartCollections with PlaybackOrder.WeightedShuffle — the path WeightedShuffleCollectionEnumerator already consumes. All-default weights keep the #69 single-SmartCollection shape. Full rationale in docs/decisions.md 2026-07-18 (#425).

  • Discriminators: TV -> live show_title:"X"; movies -> stable id:{mediaItemId}.
  • Materialization axis-dependent: TV materializes every base show individually + a live remainder (weight 1); MovieGenre materializes only touched movies + one count-weighted remainder.
  • Remainder = (base) AND NOT (materialized ∪ excluded) — a partition.
  • Ownership: new nullable OwnedByChannelId on SmartCollection + MultiCollection (dual-provider migration); owned rows hidden from all collection lists + the scheduling pickers, cascade-cleaned on channel delete.

Verification:

  • 2268+ backend tests green; new AutoTuneAxisMap partition units, DB-backed weighted-path handler tests, delete-cleanup, and picker-hiding regression tests.
  • Live-E2E: created a weighted channel (Alpha=3, Beta=1) via the real API -> built playout (183k items, no errors) aired Alpha:Beta = 3.00 exactly, confirmed in both the playout and the guide programmes (75%/25%); M3U + XMLTV valid.
  • Independent adversarial review (cold Opus agent) found + fixed a blocker (search pickers leaked owned artifacts -> user-schedule-delete via FK cascade) plus two mediums (ownership-stamp cancellation/atomicity; documented the >1000-source weight-clamp bound).

Docs updated: decisions.md, domain-model.md, api-conventions.md; OpenAPI trio regenerated.

🤖 Generated with Claude Code

Fixes #425. Per-source rotation weights (weighted round-robin, e.g. 3x Show A / 1x Show B) + query corrections (exclude / add-untagged) for auto-tune channels, supplied at bulk-create time via an optional `sources: [{sourceId, weight, excluded}]` on `POST /api/v1/channels/auto-tune`. **Design (Option A, reuse #70):** a customized channel is backed by a system-owned `MultiCollection` of per-source `SmartCollection`s with `PlaybackOrder.WeightedShuffle` — the path `WeightedShuffleCollectionEnumerator` already consumes. All-default weights keep the #69 single-SmartCollection shape. Full rationale in `docs/decisions.md` 2026-07-18 (#425). - **Discriminators**: TV -> live `show_title:"X"`; movies -> stable `id:{mediaItemId}`. - **Materialization axis-dependent**: TV materializes every base show individually + a live remainder (weight 1); MovieGenre materializes only touched movies + one count-weighted remainder. - **Remainder** = `(base) AND NOT (materialized ∪ excluded)` — a partition. - **Ownership**: new nullable `OwnedByChannelId` on SmartCollection + MultiCollection (dual-provider migration); owned rows hidden from all collection lists + the scheduling pickers, cascade-cleaned on channel delete. **Verification:** - 2268+ backend tests green; new AutoTuneAxisMap partition units, DB-backed weighted-path handler tests, delete-cleanup, and picker-hiding regression tests. - **Live-E2E**: created a weighted channel (Alpha=3, Beta=1) via the real API -> built playout (183k items, no errors) aired Alpha:Beta = **3.00** exactly, confirmed in both the playout and the guide programmes (75%/25%); M3U + XMLTV valid. - **Independent adversarial review** (cold Opus agent) found + fixed a blocker (search pickers leaked owned artifacts -> user-schedule-delete via FK cascade) plus two mediums (ownership-stamp cancellation/atomicity; documented the >1000-source weight-clamp bound). Docs updated: decisions.md, domain-model.md, api-conventions.md; OpenAPI trio regenerated. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Owner

Review-verdict: MERGEABLE @ 27221572158114f5a43a40c94c743322c3e49d76

Two independent cold-agent (Opus) review passes:

  1. Initial PR diff — found a BLOCKER (SearchSmart/MultiCollections pickers leaked owned artifacts → user-schedule-delete via FK cascade) + 2 mediums (ownership-stamp cancellation/atomicity; undocumented >1000 weight-clamp bound). All fixed in 27221572.
  2. Re-review of the fix commit (27221572) — confirmed all four fixes correct, the picker regression test non-vacuous, and no other live leaking surface (GetById handlers are direct lookups, not pickers; QuerySearchTargetsHandler is orphaned/never-dispatched Blazor-era dead code — noted for whoever revives it, not a live leak). Verdict: MERGEABLE.

Live-E2E: weighted channel (Alpha=3, Beta=1) → built playout aired Alpha:Beta = 3.00 exactly, confirmed in playout + guide. All 6 ## Done-when boxes ticked. Awaiting CI green before merge.

Review-verdict: MERGEABLE @ 27221572158114f5a43a40c94c743322c3e49d76 Two independent cold-agent (Opus) review passes: 1. **Initial PR diff** — found a BLOCKER (SearchSmart/MultiCollections pickers leaked owned artifacts → user-schedule-delete via FK cascade) + 2 mediums (ownership-stamp cancellation/atomicity; undocumented >1000 weight-clamp bound). All fixed in 27221572. 2. **Re-review of the fix commit** (27221572) — confirmed all four fixes correct, the picker regression test non-vacuous, and no other *live* leaking surface (`GetById` handlers are direct lookups, not pickers; `QuerySearchTargetsHandler` is orphaned/never-dispatched Blazor-era dead code — noted for whoever revives it, not a live leak). Verdict: MERGEABLE. Live-E2E: weighted channel (Alpha=3, Beta=1) → built playout aired Alpha:Beta = 3.00 exactly, confirmed in playout + guide. All 6 `## Done-when` boxes ticked. Awaiting CI green before merge.
timothy force-pushed feat/425-autotune-weighted-sources from 2722157215 to 3bca000e27 2026-07-18 04:08:34 +02:00 Compare
Author
Owner

Review-verdict: MERGEABLE @ 3bca000e27

Rebased onto origin/main (was 26 commits behind). The rebase changed no reviewed code — only: resolved the append-only docs/decisions.md conflict (kept both the #386/#176 entries and this PR's #425 entry), and regenerated the OpenAPI trio (check:api clean, no drift). My migration is still the latest (no competing migration landed; snapshot has no drift). Full solution rebuilds clean and the auto-tune/picker/delete tests pass on the new base. Prior two-pass review (blocker + fix-commit re-review) verdicts stand at this head.

Review-verdict: MERGEABLE @ 3bca000e27dc5bfd6241f681658db8e3067986c2 Rebased onto origin/main (was 26 commits behind). The rebase changed **no reviewed code** — only: resolved the append-only `docs/decisions.md` conflict (kept both the #386/#176 entries and this PR's #425 entry), and regenerated the OpenAPI trio (`check:api` clean, no drift). My migration is still the latest (no competing migration landed; snapshot has no drift). Full solution rebuilds clean and the auto-tune/picker/delete tests pass on the new base. Prior two-pass review (blocker + fix-commit re-review) verdicts stand at this head.
timothy merged commit 3a463db36a into main 2026-07-18 04:29:17 +02:00
timothy deleted branch feat/425-autotune-weighted-sources 2026-07-18 04:29:17 +02:00
Sign in to join this conversation.