168 records -> docs/decisions/records/<area>/<topic>.md (163 active, 23 dirs) and docs/decisions/archive/<area>/<topic>.md (5 archived). The filename IS the key, so one-active-record-per-key becomes a filesystem property rather than a validator check, and supersession becomes a `git mv`. WHY: the monolith was a concurrency problem before an aesthetic one. A 3,900-line append target made parallel sessions collide -- PR #605 and PR #614 both hit append-vs-append conflicts during routine rebases, and hand-resolving those inside the corpus is exactly the operation the rationale-rewrite guard exists to police. HOW IT IS VERIFIED: a ~170-file diff cannot be meaningfully read, so correctness does not rest on reading it. The parser was taught BOTH formats first, so the body-diff guard parses the old form at the merge-base and the new form at head -- the migration validates itself, no bypass. The proof is a field-level equivalence harness: 168 records before and after, zero lost, zero gained, zero field mismatches, zero rationale bodies differing. Reviewers should scrutinise the harness; it is the actual evidence. What measuring caught that reading would not have: - ~500 lines sit OUTSIDE any record -- decisions.md's lifecycle schema and each topic file's preamble, mostly the only copy. Source files are kept and stripped, never deleted. They also cannot be filed per-area: topic files hold several areas and 4 of 23 areas span several files. - Archive discovery was a non-recursive glob; after the split it found ZERO archived records, surfacing as four bogus "supersedes points to unknown key" errors rather than an obvious failure. - ~32 live docs point into the corpus BY DATE, which the split dangles. Each stripped file now ends with a generated "Records formerly in this file" index, which also rescues the identical breadcrumbs in old issue comments. - decisions.md's "In this file:" list was 97 same-file anchor bullets that the split makes WRONG, not merely stale. Dropped; the generated index replaces them with links that resolve. The equivalence harness now runs against a checked-in FIXTURE, not the live corpus. The earlier version migrated the real tree, which made it a one-shot: the moment the migration landed there was nothing left to move and the tests failed for reasons unrelated to the code. A fixture keeps them testing the SCRIPT rather than the repo's current state. Keys preserved verbatim, warts included: `sched` (12) and `scheduling` (1) remain two directories for one concept. Renaming a key is not a move -- it changes identity, breaks the equivalence proof, and invalidates MemPalace's per-key drawers. Taxonomy normalisation is separate work. refs #610
4.8 KiB
key, title, status, since, supersedes, superseded-by, rule, signals, mechanics
| key | title | status | since | supersedes | superseded-by | rule | signals | mechanics |
|---|---|---|---|---|---|---|---|---|
| testing.scripted-playout-golden-deferred | 2026-07-22 — Sequential (YAML) playout gets a golden; Scripted is excluded from the golden net by construction (#381) | active | 2026-07-22 | none | none | The `PlayoutBuildGoldenTests` in-memory golden net covers Sequential (YAML) as of #381. Scripted's *end-to-end pipeline* is excluded — `ScriptedPlayoutBuilder` runs a user-authored external program that drives the engine over HTTP loopback, which the in-memory harness can't pin — so that full-pipeline (integration) harness is deferred to #563. But the scheduling *behavior* those scripts drive lives entirely in the in-process `SchedulingEngine` (the `ScriptedScheduleController` is a 1:1 pass-through to it), which IS directly unit/golden-testable; the earlier "Scripted is un-golden-able by construction" framing overstated the constraint by conflating transport with engine. #395 extracts that shared switch to `ContentEnumeratorBuilder` and adds a direct regression net (`ContentEnumeratorBuilderTests`) over it. | why is there no scripted golden; scripted transport vs engine; SchedulingEngine is in-process testable; Cli.Wrap external process is transport only; ScriptedScheduleController 1:1 pass-through; engine-level scripted regression net; EnumeratorForContent · paths: `ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs`, `ErsatzTV.Core.Tests/Scheduling/Engine/SchedulingEngineTests.cs`, `ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs`, `ErsatzTV/Controllers/Api/ScriptedScheduleController.cs`, `ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs` · issues: #381, #163, #395, #563 | docs/testing.md → Golden-file nets |
Sequential (YAML) is golden-able and TZ-independent. SequentialPlayoutBuilder reads a YAML schedule
file (Playout.ScheduleFile) rather than a ProgramSchedule/Block calendar, but it is still a pure
in-process build: content resolves from an in-memory-SQLite Collection by name, and the count/all/
duration handlers do UTC-only arithmetic off the caller-supplied start. So the Sequential_yaml case
uses the exact same harness as Classic/Block — a committed input fixture
(Goldens/Fixtures/sequential-schedule.yml) with two count: 2 instructions over one chronological
collection — and needs no Assume/TZ guard (verified: it passes, not skips, under a non-UTC TZ,
unlike Block). The fixture deliberately avoids the local-time-of-day handlers (wait_until, pad_to_next,
pad_until) and shuffle order, which would reintroduce TZ- or seed-dependence. The builder checks the
file via the injected IFileSystem but reads bytes with the static System.IO.File, so the test commits a
real fixture on disk and stubs only IFileSystem.File.Exists; the JSON-schema validator is stubbed (it
loads its schema from a runtime cache folder, irrelevant to characterizing builder output).
Scripted: transport is integration-only, but the engine is in-process testable. ScriptedPlayoutBuilder
builds nothing itself — it Cli.Wrap-executes a user-authored external program, hands it
http://localhost:{Settings.UiPort} + a build id, and after the process exits reads the result straight off
the in-process engine (schedulingEngine.GetState()/GetAnchor()). The program drives the build by calling
back over HTTP, but ScriptedScheduleController is a 1:1 pass-through: every action resolves the engine by
build id and forwards to a single ISchedulingEngine method (AddCollection, AddCount, PadUntil, …) with
no scheduling logic of its own. So "Scripted is un-golden-able" conflates two different things:
- The end-to-end pipeline (real external process + Kestrel on
UiPort+ HTTP loopback) genuinely is integration-test territory — the in-memory golden harness (shared SQLite, no I/O, deterministic clock) can't pin it, and that harness is deferred to #563. This part of the original decision stands. - The scheduling behavior the scripts drive is 100% in
SchedulingEngine, a plain DI-substitutable object already unit-tested (SchedulingEngineTests,new SchedulingEngine(…4 substitutes…)). It is directly drivable and snapshot-testable with no process and no HTTP — the same way the YAML golden drives its builder, just skipping the script/HTTP front-end. #395 extracts the enumerator-construction switch that the Scripted and Sequential/YAML engines share toContentEnumeratorBuilderand adds a direct regression net (ContentEnumeratorBuilderTests) over it — the real safety net its dedup needs (not #563).
The #381 "documented decision" arm correctly deferred the pipeline golden; it overstated the case by writing off engine-level coverage too. Scripted scheduling behavior is now covered in-process; only the external-process pipeline remains #563's.