docs(release): v26.16.0 notes record the .NET 10.0.14 runtime and the release scan
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 15s
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Canceled after 30s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 0s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 0s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Canceled after 32s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Canceled after 0s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Canceled after 0s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Canceled after 0s
PR Gates / Docs update reminder (pull_request) Successful in 14s
PR Gates / decisions lifecycle (pull_request) Canceled after 17s
PR Gates / CI image pin matches docker/ci (pull_request) Canceled after 1s
PR Gates / Fix proofs (Proves trailers) (pull_request) Canceled after 16s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Canceled after 14s
review-verdict/h10 Awaiting review verdict for e458630
Review verdict / Set review-verdict status (pull_request_target) Successful in 40s

The tag build picked up MCR's 10.0.14 aspnet/sdk images (published
2026-10-26), measured inside :26.16.0. ZAP and semgrep results are
recorded as triaged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: 99d1dedb-f027-4e05-baab-2111f3a7d951
This commit is contained in:
2026-10-27 20:06:42 +01:00
co-authored by Claude Opus 5.5
parent bdd4ed7bad
commit e458630f48
+1 -1
View File
@@ -42,7 +42,7 @@ Upstream's final release was **`v26.3.0`** (archived). Our line continues from t
| `v26.13.0` | **RuleBuilder maturation** — arbitrary-depth group nesting (#436), inline smart-query authoring in Channel Builder (#437), DB-sourced facet typeahead + relative-date operators + validation (#434/#435/#438), and an artist typeahead covering music-video/song credits with `album_artist` no longer 404ing (#578). **Per-channel On Now/Next transient overlay** (#74/#570) and **per-schedule clock-boundary padding** (#392); in-browser channel preview (#60); Auto-Tune per-source weight steppers + exclude/add-untagged (#440). Library-browse pickers now resolve by search instead of a 100-row window, closing several silent at-cap truncations (#644/#650/#651/#634). Correctness: one watermark resolver for all four attachment points, incl. `MiddleCenter` (#503/#510); QSV HDR tonemaps through OpenCL because `vpp_qsv=tonemap` is a silent no-op (#505); `LibraryFolder` unique index + concurrent-insert tolerance (#491); per-library music-video identity with soft trash (#496); Jellyfin Album/Track music-video projection (#177); metadata-collection dedup (#500); accented facet values via a registered Unicode fold on SQLite (#668); `WorkAheadSlots` atomic slot claim, never a negative count (#536/#539); on-demand guide rebuild on thaw (#68). Process/CI: the H10 review-verdict gate became a sha-bound **required** commit status and was hardened through its false-open chain (#622/#629/#632/#648/#649/#672/#698), the decision corpus split to one YAML-frontmatter record per file (#610/#620), and headless Playwright UI-E2E flows landed (#445/#533). Five dual-provider migrations. |
| `v26.14.0` | **Live TV no longer starves on embedded bitmap subtitles** — `-readrate` paces an input off its *furthest-behind* stream, and a PGS/DVD subtitle read through the video's own `-i` is sparse enough to drag the whole process to **0.53x realtime** against the 1.0x a client consumes, draining the buffer until the channel stalls. Fixed with a capability-gated `-readrate_catchup` (ffmpeg 8.0+) on realtime inputs, keeping `-readrate` on the frame-producing path so the `ffmpeg.qsv-extra-hw-frames-floor` bound is untouched; measured 0.533x → 1.067x on QSV and software, with a 240s QSV soak clean of allocation errors (#726). Affects items carrying an embedded bitmap subtitle matching the channel's subtitle mode — 3,182 of 24,646 media versions on prod, and a property of the *item*, not the channel, which is why the stall presented as random. Process/CI: the H10 review-verdict gate's repair sentinel became a fixed point and its write is now fenced on the timeline retarget count, closing a raced-sentinel false-open (#706/#707/#711). **The decisions validator now cross-checks its dependency-free frontmatter parse against PyYAML** and reports both the truncating unquoted `` #`` and the scalar-closing bare apostrophe as errors, so a record whose `rule:` silently halves under PyYAML fails the local gate instead of CI (#674/#688) — the ceiling-calibration claim was also split so the suite pins what the derivation MEANS rather than live-corpus order statistics. Dependencies: CliWrap 3.10.4, JetBrains.ReSharper.GlobalTools 2025.3.5. |
| `v26.15.0` | **`main` became PR-only, which is what makes `review-verdict/h10` load-bearing** — branch protection now carries `enable_push: false` *and* `block_admin_merge_override: true`, so a direct `git push origin HEAD:main` is refused at pre-receive for every account including a site admin, and an admin cannot force-merge past a missing or red required context (#743/#749). Gitea only evaluates `status_check_contexts` on the PR merge path, so before this the whole gate was skippable with no forgery needed. The gate was then hardened through the rest of its false-open chain: CI's registry credential scoped so a head-resolved workflow cannot forge the status (#697), a stray expression delimiter **in a comment** that killed the gate outright and is now banned on the release path itself by a `scan` job `build` depends on (#751/#767), a positive verdict inherited only from an allow-listed reviewer (#742), the PR-timeline `HEAD` alias fenced on the `pull_push` count (#803/#664), and both `/statuses/{sha}` reads paged to a validated terminator (#763). App-facing: **FFmpeg profile numeric fields are bounded with a 422 that names the bound and its consequence** instead of being silently rewritten, and readrate pacing is exposed as a profile field (#735 — one dual-provider migration, `Add_FFmpegProfile_ReadRatePacing`). **On Now / Next gets a boxed background and is on by default** (#732). Field-level progressive disclosure lands as a shared `FieldHelp` trigger + portalled panel, adopted on FFmpeg Profiles and documented as `docs/spa-conventions.md` §15 (#734). Auto-Tune proposal rows align on a grid (#721); `AddItemsDialog` resolves by search instead of windowing a whole media-library type, with stale-response and is-mounted guards (#685/#740). Correctness: a paged handler now counts **the same query it pages**, so a filtered page stops reporting the unfiltered total across playouts, rerun collections and six sibling handlers (#690/#758); nullable primitive collections are guarded at the read site rather than mutated on a tracked entity — `SongMetadata.Artists` in both search indexers (#701/#824) and the scheduling `DaysOfMonth`/`MonthsOfYear`/`DaysOfWeek` columns, where a runtime NULL reads as **unrestricted** (#823); SPA full-replace bodies are built as `Complete<T>` so an omitted optional DTO field is a type error (#807); and every `ErsatzTV.Mcp` tool is pinned to its OpenAPI contract, with `graphicsElementIds` and `padToNearestMinute` declared so an MCP channel update stops silently stripping the overlay (#754/#757). Process/CI: one rule for how a guard derives its population and one for the proof it ships with, both enforced (#774/#775) — populations come from the git index, never a filesystem walk (#806/#819), every `MUTATION` grade is executed each suite rather than asserted in prose (#790), and verification code is itself code under test (#796). Workflows declare their own per-job metadata and guard populations derive from it (#786/#789); all six declare `permissions:` (#748); `ci-image.yml` publishes from `main` only and the persisted checkout credential is gone (#744/#746/#835). Hooks report their own execution and the report is measured (#776), and `docs.no-session-narrative` was generalized to every durable artifact and the sites its sweep IDENTIFIED were remediated as judgements rather than by regex — no closure is claimed over a phrasing space (#784/#812). A ruff config is committed and enforced in CI (#780). Dependencies: Scriban.Signed 7.2.6. **Cut at `736649b3b`, not at `main`'s head**: `completeAnnotations.guard.test.ts` (merged in the very next commit) needs a git index the Dockerfile's `web-build` stage does not have, so every image build after `736649b3b` fails — including a tag build (#887). `736649b3b` is the newest commit whose `Build & push image (amd64)` actually ran green. The scheduling-recurrence fix #880 therefore slips to the next release. |
| `v26.16.0` | **Channels carrying Jellyfin music videos play again** — the music-video scanner wrote the library-items projection's empty stream list straight through, so every Jellyfin music video was stored with no `MediaStream` rows (1520/1520 on prod) and `SelectVideoStream` threw on tune-in while the guide looked normal; it now reads statistics from per-item playback info under the movie scanner's conditions, writes nothing when the server cannot answer, and `SelectVideoStream` fails with the version, file and remedy instead of a bare `Sequence contains no matching element`. **Existing rows heal only on a deep scan** (`POST /api/v1/libraries/{id}/scan?deep=true`), since a regular scan skips unchanged etags (#924/#928, `scan.mediaserver-statistics-from-server`). **Alternate-schedule and template recurrence arrays: an absent array means unrestricted, an explicit `[]` is rejected with a 422 that names the consequence** — the three recurrence sets are read conjunctively, so `?? []` on an omitted array returned 200 while storing an item that could never apply; absent and null now normalize to the same `All*()` sets the read side substitutes, and an unchanged pre-existing empty set is still accepted so no other item in a whole-list PUT becomes uneditable (#880/#892 — slipped from v26.15.0). **Unknown channel `graphicsElementIds` are rejected with a 422** (and the same bounded id-list validation applies to Deco updates), and the built-in graphics elements are identified by the seeded path, not the filename (#568/#922). The channel preview says why it is not playing and only the preview opts into muted autoplay (#554/#910); a failed write in the Collections screen reports to a surface that outlives the dialog that started it (#830/#878). Correctness: a classifier-unreadable branch-protection rule (a glob rule with an empty `branch_name`) is no longer resolved to a rule that matches nothing, which had let the merge hook auto-grant against a rule Gitea may be applying (#858/#859/#897); an empty PR-timeline page before the 20-page cap is no longer read as exhaustion, which a PR author could use to hide an ABA force-push behind 50 comments — narrowed, not closed (#870/#896); a head-authored run holds no stored secret, so the PR route reads the registry and status API anonymously (#885/#919); `Complete<T>` annotations are now derived where applied, not only where defined (#820/#883). **Image builds fixed**: the Dockerfile's `web-build` stage built the SPA and then ran a vitest suite with a hand-written `--exclude` list, so `completeAnnotations.guard.test.ts` (needs a git index the stage lacks) failed every `main` and tag image build after `736649b3b`; the stage now builds the SPA and does not test it, the suite runs once unfiltered in the `test` job `build` depends on, and a guard pins that delegation (#887/#899). Process/CI: the verdict gate replaces every unknown state and proves the clauses that claim to (#849/#890), the verdict writer checks the gate will honour what it just posted (#845/#889), a sourced path is code so every hook resolves it from its own tree (#891/#903), a mutation-outcome claim is executed wherever it is written (#881/#914), `--depth` is never passed to a checkout that may be complete (#836/#884), session narrative is swept from hooks, workflows, scripts, tests and code comments (#876/#906), a scripted-engine build API is characterized in-process against a committed script fixture (#563/#915), orchestrated sessions get a mechanics page, standing prompt and pick/build/resume workflows (#907/#908) with the review loop and concurrency cap tightened (#911/#912/#916/#918), and CI claims dated at Gitea 1.25.4 are re-established on 1.27.1 (#869/#893/#905). No dual-provider migrations and no dependency bumps in this release. **Known advisories carried unchanged from v26.15.0**: `System.Security.Cryptography.Pkcs` 9.0.4 (High, transitive via the unused Oracle/SQL Server BulkExtensions providers) and `SixLabors.ImageSharp` 3.1.12 (Moderate, fix only in 4.x), tracked in #929. |
| `v26.16.0` | **Channels carrying Jellyfin music videos play again** — the music-video scanner wrote the library-items projection's empty stream list straight through, so every Jellyfin music video was stored with no `MediaStream` rows (1520/1520 on prod) and `SelectVideoStream` threw on tune-in while the guide looked normal; it now reads statistics from per-item playback info under the movie scanner's conditions, writes nothing when the server cannot answer, and `SelectVideoStream` fails with the version, file and remedy instead of a bare `Sequence contains no matching element`. **Existing rows heal only on a deep scan** (`POST /api/v1/libraries/{id}/scan?deep=true`), since a regular scan skips unchanged etags (#924/#928, `scan.mediaserver-statistics-from-server`). **Alternate-schedule and template recurrence arrays: an absent array means unrestricted, an explicit `[]` is rejected with a 422 that names the consequence** — the three recurrence sets are read conjunctively, so `?? []` on an omitted array returned 200 while storing an item that could never apply; absent and null now normalize to the same `All*()` sets the read side substitutes, and an unchanged pre-existing empty set is still accepted so no other item in a whole-list PUT becomes uneditable (#880/#892 — slipped from v26.15.0). **Unknown channel `graphicsElementIds` are rejected with a 422** (and the same bounded id-list validation applies to Deco updates), and the built-in graphics elements are identified by the seeded path, not the filename (#568/#922). The channel preview says why it is not playing and only the preview opts into muted autoplay (#554/#910); a failed write in the Collections screen reports to a surface that outlives the dialog that started it (#830/#878). Correctness: a classifier-unreadable branch-protection rule (a glob rule with an empty `branch_name`) is no longer resolved to a rule that matches nothing, which had let the merge hook auto-grant against a rule Gitea may be applying (#858/#859/#897); an empty PR-timeline page before the 20-page cap is no longer read as exhaustion, which a PR author could use to hide an ABA force-push behind 50 comments — narrowed, not closed (#870/#896); a head-authored run holds no stored secret, so the PR route reads the registry and status API anonymously (#885/#919); `Complete<T>` annotations are now derived where applied, not only where defined (#820/#883). **Image builds fixed**: the Dockerfile's `web-build` stage built the SPA and then ran a vitest suite with a hand-written `--exclude` list, so `completeAnnotations.guard.test.ts` (needs a git index the stage lacks) failed every `main` and tag image build after `736649b3b`; the stage now builds the SPA and does not test it, the suite runs once unfiltered in the `test` job `build` depends on, and a guard pins that delegation (#887/#899). Process/CI: the verdict gate replaces every unknown state and proves the clauses that claim to (#849/#890), the verdict writer checks the gate will honour what it just posted (#845/#889), a sourced path is code so every hook resolves it from its own tree (#891/#903), a mutation-outcome claim is executed wherever it is written (#881/#914), `--depth` is never passed to a checkout that may be complete (#836/#884), session narrative is swept from hooks, workflows, scripts, tests and code comments (#876/#906), a scripted-engine build API is characterized in-process against a committed script fixture (#563/#915), orchestrated sessions get a mechanics page, standing prompt and pick/build/resume workflows (#907/#908) with the review loop and concurrency cap tightened (#911/#912/#916/#918), and CI claims dated at Gitea 1.25.4 are re-established on 1.27.1 (#869/#893/#905). No dual-provider migrations and no dependency bumps in this release. **The image moves to the .NET 10.0.14 runtime** (prod had 10.0.11), so it carries the October 2026 .NET out-of-band security fixes, including the Brotli decompression overflow (CVE-2026-111497) and the SocketAsyncEventArgs memory corruption (CVE-2026-70197). No code change was needed, because `docker/Dockerfile` follows the floating `aspnet`/`sdk:10.0-noble-amd64` tags. Release scan on the immutable `:26.16.0` (`sha256:6d698162…`), 2026-10-27: ZAP 0 FAIL / 120 PASS / 1 WARN (100001, the SPA catch-all's `302 → /app` HTML, by design); semgrep 2 rule classes, both false positives (a constant-SQL test helper; JSX inside `design-system/` demo HTML, which the app doesn't bundle). **Known advisories carried unchanged from v26.15.0**: `System.Security.Cryptography.Pkcs` 9.0.4 (High, transitive via the unused Oracle/SQL Server BulkExtensions providers) and `SixLabors.ImageSharp` 3.1.12 (Moderate, fix only in 4.x), tracked in #929. |
**Before cutting a release — sweep `docs/decisions.md` + `docs/decisions/`** (ersatztv#521, supersedes
the ersatztv#303 H9 append-only ritual). Supersession/retirement is now a same-PR act (add the new