Adds a user-triggered per-playout Reshuffle action and surfaces the play-order seed.
Premise correction: the persistence half of #71 already exists upstream (Playout.Seed + owned CollectionEnumeratorState persist the shuffled order + position across rebuilds/restarts). The genuine gap was a per-playout, user-controlled reshuffle (a Classic channel's seed was otherwise frozen — reset-all only Refreshes Classic) plus visibility.
ReshufflePlayout command + handler — reseeds + clears via ErasePlayoutHistory (the one primitive that reseeds all four kinds) then enqueues BuildPlayout(id, Reset) to rebuild.
Seed surfacing — int Seed on the playout list + detail DTOs; SPA shows it (visible confirmation the reshuffle worked) + a Reshuffle button.
The initial implementation enqueued Reset alone, assuming it reseeds — but Reset only reseeds Classic (PlayoutBuilder.cs:301); Block/Sequential/Scripted rebuild deterministically. Caught by the independent adversarial review (Critical). Fixed by routing through ErasePlayoutHistory (reseeds all 4 kinds) — confirmed end-to-end.
Task 3 of ersatztv#71 (reshuffle-playout). Appends Seed as the last
positional field on PlayoutNameViewModel, threading it through the
Mapper list projection, GetPlayoutByIdHandler detail projection, and
the four Update*PlayoutHandler VM constructions, then exposing it on
PlayoutListItemResponseModel/PlayoutResponseModel via the controller's
ToResponse/ToListItemResponse mappers.
ChannelControllerTests.MakePlayout was an additional construction site
not listed in the task brief (positional-record break); updated to
append 0 for Seed alongside the brief's ErsatzTV.Tests changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reset only reseeds Playout.Seed for Classic playouts (PlayoutBuilder); Block/
Sequential/Scripted rebuild deterministically from the existing seed, so
reshuffle was a silent no-op for 3 of the 4 supported kinds (C1). Fix:
ReshufflePlayoutHandler now sends ErasePlayoutHistory (reseed + clear anchors/
history, the only primitive that reseeds all four kinds) before enqueueing
BuildPlayout(Reset).
Also: correct docs/decisions.md's false "Reset already reseeds..." claim,
fix the SPA reshuffle test mock to return 202 (matches the real endpoint),
and gate the seed-help text to the resettable kinds (was showing even for
ExternalJson/None where no Reshuffle button exists).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cross-model (Codex) was quota-exhausted this window, so review ran cold-context on the frontier model (fresh context per pass), scoped review-only.
Whole-branch review @ 6a4d81f8 — lock cross-release/double-release safety enumerated the full BuildPlayout producer/consumer graph + all lock acquire/release sites: PASS (the new handler touches no locks). But found Critical C1: BuildPlayout(Reset) only reseeds Playout.Seed for Classic; Block/Sequential/Scripted rebuilt deterministically → reshuffle was a no-op for 3 of 4 kinds. → BLOCKED.
Fix @ 81684411 — reshuffle now routes through ErasePlayoutHistory (reseeds + clears derived enumerator anchors for all 4 kinds) then rebuilds. Fix-commit re-review traced each builder's seed derivation and confirmed the reseed genuinely produces a new order for every kind; ordering (erase commits before build enqueued), IMediator injection, and the non-vacuous regression test all verified.
Live-E2E (write-path requirement): Block playout (the previously-broken kind) seed 453788318 → 1371155125 → 1124007100 across two reshuffles, round-tripped through GET; POST /reshuffle 202; nonexistent → 404; ExternalJson → 422.
## Independent adversarial review
Cross-model (Codex) was quota-exhausted this window, so review ran cold-context on the frontier model (fresh context per pass), scoped review-only.
**Whole-branch review @ 6a4d81f8** — lock cross-release/double-release safety enumerated the full `BuildPlayout` producer/consumer graph + all lock acquire/release sites: **PASS** (the new handler touches no locks). But found **Critical C1**: `BuildPlayout(Reset)` only reseeds `Playout.Seed` for Classic; Block/Sequential/Scripted rebuilt deterministically → reshuffle was a no-op for 3 of 4 kinds. → BLOCKED.
**Fix @ 81684411** — reshuffle now routes through `ErasePlayoutHistory` (reseeds + clears derived enumerator anchors for all 4 kinds) then rebuilds. **Fix-commit re-review** traced each builder's seed derivation and confirmed the reseed genuinely produces a new order for every kind; ordering (erase commits before build enqueued), `IMediator` injection, and the non-vacuous regression test all verified.
**Live-E2E** (write-path requirement): Block playout (the previously-broken kind) seed `453788318 → 1371155125 → 1124007100` across two reshuffles, round-tripped through GET; `POST /reshuffle` 202; nonexistent → 404; ExternalJson → 422.
Review-verdict: MERGEABLE @ 81684411
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.
fixes #71
Summary
Adds a user-triggered per-playout Reshuffle action and surfaces the play-order seed.
Premise correction: the persistence half of #71 already exists upstream (
Playout.Seed+ ownedCollectionEnumeratorStatepersist the shuffled order + position across rebuilds/restarts). The genuine gap was a per-playout, user-controlled reshuffle (a Classic channel's seed was otherwise frozen —reset-allonlyRefreshes Classic) plus visibility.Changes
POST /api/v1/playouts/{id}/reshuffle(PlayoutController.Reshuffle) — guards 409 (build lock) → 404 → 422 (kind ∉ Classic/Block/Sequential/Scripted) → 202.ReshufflePlayoutcommand + handler — reseeds + clears viaErasePlayoutHistory(the one primitive that reseeds all four kinds) then enqueuesBuildPlayout(id, Reset)to rebuild.int Seedon the playout list + detail DTOs; SPA shows it (visible confirmation the reshuffle worked) + a Reshuffle button.Important note (found in review)
The initial implementation enqueued
Resetalone, assuming it reseeds — butResetonly reseeds Classic (PlayoutBuilder.cs:301); Block/Sequential/Scripted rebuild deterministically. Caught by the independent adversarial review (Critical). Fixed by routing throughErasePlayoutHistory(reseeds all 4 kinds) — confirmed end-to-end.Verification
dotnet test1638/1638, SPA 784/784, lint +npm run build+check:apiclean.453788318 → 1371155125 → 1124007100across two reshuffles (round-tripped through GET); reshuffle 202; 404 nonexistent; 422 ExternalJson.Design/plan:
docs/superpowers/{specs,plans}/2026-07-16-reshuffle-playout*.md.🤖 Generated with Claude Code
Independent adversarial review
Cross-model (Codex) was quota-exhausted this window, so review ran cold-context on the frontier model (fresh context per pass), scoped review-only.
Whole-branch review @
6a4d81f8— lock cross-release/double-release safety enumerated the fullBuildPlayoutproducer/consumer graph + all lock acquire/release sites: PASS (the new handler touches no locks). But found Critical C1:BuildPlayout(Reset)only reseedsPlayout.Seedfor Classic; Block/Sequential/Scripted rebuilt deterministically → reshuffle was a no-op for 3 of 4 kinds. → BLOCKED.Fix @
81684411— reshuffle now routes throughErasePlayoutHistory(reseeds + clears derived enumerator anchors for all 4 kinds) then rebuilds. Fix-commit re-review traced each builder's seed derivation and confirmed the reseed genuinely produces a new order for every kind; ordering (erase commits before build enqueued),IMediatorinjection, and the non-vacuous regression test all verified.Live-E2E (write-path requirement): Block playout (the previously-broken kind) seed
453788318 → 1371155125 → 1124007100across two reshuffles, round-tripped through GET;POST /reshuffle202; nonexistent → 404; ExternalJson → 422.Review-verdict: MERGEABLE @
81684411