# Decisions — lifecycle log Purpose: why the codebase does what it does, so agents don't "fix" an established convention or relitigate a settled call. **Update this doc (or a topic file under `docs/decisions/`) in the same PR that changes any fact below** (or that establishes a new convention worth recording). **Lifecycle, not append-only (ersatztv#521, supersedes the ersatztv#303 H9 append-only mechanic).** A migrated record is an H2 whose first non-blank content line is a metadata block: > YYYY-MM-DD — Title … (#issue) > `` `key: area.topic` · `status: active` · `since: YYYY-MM-DD` · `supersedes: none` · `superseded-by: none` `` > **Rule:** one-line current rule. > **Signals:** concept · paths: a/b.yml · issues: #issue > **Mechanics:** docs/some-doc.md → section > \ (Blockquoted here so the illustrative `key`/heading don't parse as a real record; the `## 2026-07-17 — No persistent compiler servers …` entry below is a live example.) Five metadata fields: `key` (dotted, e.g. `ci.runner-placement`), `status`, `since`, `supersedes`, `superseded-by`. The **`Signals:`** line is also required (validator-enforced, ersatztv#545): it is the keywords/paths MemPalace's recall matches on, so a record without it ingests with weak recall metadata and under-surfaces — pack it with synonyms, symbol names, `paths:`, and issue refs. `status` is one of: - **`active`** — the current, authoritative record for its `key`. Exactly one active record per key. - **`superseded`** — reversed by a newer record; relocated to `docs/decisions/archive/` with `superseded-by: ` pointing forward, and the successor's `supersedes` pointing back. - **`retired`** — no longer applicable (not reversed, just obsolete); also lives in the archive. (An H2 with no metadata block is `legacy-unmigrated` — not yet ported to this schema. The validator tolerates these and reports a count; they trend to zero over time, not required to hit zero at once.) **Two optional fields (ersatztv#603).** `` `stale-after: YYYY-MM-DD` `` on the metadata line marks a record that asserts a fact about the *outside world* as due for re-confirmation once that date passes — an absolute date, no TTL. A **`Sources:`** line in the metadata block cites the evidence the record rests on (a CI run, a measurement, a log line), as distinct from `Signals:`, which is recall keywords. Both are optional and never required; a malformed `stale-after` is a blocking error (it would silently never fire), but a *past-due* record is only a non-blocking notice — going stale is the passage of time, not a defect in whatever commit happens to be under test. Dated records are listed in the catalog's **Review due** section, which renders the date only and never a clock-derived verdict. **Supersession is same-PR, not a later consolidation pass**: add the new active record, then relocate the predecessor's file (or section) into `docs/decisions/archive/` with the metadata above rewritten to `status: superseded` (or `retired`) and the reciprocal `supersedes`/`superseded-by` links filled in on both records. Never silently rewrite a record's rationale prose in place. **Generated active catalog**: `docs/decisions/README.md` is generated from every `active` record (this file + the topic files, excluding the archive) via `scripts/build_decisions_catalog.py` — run it after any status change; CI's `decisions lifecycle` job fails on drift (`--check`). **Enforcement**: `scripts/decisions_validate.py` checks metadata well-formedness (including a required `Signals:` line), one-active-record-per-key, reciprocal links, that no record vanishes from the active set without an archive copy, and that the active catalog is in sync. Separately, and as a NON-BLOCKING warning rather than an error, it reports a per-record prose ceiling (ersatztv#620). The aggregate active-corpus line budget that used to sit in this list — itself the replacement for an older 1800-line floor on this single file — is RETIRED: the total is now printed as an unthresholded trend notice only. The Husky `pre-commit` hook runs the structural checks over the working tree; the CI `decisions lifecycle` job additionally runs the body-diff check with `--base`/`--head`. **`Decisions-Edit: yes`** (a git **trailer** — the message's final paragraph, alongside `Co-Authored-By:`; the pre-#609 `[decisions-edit]` substring is retired and no longer exempts anything) is narrow: it is required ONLY when a commit changes the **rationale prose** of a surviving or archived record — a factual correction to already-written history. Routine lifecycle writes (adding a new active record, relocating a superseded/retired record to the archive, updating metadata fields, regenerating the catalog) are token-free; the validator proves they're legitimate structurally instead of gating on the token. --- ## Index Decisions are split between the chronological log **in this file** and four **topic files** under `docs/decisions/` — large same-topic clusters extracted at the v26.9.0 consolidation (full rationale preserved). Check the relevant topic file below for its subject; otherwise scan the in-file entries. **Topic files:** - [`decisions/optimistic-concurrency.md`](decisions/optimistic-concurrency.md) — ETag / If-Match / `Version` optimistic concurrency (#253, #259, #265, #269). Mechanics: `api-conventions.md` §7a–§7c. - [`decisions/api-auth-security.md`](decisions/api-auth-security.md) — API/SPA auth & security posture (#197 bundles + #279 headers, #206, #283, #292, #295, #301, #319, #330). - [`decisions/release-ci-governance.md`](decisions/release-ci-governance.md) — release / CI / merge governance (#303 H3/H4/H5/H6/H9/H10, #311, #314, #315, #335). - [`decisions/spa-modularization.md`](decisions/spa-modularization.md) — App.tsx screen/shell extraction epic #243 (#244, #245, #247). - [`decisions/workflow-process.md`](decisions/workflow-process.md) — session workflow, CI-run triage, review routing, worktree/parallel-session hygiene and tooling gotchas (#542). Extracted from the kickoff handoff doc, which was their only copy. **In this file:** --- gates, and cheaper to reason about than the version-string parsing in `NvidiaHardwareCapabilities`. their backend endpoints exist. No `/api` endpoint was added, so no OpenAPI regen. re-throw cancellation is a separate, pre-existing concern. pre-fix shape, 15 912 of 20 000 rounds over-claimed. **Per-viewer resume was deliberately not built.** `OnDemandCheckpoint` is a single value on the playout, so resume is per-channel, not per-viewer. #68 states per-channel suffices for a single household; multi-viewer identity would diverge from this model and is out of scope. leaving the shared tree dirty is the one outcome that would make this script a net negative. **Negative control (inherited from #231/#250).** A dedicated test hammers unbalanced releases on an empty pool while reader threads sample the count; none may ever observe a value below zero. Reinstating the pre-#539 decrement-first body makes it fail (`sawNegative > 0` — the readers catch the transient `−1`); verified. As with the #536 tests, break the primitive by reverting the real body, **not** `if (true)` (CS0219 under warnings-as-errors leaves `--no-build` running a stale, still-fixed dll). **SPA is id-fields-first; the API is complete ahead of the UI.** The Channel Builder + Auto-Tune DetailPanel re-add a real "None" option to the five id selects (watermark + fillers) — the pickers #89 had degraded to "Inherit"-only — routed through a `CLEAR` overrides sentinel folded into `advanced.clear` at request-build time (`applyOverridesToRequest`, so the sentinel never leaks as a field value). The three string clear-fields are covered by the backend enum for machine clients (MCP) but the SPA text inputs keep "empty = inherit"; adding a tri-state to those inputs is deferred, not blocked. This is the deliberate "REST API is a real audience" posture (`rest-api-purpose-mcp-and-new-ui`). `security.iptv-browser-token`. external-process pipeline remains #563's. picker; TZ-independence holds only for divisors of 60. See #77 (prior art) and #392. **`Unknown = 0` is the honest legacy default.** A new non-null int column defaults existing rows to `0`; making that `Unknown` (rather than `UserCreated`) means pre-migration rows say "we never recorded this" instead of asserting a provenance we cannot know. The SPA badges only `AutoTuned`, so `Unknown` and `UserCreated` both render unbadged. Enum (not `bool IsAutoTuned`) so a future origin (e.g. `Imported`) is additive without a wire-contract break. Stamped in `CreateChannelFromLineupHandler.BuildChannel`, which is the single channel-construction primitive `CreateAutoTunedChannelsHandler` delegates to, so both the lineup endpoint and bulk auto-tune are covered by one stamp site. Empty-schedule and broken-source fault detection remain deferred to #415. before there's a measured cost would be premature. (`dateMacro.test.ts`, and the property test in `roundtrip.test.ts`, #438) rather than a special case. ## Records formerly in this file Each record below moved to its own file under `records/` (ersatztv#610); the rationale is unchanged. Resolve by **key** — that is the stable identity. A date-based pointer from another doc or an old issue comment should land here and then follow the link. - 2026-06 — REST API wraps existing MediatR handlers 1:1, no service layer — [`api.mediatr-passthrough`](decisions/records/api/mediatr-passthrough.md) - 2026-06 — UI rebuild is a React SPA (ChicoryTV) on the REST API, not a Blazor reskin — [`spa.spa-rebuild-decision`](decisions/records/spa/spa-rebuild-decision.md) - 2026-07 — PUT-replace list endpoints derive `Index` from array order; alternate-schedules last row = catch-all default — [`api.put-replace-index-order`](decisions/records/api/put-replace-index-order.md) - 2026-07 — Response DTOs live in `ErsatzTV.Core/Api`, file-scoped `#nullable enable` — [`api.response-dtos`](decisions/records/api/response-dtos.md) - 2026-07 — Templates editor in the SPA is a table, not Blazor's drag-calendar — [`spa.templates-editor-table`](decisions/records/spa/templates-editor-table.md) - 2026-07-07 — API artwork contract: rooted URLs produced server-side — [`api.artwork-rooted-urls`](decisions/records/api/artwork-rooted-urls.md) - 2026-07-07 — Convention docs read at session start, updated in-PR — [`docs.convention-docs-session-start`](decisions/records/docs/convention-docs-session-start.md) - 2026-07-07 — Decode-style endpoints take a row id and look up server-side — [`api.decode-by-id`](decisions/records/api/decode-by-id.md) - 2026-07-07 — Season/episode/music-video drill-in via `parentId`, not new child-listing endpoints — [`api.parentid-drillin`](decisions/records/api/parentid-drillin.md) - 2026-07-09 — "Table, not calendar" convention also covers the deco-templates editor — [`spa.deco-templates-table`](decisions/records/spa/deco-templates-table.md) - 2026-07-09 — Channel numbers: prompt-driven sequential renumber instead of drag-to-reorder — [`spa.channel-renumber-prompt`](decisions/records/spa/channel-renumber-prompt.md) - 2026-07-09 — Collection custom order: move up/down buttons, any-kind collections — [`spa.collection-custom-order-ui`](decisions/records/spa/collection-custom-order-ui.md) - 2026-07-09 — OpenAPI spec mirrors the runtime Newtonsoft serializer (#198) — [`api.openapi-mirrors-runtime`](decisions/records/api/openapi-mirrors-runtime.md) - 2026-07-09 — Per-playout "Schedule reset" button dropped; Reset uses the server-default build mode — [`spa.playout-reset-button`](decisions/records/spa/playout-reset-button.md) - 2026-07-09 — Playback-troubleshooting completion feedback: poll status, no push channel — [`spa.playback-troubleshoot-poll`](decisions/records/spa/playback-troubleshoot-poll.md) - 2026-07-09 — SPA gates Download Media Sample while a session is active — [`spa.download-sample-gate`](decisions/records/spa/download-sample-gate.md) - 2026-07-09 — YAML playout validator: paste-textarea instead of a server file path — [`spa.yaml-validator-textarea`](decisions/records/spa/yaml-validator-textarea.md) - 2026-07-09 — datetime-local instead of Chronic natural-language start parsing — [`spa.datetime-local-input`](decisions/records/spa/datetime-local-input.md) - 2026-07-10 — Playout API mutations return 409 while the build lock is held (#215) — [`api.playout-build-lock-409`](decisions/records/api/playout-build-lock-409.md) - 2026-07-10 — Schedule-item GET returns a flat, non-polymorphic DTO (`ScheduleItemResponseModel`) — [`api.schedule-item-flat-dto`](decisions/records/api/schedule-item-flat-dto.md) - 2026-07-10 — Shared "Add to…" layer lives in `web/src/media/addTo/`; select-mode is an explicit toggle — [`spa.add-to-layer`](decisions/records/spa/add-to-layer.md) - 2026-07-11 — Async-op API contract normalization + playout build observability + F9 scan endpoints (#235) — [`api.async-op-contract`](decisions/records/api/async-op-contract.md) - 2026-07-11 — Blazor Server UI removed (#91 phase b) — [`blazor.ui-removed`](decisions/records/blazor/ui-removed.md) - 2026-07-11 — Channel editor: bare-create entry point + external-logo mutual exclusion (#212) — [`spa.channel-editor-create-logo`](decisions/records/spa/channel-editor-create-logo.md) - 2026-07-11 — EntityLocker: atomic flags + single-owner release discipline, no owner tokens (#231) — [`locking.entitylocker-atomic-flags`](decisions/records/locking/entitylocker-atomic-flags.md) - 2026-07-11 — Legacy→SPA redirect matcher: exact map + ordered segment-template patterns (#204) — [`spa.legacy-redirect-matcher`](decisions/records/spa/legacy-redirect-matcher.md) - 2026-07-11 — Logs column sorting: allow-listed `sortField`/`sortDirection` on `GET /api/logs` — [`api.logs-sort-params`](decisions/records/api/logs-sort-params.md) - 2026-07-11 — Logs page-size is a client-local preference, not a server ConfigElement — [`spa.logs-page-size-local`](decisions/records/spa/logs-page-size-local.md) - 2026-07-11 — Media-source management REST write API + SPA (#202) — [`media.source-mgmt-write-api`](decisions/records/media/source-mgmt-write-api.md) - 2026-07-11 — Post-commit side effects run on `CancellationToken.None` (generalized from #251 to #254) — [`api.postcommit-cancellation-none`](decisions/records/api/postcommit-cancellation-none.md) - 2026-07-11 — Pre-removal Blazor rollback tag `blazor-final` (#205) — [`blazor.rollback-tag`](decisions/records/blazor/rollback-tag.md) - 2026-07-11 — Schedules SPA editor: draft/explicit-Save over instant-persist; Copy includes multi/smart/rerun; shuffled-GET normalization preserved — [`spa.schedules-editor-draft-save`](decisions/records/spa/schedules-editor-draft-save.md) - 2026-07-11 — Trash "See all" reuses library-browse paging; search stays capped per kind (#213) — [`api.search-paging-cap`](decisions/records/api/search-paging-cap.md) - 2026-07-12 — External-collections scans get an authoritative status surface (#271); the SPA timeout is retired — [`scan.collections-scan-status`](decisions/records/scan/collections-scan-status.md) - 2026-07-12 — Live-E2E is a required step for API write-path handler changes (#303) — [`release.live-e2e-required`](decisions/records/release/live-e2e-required.md) - 2026-07-12 — TopBar primary-action button: wire creates, drop the rest (#238) — [`spa.topbar-primary-action`](decisions/records/spa/topbar-primary-action.md) - 2026-07-13 — API versioning: the whole `/api` surface is mounted at `/api/v1`, additive-only after freeze (#286) — [`api.versioning-v1`](decisions/records/api/versioning-v1.md) - 2026-07-13 — Scheduling API hardening: null-name 500s, duplicate template items, unreachable 404 (#172) — [`api.scheduling-hardening`](decisions/records/api/scheduling-hardening.md) - 2026-07-16 — Auto-tuning enumerates via EF, persists via SmartCollection; additive coexistence (#69) — [`sched.auto-tune-foundation`](decisions/records/sched/auto-tune-foundation.md) - 2026-07-16 — Functional-E2E CI harness: advisory curl-contract job over an app booted from source (#299) — [`ci.functional-e2e-harness`](decisions/records/ci/functional-e2e-harness.md) - 2026-07-16 — Optional advertised IPTV base URL (`iptv.base_url`) resolved centrally in the two generators (#340) — [`iptv.base-url`](decisions/records/iptv/base-url.md) - 2026-07-16 — Per-playout reshuffle = scoped Reset build; seed surfaced (#71) — [`sched.reshuffle-scoped-reset`](decisions/records/sched/reshuffle-scoped-reset.md) - 2026-07-17 — Auto-Tune DetailPanel member list = live search-index roll-up, not EF enumeration (#384) — [`sched.autotune-detailpanel-members`](decisions/records/sched/autotune-detailpanel-members.md) - 2026-07-17 — Auto-Tune per-channel overrides reuse the Channel Builder advanced-options DTO; weights + bug-colour logo split out to #425 (#385) — [`sched.autotune-per-channel-overrides`](decisions/records/sched/autotune-per-channel-overrides.md) - 2026-07-17 — Clock-boundary schedule padding already exists (FillerMode.Pad); #77 verified, convenience toggle deferred — [`sched.clock-padding-existing`](decisions/records/sched/clock-padding-existing.md) - 2026-07-17 — Docs-only CI skip gates STEPS in always-running required jobs, never `if:`-skips them (#416) — [`ci.docs-only-skip-steps`](decisions/records/ci/docs-only-skip-steps.md) - 2026-07-17 — Docs-only detect must be shallow-checkout safe: FETCH_HEAD + two-dot, not origin/main + three-dot (#416 follow-up) — [`ci.docs-only-detect-shallow-safe`](decisions/records/ci/docs-only-detect-shallow-safe.md) - 2026-07-17 — Health-check remediation is server-declared `{Kind, Target}` on an additive DTO; the SPA acts on it (#164) — [`api.healthcheck-remediation-dto`](decisions/records/api/healthcheck-remediation-dto.md) - 2026-07-17 — No persistent compiler servers in CI; every `services:` container gets an explicit cap; #390's small-lane move reversed (#406) — [`ci.runner-placement`](decisions/records/ci/runner-placement.md) - 2026-07-17 — Pre-push guard: don't push a file whose working-tree copy is uncommitted (H13, #416 session) — [`release.prepush-clean-worktree-guard`](decisions/records/release/prepush-clean-worktree-guard.md) - 2026-07-17 — Seasonal / date-conditional scheduling already exists (alternate schedules / playout templates); #73 closed as implemented — [`sched.seasonal-scheduling-existing`](decisions/records/sched/seasonal-scheduling-existing.md) - 2026-07-17 — Shuffle-source construction extracted to `ShuffleSourceBuilder`; per-family seam, not a god-factory (#380) — [`sched.shuffle-source-builder`](decisions/records/sched/shuffle-source-builder.md) - 2026-07-17 — Weighted / fair-share distribution is a new `WeightedShuffle` order; `ShuffleInOrder` is anti-clumping, not fair-share (#70) — [`sched.weighted-shuffle`](decisions/records/sched/weighted-shuffle.md) - 2026-07-18 — Auto-Tune DetailPanel SPA: reusable `SlideOver` + shared advanced-options model; decorative panes dropped to match the backend (#386) — [`spa.autotune-detailpanel-slideover`](decisions/records/spa/autotune-detailpanel-slideover.md) - 2026-07-18 — Auto-Tune per-source weights ride #70's MultiCollection machinery; created at tune time, not a post-hoc PUT (#425) — [`sched.autotune-per-source-weights`](decisions/records/sched/autotune-per-source-weights.md) - 2026-07-18 — CI build-once was measured and rejected; keep the #420 tree-skip — [`ci.build-once-rejected`](decisions/records/ci/build-once-rejected.md) - 2026-07-18 — Collapsible sidebar + nav-group accordions: two `ctv-sidebar-*` localStorage keys, labeled groups default-collapsed (#396) — [`spa.sidebar-collapsible-accordions`](decisions/records/spa/sidebar-collapsible-accordions.md) - 2026-07-18 — Never-scanned `LastScan` surfaces as null at the API boundary, not the 0001-01-01 MinValue sentinel (#409) — [`media.lastscan-null-boundary`](decisions/records/media/lastscan-null-boundary.md) - 2026-07-18 — Search all-items is paged to cap DoS exposure; SPA add-all pages to completeness (#293) — [`api.search-allitems-paging`](decisions/records/api/search-allitems-paging.md) - 2026-07-18 — Unsupported PlaybackOrder is loud at build time; a declared support matrix and tripwire test make new orders safe by construction (#403) — [`sched.playbackorder-support-matrix`](decisions/records/sched/playbackorder-support-matrix.md) - 2026-07-19 — A media-server library sweep refuses to flag when a successful fetch returns zero items, rather than nuking the whole library (#477) — [`scan.zero-item-fetch-guard`](decisions/records/scan/zero-item-fetch-guard.md) - 2026-07-19 — CI `test` job reports a sampled true peak-anon, not cache-inflated `memory.peak` (#412) — [`ci.peak-anon-measurement`](decisions/records/ci/peak-anon-measurement.md) - 2026-07-19 — Health-check results are TTL-cached; `?refresh=true` forces a fresh run (#431) — [`api.healthcheck-ttl-cache`](decisions/records/api/healthcheck-ttl-cache.md) - 2026-07-19 — Media-server remote-stream URLs are probed before use: a redirected 404 fails closed, everything else fails open, no toggle (#473) — [`media.remote-stream-probe`](decisions/records/media/remote-stream-probe.md) - 2026-07-19 — The `format` gate runs `dotnet format whitespace . --folder`, not the full solution format (#469) — [`ci.format-gate-folder-mode`](decisions/records/ci/format-gate-folder-mode.md) - 2026-07-19 — WeightedShuffle SPA: weights edited on the multi-collection, order offered only on classic MultiCollection schedule items; fair-share is a reset not a mode (#404) — [`sched.weightedshuffle-editor`](decisions/records/sched/weightedshuffle-editor.md) - 2026-07-20 (#489) — Jellyfin mixed-content libraries map to one library holding many kinds — [`scan.jellyfin-mixed-content-library`](decisions/records/scan/jellyfin-mixed-content-library.md) - 2026-07-20 (#498) — QSV decode is split from QSV encode via a single `QsvPreferNativeDecoder` bool — [`ffmpeg.qsv-decode-encode-split`](decisions/records/ffmpeg/qsv-decode-encode-split.md) - 2026-07-20 — External-JSON playout channels now probe the remote-stream URL too, closing the #473 scope gap (#480) — [`media.remote-stream-probe-externaljson`](decisions/records/media/remote-stream-probe-externaljson.md) - 2026-07-20 — External-URL channel logos pass through to the graphics engine; never `File.Exists`-gated, never ffmpeg-native (#502) — [`ffmpeg.external-logo-graphics-engine`](decisions/records/ffmpeg/external-logo-graphics-engine.md) - 2026-07-20 — HLS cold start is fixed with `-readrate_initial_burst`, not by raising the work-ahead limit (#350) — [`ffmpeg.hls-cold-start-burst`](decisions/records/ffmpeg/hls-cold-start-burst.md) - 2026-07-20 — MCP server (`ErsatzTV.Mcp`) built fresh over frozen `/api/v1`: read + cautious writes (#58) — [`mcp.server-foundation`](decisions/records/mcp/server-foundation.md) - 2026-07-20 — One logo drives the bug via a shared ChannelLogo preset, not new schema (#67) — [`iptv.logo-drives-bug-preset`](decisions/records/iptv/logo-drives-bug-preset.md) - 2026-07-20 — Remote graphics-engine images are fetched through a bounded, pooled `IRemoteImageFetcher`; re-fetched per element init, not cached (#511) — [`ffmpeg.remote-image-fetcher-bounded`](decisions/records/ffmpeg/remote-image-fetcher-bounded.md) - 2026-07-20 — `ILibraryRepository.GetOrAddFolder` resolves the folder from the DB, not the caller's `LibraryPath.LibraryFolders` navigation (#488) — [`scan.getoraddfolder-db-lookup`](decisions/records/scan/getoraddfolder-db-lookup.md) - 2026-07-20 — `runs-on: small` means git-only; the two `docker build` jobs move to `ubuntu-latest` (server-management#639) — [`ci.small-lane-git-only`](decisions/records/ci/small-lane-git-only.md) - 2026-07-21 — An on-demand time shift rebuilds the channel's cached XMLTV so the guide can't lag playback (#68) — [`scheduling.ondemand-guide-refresh-on-thaw`](decisions/records/scheduling/ondemand-guide-refresh-on-thaw.md) - 2026-07-21 — Browser channel preview is a server-declared per-channel capability (#60) — [`api.channel-preview-capability`](decisions/records/api/channel-preview-capability.md) - 2026-07-21 — Check the worked issue before the decision corpus; a closed tracker's comments need no retrofit (#524) — [`docs.tracker-comment-retrofit`](decisions/records/docs/tracker-comment-retrofit.md) - 2026-07-21 — Decision records carry a lifecycle schema, validated by a script; append-only-by-diff is retired (#521) — [`docs.decision-lifecycle`](decisions/records/docs/decision-lifecycle.md) - 2026-07-21 — External channel-logo URLs are downloaded and cached at save time; the render path never fetches a logo (#525) — [`graphics.channel-logo-caching`](decisions/records/graphics/channel-logo-caching.md) - 2026-07-21 — Parallel orientation + selection is the startup protocol; #237 retired (#520) — [`startup.parallel-orientation`](decisions/records/startup/parallel-orientation.md) - 2026-07-21 — QSV hardware-frame headroom is a floor, not an operator preference (#529) — [`ffmpeg.qsv-extra-hw-frames-floor`](decisions/records/ffmpeg/qsv-extra-hw-frames-floor.md) - 2026-07-21 — Session end fast-forwards the shared checkout; a stale tree serves stale FILES (#541) — [`session.shared-checkout-refresh`](decisions/records/session/shared-checkout-refresh.md) - 2026-07-21 — Work-ahead slots are claimed atomically by the caller, released by the transcode it hands them to (#536) — [`ffmpeg.work-ahead-slot-atomic`](decisions/records/ffmpeg/work-ahead-slot-atomic.md) - 2026-07-21 — `WorkAheadSlots.Release()` clamps before decrementing and reports unbalance in-band (#539) — [`ffmpeg.work-ahead-slot-release-never-negative`](decisions/records/ffmpeg/work-ahead-slot-release-never-negative.md) - 2026-07-21 — `from-lineup` advanced overrides express "clear to none" via a typed `clear` enum list (#135) — [`api.from-lineup-clear-to-none`](decisions/records/api/from-lineup-clear-to-none.md) - 2026-07-22 — Channel-level graphics-element attachment + seeded On Now/Next text element (#74) — [`graphics.channel-level-attachment`](decisions/records/graphics/channel-level-attachment.md) - 2026-07-22 — Sequential (YAML) playout gets a golden; Scripted is excluded from the golden net by construction (#381) — [`testing.scripted-playout-golden-deferred`](decisions/records/testing/scripted-playout-golden-deferred.md) - 2026-07-22 — per-schedule clock-boundary padding is a synthetic content-less Pad over the existing per-episode machinery (#392) — [`sched.clock-padding-schedule-toggle`](decisions/records/sched/clock-padding-schedule-toggle.md) - 2026-07-23 — Channel health = a server-derived `health` object on the channel DTOs, built-timeline detection (#415) — [`api.channel-health-object`](decisions/records/api/channel-health-object.md) - 2026-07-23 — Channel origin is immutable creation-provenance, stamped at insert, not a health signal (#414) — [`channel.origin-marker`](decisions/records/channel/origin-marker.md) - 2026-07-23 — Facet-value typeahead is a new endpoint, allow-listed to text fields, no caching (#434) — [`api.search-field-values`](decisions/archive/api/search-field-values.md) (superseded by `api.search-field-values-sources`) - 2026-07-23 — Relative-date rule builder operators are a frontend-only mapping onto existing Lucene macros (#435) — [`rulebuilder.relative-date-macros`](decisions/records/rulebuilder/relative-date-macros.md) - 2026-07-25 — A media-server sweep also refuses when the api client silently dropped items whose projection threw; the ratio threshold is rejected (#484) — [`scan.projection-failure-sweep-guard`](decisions/records/scan/projection-failure-sweep-guard.md) - 2026-07-25 — LibraryFolder identity is enforced by a unique index on `(LibraryPathId, PathHash)`, not an in-process lock (#491) — [`scan.libraryfolder-unique-identity`](decisions/records/scan/libraryfolder-unique-identity.md) - 2026-07-25 — Music videos carry a per-library server identity; reconciliation is an itemId diff + soft trash (#496) — [`scan.musicvideo-server-identity`](decisions/records/scan/musicvideo-server-identity.md) - 2026-07-25 — OKF evaluated and rejected as a replacement; two of its optional fields adopted (#603) — [`docs.decision-optional-provenance`](decisions/records/docs/decision-optional-provenance.md) - 2026-07-25 — Rule-builder group nesting is bounded-arbitrary depth (`MAX_GROUP_DEPTH`), not one level (#436) — [`spa.rulebuilder-nesting`](decisions/records/spa/rulebuilder-nesting.md) - 2026-07-25 — The rationale-edit marker is a git trailer, not a substring anywhere in the commit range (#609) — [`ci.decisions-edit-trailer`](decisions/records/ci/decisions-edit-trailer.md) - 2026-07-25 — UI-E2E: headless Playwright flows in the existing `functional-e2e` job, browser baked into the CI image (#445) — [`ci.ui-e2e-harness`](decisions/records/ci/ui-e2e-harness.md) - 2026-07-26 — Facet-value typeahead restated: every artist source covered; the JSON-column source is paged by row position with no residual SQL predicate (#578) — [`api.search-field-values-sources`](decisions/records/api/search-field-values-sources.md)