Docs-only, single commit. Adds the v26.15.0 row to the release table in docs/ci-cd.md.
The tag does NOT go on this commit
Unlike #739 (v26.14.0) and #716 (v26.13.0), the tag will be pushed onto 736649b3b, an ancestor of main — not onto this commit and not onto main's head.
main cannot build an image and has not been able to since e8f80c42c:
e8f80c42c added web/src/api/completeAnnotations.guard.test.ts, a third importer of virtual:etv-tracked-source-files, without adding it to the hand-maintained --exclude list in the Dockerfile's web-build stage — and that stage has no git index by construction. The guard is behaving correctly: it refuses to fall back to a filesystem walk (testing.guard-derives-population-from-source). That is #887, priority: high, claimed and in progress in another session — this PR does not touch it.
A v* tag runs the same Dockerfile, so tagging main's head would fail at the image build and produce no release.
Two independent confirmations that 736649b3b is the newest valid tag point:
Structural — at 736649b3b the only test importing the virtual module is pageSizeCallSites.guard.test.ts, which is in the exclude list. completeAnnotations.guard.test.ts does not exist there (git cat-file -e → absent).
End-to-end — the container registry holds ersatztv:736649b3 and none of the eight shas after it.
Consequence, recorded in the row itself: #880 (scheduling recurrence — an absent array means unrestricted) merged after the break and therefore slips to the next release. The other seven excluded commits are hooks/scripts/CI/docs only, none of which ship in the image.
Release-boundary sweep
Per docs/ci-cd.md → "Before cutting a release":
Step
Result
decisions_validate.py
OK
build_decisions_catalog.py
no drift
superseded/retired under archive/
validator-enforced, clean
legacy-unmigrated count
0
per-record ceiling
45/218 over 60 lines — fraction 0.21, inside the blocking 0.02–0.25 band
aggregate prose trend
10276 lines / 218 records (mean 47) + 680 scaffolding
The validator notes the 60-line ceiling has drifted below the tail boundary it was derived from (p90=104, p95=142) and asks for re-derivation when convenient. That is a maintenance signal about the constant, not a blocker — flagging it rather than silently re-deriving it inside a release PR.
Local gate on 736649b3b
Check
Result
dotnet build ErsatzTV.sln -c Release
0 errors, 60 warnings
dotnet test -c Release --no-build
4556 passed, 7 skipped, 0 failed across 7 projects
dotnet list package --vulnerable --include-transitive
no vulnerable packages, all 17 projects
SPA lint / typecheck / test / build
119/119 files, 1272/1272 tests
Verification of the row itself
Every claim was checked against the range rather than asserted: 8 paged handlers (GetPaged*Handler.cs), exactly one dual-provider migration (Add_FFmpegProfile_ReadRatePacing), SongMetadata.Artists in both ElasticSearchIndex and LuceneSearchIndex, and Scriban.Signed 7.2.6 as the only dependency change across Directory.Packages.props + .config/dotnet-tools.json.
Docs
docs/ci-cd.md is the only doc a release-notes row touches per the CLAUDE.md docs-update table. No route, endpoint, SPA convention or decision changed.
Docs-only, single commit. Adds the `v26.15.0` row to the release table in `docs/ci-cd.md`.
## The tag does NOT go on this commit
Unlike #739 (v26.14.0) and #716 (v26.13.0), the tag will be pushed onto **`736649b3b`**, an ancestor of `main` — not onto this commit and not onto `main`'s head.
`main` cannot build an image and has not been able to since `e8f80c42c`:
| run / job | commit | `Build & push image (amd64)` |
|---|---|---|
| 2459 / 10545 | `736649b3b` (#882) | **success**, 6m45s, published |
| 2472 | `e8f80c42c` (#883) | failure |
| … 6 more … | | failure |
| 2515 / 10843 | `cf5f42edf` (#897) | failure, 86s, died in `web-build` |
`e8f80c42c` added `web/src/api/completeAnnotations.guard.test.ts`, a third importer of `virtual:etv-tracked-source-files`, without adding it to the hand-maintained `--exclude` list in the Dockerfile's `web-build` stage — and that stage has no git index by construction. The guard is behaving correctly: it refuses to fall back to a filesystem walk (`testing.guard-derives-population-from-source`). That is **#887**, `priority: high`, claimed and in progress in another session — this PR does not touch it.
A `v*` tag runs the same Dockerfile, so tagging `main`'s head would fail at the image build and produce no release.
Two independent confirmations that `736649b3b` is the newest valid tag point:
- **Structural** — at `736649b3b` the only *test* importing the virtual module is `pageSizeCallSites.guard.test.ts`, which **is** in the exclude list. `completeAnnotations.guard.test.ts` does not exist there (`git cat-file -e` → absent).
- **End-to-end** — the container registry holds `ersatztv:736649b3` and **none** of the eight shas after it.
Consequence, recorded in the row itself: **#880** (scheduling recurrence — an absent array means unrestricted) merged after the break and therefore slips to the next release. The other seven excluded commits are hooks/scripts/CI/docs only, none of which ship in the image.
## Release-boundary sweep
Per `docs/ci-cd.md` → "Before cutting a release":
| Step | Result |
|---|---|
| `decisions_validate.py` | **OK** |
| `build_decisions_catalog.py` | no drift |
| superseded/retired under `archive/` | validator-enforced, clean |
| legacy-unmigrated count | **0** |
| per-record ceiling | 45/218 over 60 lines — fraction 0.21, inside the blocking 0.02–0.25 band |
| aggregate prose trend | 10276 lines / 218 records (mean 47) + 680 scaffolding |
The validator notes the 60-line ceiling has drifted below the tail boundary it was derived from (p90=104, p95=142) and asks for re-derivation when convenient. That is a maintenance signal about the constant, not a blocker — flagging it rather than silently re-deriving it inside a release PR.
## Local gate on `736649b3b`
| Check | Result |
|---|---|
| `dotnet build ErsatzTV.sln -c Release` | 0 errors, 60 warnings |
| `dotnet test -c Release --no-build` | **4556 passed, 7 skipped, 0 failed** across 7 projects |
| `dotnet list package --vulnerable --include-transitive` | **no vulnerable packages**, all 17 projects |
| SPA `lint` / `typecheck` / `test` / `build` | 119/119 files, 1272/1272 tests |
## Verification of the row itself
Every claim was checked against the range rather than asserted: 8 paged handlers (`GetPaged*Handler.cs`), exactly one dual-provider migration (`Add_FFmpegProfile_ReadRatePacing`), `SongMetadata.Artists` in both `ElasticSearchIndex` and `LuceneSearchIndex`, and `Scriban.Signed` 7.2.6 as the only dependency change across `Directory.Packages.props` + `.config/dotnet-tools.json`.
## Docs
`docs/ci-cd.md` is the only doc a release-notes row touches per the CLAUDE.md docs-update table. No route, endpoint, SPA convention or decision changed.
Adds the `v26.15.0` row to the release table in `docs/ci-cd.md`.
The tag goes on `736649b3b`, NOT on this commit and not on `main`'s head. Every
`Build & push image (amd64)` since `e8f80c42c` fails: that commit added
`web/src/api/completeAnnotations.guard.test.ts`, a third importer of
`virtual:etv-tracked-source-files`, without adding it to the hand-maintained
`--exclude` list in the Dockerfile's `web-build` stage — and that stage has no
git index, by construction (#887, claimed and in progress elsewhere). The guard
is behaving correctly; it refuses to fall back to a filesystem walk. Measured:
run 2459 on `736649b3b` ran the image job for 6m45s and published; run 2515 on
`cf5f42edf` died in web-build after 86s. `736649b3b` is therefore the newest
commit on `main` that can produce a release image.
Consequence recorded in the row itself: #880 (scheduling recurrence) slips to
the next release, since it merged after the break.
Release-boundary sweep (docs/ci-cd.md -> "Before cutting a release"):
- `decisions_validate.py` -> OK; 0 legacy-unmigrated records remain
- `build_decisions_catalog.py` -> no drift
- record ceiling: 45/218 over 60 lines (fraction 0.21, inside the blocking
0.02-0.25 band). The validator notes the 60 has drifted below the tail
boundary (p90=104, p95=142) and asks for re-derivation when convenient —
a maintenance signal about the constant, not a blocker for this cut.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Docs-only, single commit. Adds the
v26.15.0row to the release table indocs/ci-cd.md.The tag does NOT go on this commit
Unlike #739 (v26.14.0) and #716 (v26.13.0), the tag will be pushed onto
736649b3b, an ancestor ofmain— not onto this commit and not ontomain's head.maincannot build an image and has not been able to sincee8f80c42c:Build & push image (amd64)736649b3b(#882)e8f80c42c(#883)cf5f42edf(#897)web-builde8f80c42caddedweb/src/api/completeAnnotations.guard.test.ts, a third importer ofvirtual:etv-tracked-source-files, without adding it to the hand-maintained--excludelist in the Dockerfile'sweb-buildstage — and that stage has no git index by construction. The guard is behaving correctly: it refuses to fall back to a filesystem walk (testing.guard-derives-population-from-source). That is #887,priority: high, claimed and in progress in another session — this PR does not touch it.A
v*tag runs the same Dockerfile, so taggingmain's head would fail at the image build and produce no release.Two independent confirmations that
736649b3bis the newest valid tag point:736649b3bthe only test importing the virtual module ispageSizeCallSites.guard.test.ts, which is in the exclude list.completeAnnotations.guard.test.tsdoes not exist there (git cat-file -e→ absent).ersatztv:736649b3and none of the eight shas after it.Consequence, recorded in the row itself: #880 (scheduling recurrence — an absent array means unrestricted) merged after the break and therefore slips to the next release. The other seven excluded commits are hooks/scripts/CI/docs only, none of which ship in the image.
Release-boundary sweep
Per
docs/ci-cd.md→ "Before cutting a release":decisions_validate.pybuild_decisions_catalog.pyarchive/The validator notes the 60-line ceiling has drifted below the tail boundary it was derived from (p90=104, p95=142) and asks for re-derivation when convenient. That is a maintenance signal about the constant, not a blocker — flagging it rather than silently re-deriving it inside a release PR.
Local gate on
736649b3bdotnet build ErsatzTV.sln -c Releasedotnet test -c Release --no-builddotnet list package --vulnerable --include-transitivelint/typecheck/test/buildVerification of the row itself
Every claim was checked against the range rather than asserted: 8 paged handlers (
GetPaged*Handler.cs), exactly one dual-provider migration (Add_FFmpegProfile_ReadRatePacing),SongMetadata.Artistsin bothElasticSearchIndexandLuceneSearchIndex, andScriban.Signed7.2.6 as the only dependency change acrossDirectory.Packages.props+.config/dotnet-tools.json.Docs
docs/ci-cd.mdis the only doc a release-notes row touches per the CLAUDE.md docs-update table. No route, endpoint, SPA convention or decision changed.