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.2 KiB
key, title, status, since, supersedes, superseded-by, rule, signals
| key | title | status | since | supersedes | superseded-by | rule | signals |
|---|---|---|---|---|---|---|---|
| graphics.channel-level-attachment | 2026-07-22 — Channel-level graphics-element attachment + seeded On Now/Next text element (#74) | active | 2026-07-22 | none | none | A channel can attach `GraphicsElement`s directly via a new `ChannelGraphicsElement` join table (a base layer under deco/playout-item elements), and a built-in text element (`on-now-next.yml`) is seeded once per database so the On Now/Next overlay works out of the box. | ChannelGraphicsElement, Channel graphics attachment, GraphicsElementSelector base layer, on-now-next seeded element, GraphicsElementDefaults.OnNowNextFileName, builtIn discriminator · paths: `ErsatzTV.Core/Domain/ChannelGraphicsElement.cs`, `ErsatzTV.Core/FFmpeg/GraphicsElementSelector.cs`, `ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsElementSeeder.cs`, `ConfigElementKey.GraphicsOnNowNextSeeded`, `GraphicsElementResponseModel.BuiltIn` · issues: #74 |
#74 asked for a transient "On Now / Next" text bug burned onto the transcoded stream at each
program transition. The rendering and EPG-template-data infrastructure already existed (upstream
graphics engine + our #502/#511 remote-image/graphics-engine work); the gap was that graphics
elements had no channel-level attachment — only PlayoutItem/ProgramScheduleItem/BlockItem/
Deco joins existed — and there was no seeded/built-in graphics element, unlike watermarks.
Decision: add a 5th join table, ChannelGraphicsElement, rather than reuse the watermark FK.
Watermarks and graphics elements are separate parallel systems; a channel already has exactly one
WatermarkId, already spent on the #67 logo bug, and multi-line EPG text is a poor fit for the
single-image watermark model. ChannelGraphicsElement is structurally identical to the existing
four joins (composite key {ChannelId, GraphicsElementId}), added via a dual-provider migration
(scripts/add-migration.sh Add_ChannelGraphicsElement).
GraphicsElementSelector.SelectGraphicsElementsappends channel-level elements at the final fall-through, alongsideplayoutItem.PlayoutItemGraphicsElements— a base layer. A deco inMergemode composes with it; a deco inOverride/Disablemode returns earlier and so suppresses it (decos are allowed to override channel defaults, a deliberate rule). One more suppression path: on a filler item, a deco whose graphics-elements section is not set to run during filler (UseGraphicsElementsDuringFillerfalse) clears the result and returns forMergeandOverridealike, so the channel base layer is dropped there too.HttpLiveStreamingDirectcontinues to return empty (ErsatzTV isn't transcoding, so there is no frame pipeline to draw into).- Seeded built-in element, mirroring the
iptv.logo-drives-bug-preset(#67) pattern:GraphicsElementSeeder.SeedOnNowNextwriteson-now-next.ymlinto the graphics-elements templates folder (only if the file is absent — operator edits are never clobbered) and ensures aGraphicsElementrow exists for it, guarded by thegraphics.on_now_next_seededConfigElementmarker so it runs once per database, not once per file-absence (the same reasoning as #67: the seeder runs at every startup, so a name/file-only guard would resurrect a deliberately deleted preset). - The API needed a way for the SPA to find the built-in element without a fragile name-match — the
direct #67 lesson (
WatermarkResponseModel.imageSource).GraphicsElementResponseModelgained a server-derivedBuiltInbool, computed by comparing the row'sPathfilename toGraphicsElementDefaults.OnNowNextFileNamerather than trusting the element's editableName. - The channel editor's Branding-tab "Show On Now / Next overlay" switch follows the exact pattern
of the existing logo-bug toggle: on adds the built-in element's id to
graphicsElementIds, off removes it; disabled (with an explanatory caption) when the channel is HLS-Direct.
Accepted trade-off: all channels that enable the toggle share one seeded element's geometry/ content; per-channel customization means editing the shared YAML or attaching a different element (the join is general, not restricted to the seeded one).