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.9 KiB
key, title, status, since, supersedes, superseded-by, rule, signals, mechanics
| key | title | status | since | supersedes | superseded-by | rule | signals | mechanics |
|---|---|---|---|---|---|---|---|---|
| ffmpeg.external-logo-graphics-engine | 2026-07-20 — External-URL channel logos pass through to the graphics engine; never `File.Exists`-gated, never ffmpeg-native (#502) | active | 2026-07-20 | none | none | External-URL channel logos pass through to the graphics engine like any other watermark source; `WatermarkSelector` must never gate them on `File.Exists` (always false for a URL) and never route them through the ffmpeg-native overlay shortcut. | external-URL logo, WatermarkSelector, graphics engine · paths: `WatermarkSelector`, `FFmpegLibraryProcessService`, `ImageElementBase.LoadImage` · issues: #502, #67, #1, #510, #511 | `WatermarkSelectorChannelLogoTests`; `ChannelLogoWatermarkOptions` helper |
A channel whose logo is an external URL never rendered an on-screen bug, even with a
ImageSource = ChannelLogo watermark attached. WatermarkSelector resolved the URL correctly and then
existence-checked it on the filesystem — File.Exists("https://…") is always false — so all three
precedence levels (playout item, channel, global) logged "Channel logo no longer exists" and returned
None. The channel editor advertises the URL as winning over an uploaded logo, which is true for the
guide listing and was silently false for the bug.
External artwork passes through; it is not downloaded into the image cache. That is already the
codebase-wide convention — ChannelPlaylist (M3U), the XMLTV template data and Channels/Mapper
(SPA JSON) all emit the raw URL and let the client fetch it. No fetch→SaveArtworkToCache glue exists
anywhere, and adding it here would have invented a second convention for one consumer. The render path
needs no such glue: ImageElementBase.LoadImage already detects an http(s) path, fetches it with
HttpClient, and decodes it with ImageSharp for real pixel dimensions.
A remote-URL watermark is therefore forced onto the graphics engine, not the ffmpeg-native path.
FFmpegLibraryProcessService normally shortcuts a single permanent watermark into a WatermarkInputFile
handed to ffmpeg as a bare -i argument (and to ffprobe for animation detection). ffmpeg would likely
open an http URL itself, but that puts an unbounded network fetch inside stream startup, with no
timeout, redirect or auth handling under our control, and with dimensions left as a FrameSize(1, 1)
placeholder. The graphics engine is the path actually built for remote images, so the shortcut now
additionally requires a non-URL path.
Scope held deliberately narrow — two adjacent defects were left alone:
- The generated-initials fallback (no logo artwork ⇒
ChannelLogoGenerator.GenerateChannelLogoUrl, which hardcodeslocalhost, issue #1) is also killed by the sameFile.Exists. Reviving it is deferred by an earlier entry in this file, so it stays ignored — now behind an explicit comment and a scope-guard test rather than as an accident of the existence check. - The deco path (
OptionsForWatermarks→ the privateGetWatermarkOptions) has always returned its resolved path unchecked, so #502'sFile.Existsdefect never reached it and its resolution is unchanged. Aligning its missing-file and no-artwork behavior with the three precedence levels is a behavior change in its own right, tracked as #510.
The routing change is NOT scoped that way, deliberately. CanUseFFmpegNativeWatermark keys off the
resolved WatermarkOptions.ImagePath alone, and SelectWatermarks puts deco-derived options into the same
list — so a deco watermark resolving to a URL is rerouted to the graphics engine too, including the
generated-initials http://localhost:…/iptv/logos/gen URL that the deco path does still pass through.
Routing by provenance instead of by what the path actually is would mean deciding the same thing twice and
letting the two drift; the URL-aware path is the right one for any URL. Worth knowing when picking up #510:
that fallback plausibly rendered through ffmpeg before and now composites through ImageSharp, which this
change's live-E2E did not cover.
Accepted cost asymmetry. Graphics-engine compositing is per-frame ImageSharp work rather than ffmpeg's
overlay filter, so two channels with visually identical bugs now transcode at different cost based only on
whether the logo is a URL. The rejected fetch-once-into-the-image-cache alternative would have avoided that;
if #511 (remote-fetch hardening: timeout, size cap, pooling, caching) adds such a cache, this goes with it.
The three gated levels now share one ChannelLogoWatermarkOptions helper so they cannot drift apart
again — the duplication is what let the defect exist in triplicate. Covered by
WatermarkSelectorChannelLogoTests, which pins the external-URL fix, both preserved regressions
(cached local path, missing local file ignored) and the generated-fallback scope guard.