The Scripted engine (SchedulingEngine.EnumeratorForContent) and the Sequential/YAML engine (EnumeratorCache.GetEnumeratorForContent) built their stateless-index IMediaCollectionEnumerator with byte-identical switch bodies. This extracts that switch into one static per-family seam, ContentEnumeratorBuilder.ForContent(items, state, playbackOrder, multiPart), mirroring #380's ShuffleSourceBuilder. Both engines delegate; each keeps its own "not supported" warning on the None branch (per-engine message unchanged).
The blocker premise, corrected
#395's original Done-when box 1 assumed #381 would land a Scripted characterization golden. It didn't — #381 shipped the Sequential (YAML) golden and recorded testing.scripted-playout-golden-deferred, deferring Scripted to an integration harness (#563). On re-examination that record overstated the constraint: ScriptedScheduleController is a 1:1 pass-through, and the scheduling logic — including this exact switch — lives in the in-process SchedulingEngine, which is directly unit-testable. So the real regression net this refactor needs is a direct helper test, not a Scripted golden (which is why #563 is not a prerequisite). This PR:
corrects the testing.scripted-playout-golden-deferred record (transport-vs-engine line) + docs/testing.md to match, and
adds ContentEnumeratorBuilderTests as that net.
Safety net
ContentEnumeratorBuilderTests pins the two documented traps: Shuffle → BlockPlayoutShuffledMediaCollectionEnumerator (exact type, never Classic's ShuffledMediaCollectionEnumerator), and every one of the 8 unsupported PlaybackOrder values → None (the #70 contract).
The existing Sequential (YAML) golden (PlayoutBuildGoldenTests, #381) covers the YAML call-site end-to-end — green before (on origin/main via #564) and after.
dotnet format (touched .cs): clean; BOM check clean.
Cold-context adversarial review: MERGEABLE, no substantive defects (equivalence confirmed against both origin/main originals; warnings preserved and correctly gated; test non-vacuous with complete unsupported-order coverage).
Docs
docs/decisions.md (testing.scripted-playout-golden-deferred reframed; rationale-prose correction → [decisions-edit], same key, not a supersession), docs/decisions/README.md (regenerated), docs/testing.md.
Also filed #565 (blue-sky Auto-Tune→Scripted bridge) from the same discussion.
Fixes #395.
## What
The Scripted engine (`SchedulingEngine.EnumeratorForContent`) and the Sequential/YAML engine (`EnumeratorCache.GetEnumeratorForContent`) built their stateless-index `IMediaCollectionEnumerator` with **byte-identical** switch bodies. This extracts that switch into one static per-family seam, `ContentEnumeratorBuilder.ForContent(items, state, playbackOrder, multiPart)`, mirroring #380's `ShuffleSourceBuilder`. Both engines delegate; each keeps its **own** "not supported" warning on the `None` branch (per-engine message unchanged).
## The blocker premise, corrected
#395's original Done-when box 1 assumed #381 would land a **Scripted** characterization golden. It didn't — #381 shipped the Sequential (YAML) golden and recorded `testing.scripted-playout-golden-deferred`, deferring Scripted to an integration harness (#563). On re-examination that record **overstated** the constraint: `ScriptedScheduleController` is a 1:1 pass-through, and the scheduling logic — including this exact switch — lives in the in-process `SchedulingEngine`, which is directly unit-testable. So the real regression net this refactor needs is a **direct helper test**, not a Scripted golden (which is why #563 is *not* a prerequisite). This PR:
- corrects the `testing.scripted-playout-golden-deferred` record (transport-vs-engine line) + `docs/testing.md` to match, and
- adds `ContentEnumeratorBuilderTests` as that net.
## Safety net
- **`ContentEnumeratorBuilderTests`** pins the two documented traps: Shuffle → `BlockPlayoutShuffledMediaCollectionEnumerator` (exact type, **never** Classic's `ShuffledMediaCollectionEnumerator`), and every one of the 8 unsupported `PlaybackOrder` values → `None` (the #70 contract).
- The existing **Sequential (YAML) golden** (`PlayoutBuildGoldenTests`, #381) covers the YAML call-site end-to-end — green before (on `origin/main` via #564) and after.
- Scripted call-sites are unchanged trivial delegations; `multiPart` wiring preserved exactly (Scripted defaults false; YAML forwards `content.MultiPart`).
## Verification
- Full solution build: 0 errors.
- `ErsatzTV.Core.Tests`: **644 passed**, 1 pre-existing skip (incl. new 11 cases, Sequential golden, `SchedulingEngineTests`).
- `dotnet format` (touched `.cs`): clean; BOM check clean.
- Cold-context adversarial review: **MERGEABLE**, no substantive defects (equivalence confirmed against both `origin/main` originals; warnings preserved and correctly gated; test non-vacuous with complete unsupported-order coverage).
## Docs
- `docs/decisions.md` (`testing.scripted-playout-golden-deferred` reframed; rationale-prose correction → `[decisions-edit]`, same key, not a supersession), `docs/decisions/README.md` (regenerated), `docs/testing.md`.
Also filed #565 (blue-sky Auto-Tune→Scripted bridge) from the same discussion.
Extract the byte-identical PlaybackOrder -> IMediaCollectionEnumerator switch
shared by SchedulingEngine.EnumeratorForContent (Scripted) and
EnumeratorCache.GetEnumeratorForContent (Sequential/YAML) into one static
per-family seam, mirroring #380's ShuffleSourceBuilder. Each engine keeps its own
"not supported" warning on the None branch, so the per-engine message is unchanged.
Adds ContentEnumeratorBuilderTests pinning the block-shuffle-not-classic trap and
the unsupported-order -> None (#70) contract across all 8 unsupported orders.
Corrects the testing.scripted-playout-golden-deferred decision record: Scripted's
external-process + HTTP pipeline is integration-only (deferred to #563), but the
in-process SchedulingEngine it drives IS unit-testable (ScriptedScheduleController
is a 1:1 pass-through) -- the earlier "un-golden-able by construction" framing
conflated transport with engine. docs/testing.md reframed to match.
[decisions-edit]
fixes#395
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cold-context, review-only adversarial pass over the full diff (both call sites vs their origin/main originals). Verified: switch bodies semantically identical (the dropped keepMultiPartEpisodesTogether local was a pure alias); each engine's "not supported" warning preserved verbatim and correctly re-gated on IsNone (fires only for unsupported orders, exactly as before); Shuffle→BlockPlayoutShuffledMediaCollectionEnumerator trap enforced in helper + test; multiPart wiring exact (Scripted defaults false, YAML forwards content.MultiPart); test non-vacuous with complete 8/8 unsupported-order coverage. Build 0 warnings/errors, 11 new tests pass. No substantive defects.
Review-verdict: MERGEABLE @ f54c9ae1
Cold-context, review-only adversarial pass over the full diff (both call sites vs their `origin/main` originals). Verified: switch bodies semantically identical (the dropped `keepMultiPartEpisodesTogether` local was a pure alias); each engine's "not supported" warning preserved verbatim and correctly re-gated on `IsNone` (fires only for unsupported orders, exactly as before); Shuffle→`BlockPlayoutShuffledMediaCollectionEnumerator` trap enforced in helper + test; `multiPart` wiring exact (Scripted defaults false, YAML forwards `content.MultiPart`); test non-vacuous with complete 8/8 unsupported-order coverage. Build 0 warnings/errors, 11 new tests pass. No substantive defects.
The lifecycle validator matches records by their `##` heading for removal
detection (gone = base_headings - head_headings), ungated by [decisions-edit].
Renaming the heading read as an unlogged record removal. Restore the exact
original heading -- which stays literally true ("excluded from the golden net":
#395 adds a unit test, not a golden) -- and keep the rationale correction in the
Rule/Signals/body, which is what [decisions-edit] authorizes.
[decisions-edit]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI on the first head (f54c9ae1) flagged onlydecisions lifecycle red — and this was NOT the known flake: the validator matches decision records by their ## heading for removal-detection (gone = base_headings − head_headings, ungated by [decisions-edit]), so my renaming the record's heading read as an unlogged record removal. Reproduced locally, root-caused, fixed by restoring the exact original heading (which stays literally true — "excluded from the golden net": #395 adds a unit test, not a golden) while keeping the rationale correction in the Rule/Signals/body where [decisions-edit] authorizes it.
Delta vs the reviewed f54c9ae1 is docs-only (one heading line + regenerated catalog row) — no code change — so the prior equivalence review stands. Both decisions lifecycle steps now pass locally: decisions_validate.py --base origin/main --head HEAD → OK; build_decisions_catalog.py --check → up to date. All other jobs were green/running on the prior head.
Review-verdict: MERGEABLE @ 7b77a37f
CI on the first head (f54c9ae1) flagged **only** `decisions lifecycle` red — and this was NOT the known flake: the validator matches decision records by their `##` heading for removal-detection (`gone = base_headings − head_headings`, ungated by `[decisions-edit]`), so my renaming the record's heading read as an unlogged record removal. Reproduced locally, root-caused, fixed by restoring the exact original heading (which stays literally true — "excluded from the golden net": #395 adds a unit test, not a golden) while keeping the rationale correction in the Rule/Signals/body where `[decisions-edit]` authorizes it.
Delta vs the reviewed f54c9ae1 is **docs-only** (one heading line + regenerated catalog row) — no code change — so the prior equivalence review stands. Both `decisions lifecycle` steps now pass locally: `decisions_validate.py --base origin/main --head HEAD` → OK; `build_decisions_catalog.py --check` → up to date. All other jobs were green/running on the prior head.
timothy
merged commit d759a9fc83 into main2026-07-22 20:52:24 +02:00
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 #395.
What
The Scripted engine (
SchedulingEngine.EnumeratorForContent) and the Sequential/YAML engine (EnumeratorCache.GetEnumeratorForContent) built their stateless-indexIMediaCollectionEnumeratorwith byte-identical switch bodies. This extracts that switch into one static per-family seam,ContentEnumeratorBuilder.ForContent(items, state, playbackOrder, multiPart), mirroring #380'sShuffleSourceBuilder. Both engines delegate; each keeps its own "not supported" warning on theNonebranch (per-engine message unchanged).The blocker premise, corrected
#395's original Done-when box 1 assumed #381 would land a Scripted characterization golden. It didn't — #381 shipped the Sequential (YAML) golden and recorded
testing.scripted-playout-golden-deferred, deferring Scripted to an integration harness (#563). On re-examination that record overstated the constraint:ScriptedScheduleControlleris a 1:1 pass-through, and the scheduling logic — including this exact switch — lives in the in-processSchedulingEngine, which is directly unit-testable. So the real regression net this refactor needs is a direct helper test, not a Scripted golden (which is why #563 is not a prerequisite). This PR:testing.scripted-playout-golden-deferredrecord (transport-vs-engine line) +docs/testing.mdto match, andContentEnumeratorBuilderTestsas that net.Safety net
ContentEnumeratorBuilderTestspins the two documented traps: Shuffle →BlockPlayoutShuffledMediaCollectionEnumerator(exact type, never Classic'sShuffledMediaCollectionEnumerator), and every one of the 8 unsupportedPlaybackOrdervalues →None(the #70 contract).PlayoutBuildGoldenTests, #381) covers the YAML call-site end-to-end — green before (onorigin/mainvia #564) and after.multiPartwiring preserved exactly (Scripted defaults false; YAML forwardscontent.MultiPart).Verification
ErsatzTV.Core.Tests: 644 passed, 1 pre-existing skip (incl. new 11 cases, Sequential golden,SchedulingEngineTests).dotnet format(touched.cs): clean; BOM check clean.origin/mainoriginals; warnings preserved and correctly gated; test non-vacuous with complete unsupported-order coverage).Docs
docs/decisions.md(testing.scripted-playout-golden-deferredreframed; rationale-prose correction →[decisions-edit], same key, not a supersession),docs/decisions/README.md(regenerated),docs/testing.md.Also filed #565 (blue-sky Auto-Tune→Scripted bridge) from the same discussion.
Review-verdict: MERGEABLE @
f54c9ae1Cold-context, review-only adversarial pass over the full diff (both call sites vs their
origin/mainoriginals). Verified: switch bodies semantically identical (the droppedkeepMultiPartEpisodesTogetherlocal was a pure alias); each engine's "not supported" warning preserved verbatim and correctly re-gated onIsNone(fires only for unsupported orders, exactly as before); Shuffle→BlockPlayoutShuffledMediaCollectionEnumeratortrap enforced in helper + test;multiPartwiring exact (Scripted defaults false, YAML forwardscontent.MultiPart); test non-vacuous with complete 8/8 unsupported-order coverage. Build 0 warnings/errors, 11 new tests pass. No substantive defects.The lifecycle validator matches records by their `##` heading for removal detection (gone = base_headings - head_headings), ungated by [decisions-edit]. Renaming the heading read as an unlogged record removal. Restore the exact original heading -- which stays literally true ("excluded from the golden net": #395 adds a unit test, not a golden) -- and keep the rationale correction in the Rule/Signals/body, which is what [decisions-edit] authorizes. [decisions-edit] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Review-verdict: MERGEABLE @
7b77a37fCI on the first head (
f54c9ae1) flagged onlydecisions lifecyclered — and this was NOT the known flake: the validator matches decision records by their##heading for removal-detection (gone = base_headings − head_headings, ungated by[decisions-edit]), so my renaming the record's heading read as an unlogged record removal. Reproduced locally, root-caused, fixed by restoring the exact original heading (which stays literally true — "excluded from the golden net": #395 adds a unit test, not a golden) while keeping the rationale correction in the Rule/Signals/body where[decisions-edit]authorizes it.Delta vs the reviewed
f54c9ae1is docs-only (one heading line + regenerated catalog row) — no code change — so the prior equivalence review stands. Bothdecisions lifecyclesteps now pass locally:decisions_validate.py --base origin/main --head HEAD→ OK;build_decisions_catalog.py --check→ up to date. All other jobs were green/running on the prior head.