8dcd4f3602652c8353a7c331ce9e7e3073faa784
3494
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8dcd4f3602 |
Merge pull request 'fix(632): bind a review verdict to its BASE branch, not only to its head sha' (#667) from fix/632-verdict-base-ref into main
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 18s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 36s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
|
||
|
|
ed8de77e10 |
fix(632): validate status ROWS, not just the top-level array — the same swallow one level down
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
PR Gates / Docs update reminder (pull_request) Successful in 19s
Review verdict / Set review-verdict status (pull_request) Successful in 6s
PR Gates / decisions lifecycle (pull_request) Successful in 28s
PR Gates / Script tests (pytest) (pull_request) Successful in 49s
review-verdict/h10 Review-verdict: MERGEABLE @ ed8de77 (base: main)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m14s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m1s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 16m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Re-review caught my previous fix claiming more than it delivered. "Every unreadable input
asks" was false: validating only that `.statuses` is an array left `{"statuses":[1]}`
passing the guard, after which `.context` on a number errors and the `|| true` on the
extraction turned that error into an empty description — straight back onto the
graceful-adoption path the guard exists to distinguish from. The identical
swallow-the-error shape I had just fixed a few lines up, surviving one level deeper.
The validation domain now matches the CONSUMPTION domain: every row must be an object
with a string `.context` and a `.description` that is absent or a string. The extraction
drops its `|| true` and asks explicitly instead, since a swallowed error there is
indistinguishable from a benign "no base recorded".
Both guards are load-bearing, for DIFFERENT shapes — established by mutating them
together and separately rather than assuming the pair was redundant:
- a non-string `.description` is caught ONLY by the row validation (jq -r renders the
object as JSON, the sed finds no `(base: …)`, and it silently reads as a legacy verdict);
- a scalar row is caught by EITHER, so with the validation weakened the extraction guard
is what still asks.
Also noted rather than changed: this is the third read of the same status endpoint in a
worst-case hook run. Sharing one snapshot would close a narrow same-run disagreement
window, but the other two branches derive different decisions from a failed read, so
threading a shared response through them changes pre-existing logic rather than #632's.
Recorded in place so it is not rediscovered as an oversight — every `decide` exits
immediately, so the reads cannot produce one self-contradictory message.
Refs #632
|
||
|
|
d51255a8ef |
fix(632): "could not check" is a third outcome, not a quiet synonym for "nothing to check"
Cold review's substantive finding. The first draft collapsed an unreadable status response into the graceful-adoption path: `vdesc` came back empty, so `recorded_base` was empty, so the comparison was skipped IN SILENCE — and a later, successful status read could then auto-grant, emitting "merge gate: satisfied" for a comparison that never happened. A transient Gitea hiccup is not evidence that the base is unchanged. The unreadable status response and a PR with no resolvable `.base.ref` now both fall through to a human `ask`, leaving exactly one benign silent case: a verdict that predates #632 and could not have carried the field. The emptiness check is done in SHELL before jq sees it, same jq-1.6 rule as the rest of this file. Also from review: the graceful-adoption test asserted only that the decision lacked the issue tag, so it would have passed for a base-specific ask or deny whose wording omitted it — the failure mode most likely to appear when someone edits these messages. It now asserts on the word "base". Recorded rather than fixed, because fixing it would be worse: docs-only PRs exit before this check, since that carve-out short-circuits the gate earlier. It does not auto-grant — it passes through to an ordinary permission prompt — so the exposure is a missing warning on a merge a human is already confirming, not a silent merge. The record now says so instead of implying the deny is unconditional. Mutation-verified: collapsing the unreadable case back into graceful adoption, skipping the check on a missing live base, and dropping the mismatch deny each redden their own test and nothing else. Refs #632 |
||
|
|
f0f8708a6e |
fix(632): fail closed when the head/base re-read itself fails
Self-review of the previous commit. Folding the head and base re-reads into one `prjson_now=$(api_get ... || true)` swallowed a guard that used to be implicit: the old `sha_now=$(api_get ... | jq ...)` aborted under `set -e` + `pipefail` when the GET failed, before any status was written. With `|| true`, both `sha_now` and `base_now` come back empty, both `[ -n ... ]` guards no-op, and the status is written having confirmed nothing about either the head or the base — a fail-open regression introduced by the refactor itself. Confirmed the old behaviour empirically rather than by reading it: a failed piped command substitution under `set -euo pipefail` exits with curl's status. The refusal is now explicit, and pinned by a test — nothing asserted it before, which is exactly why the refactor could drop it silently. Mutation-verified: restoring `|| true` reddens that test alone. Refs #632 |
||
|
|
00e623c066 |
fix(632): bind a review verdict to its BASE branch, not only to its head sha
#622 made `review-verdict/h10` a per-sha required status, so a new commit cannot inherit an old verdict — the required context is simply absent on the new head. Retargeting a PR's base reaches the same end from the opposite direction: the head sha and the status both hold still while the merge-base, and therefore the effective diff the verdict was formed against, changes underneath them. #622's record claimed the invariant holds "by construction"; this was the documented exception, and an unrecorded exception is how a guarantee degrades into a habit. `post-review-verdict.sh` now records the base branch in the status description as a trailing `(base: <ref>)`, and refuses to write a status at all if the base moved between reading the PR and posting — the same TOCTOU window the head check already covers, which the head check cannot see because retargeting does not move the head. `pretooluse-merge-consent.sh` reads the field back and denies when it no longer matches the PR's live `base.ref`. Two choices are load-bearing, and each is pinned by a test rather than left to a comment: - The comparator is `base.ref`, NOT `base.sha`. `base.sha` tracks the base branch's tip, which moves whenever anything merges to `main` — comparing it would invalidate every open verdict on every unrelated merge, converting a rare-event guard into a permanent merge deadlock. A base that merely advances is out of scope by design: rebasing onto it moves the head sha, which the per-sha binding already covers. - The field goes in the status DESCRIPTION, not the verdict comment. The comment body is parsed by `scripts/check-review-verdict.sh`, whose grammar had three false-opens in its history (#629); nothing parses the description, so this adds a field without reopening that surface. Scope is stated honestly rather than overclaimed: this is DETECTION on the hook path only. A commit status carries no base of its own, so the server-side required check cannot see a retarget, and a merge driven through the Gitea UI or API is unaffected. That is the accepted exposure — base changes are rare, manual, and this is a two-account repo — but it now fails loud in the one place that evaluates consent, instead of living only in a doc. Verdicts posted before this change carry no `(base: …)` and get NO opinion rather than a deny; denying would block every in-flight PR the day it lands, and the window closes on its own since verdicts are per-head and short-lived. Verified by mutation, six mutants, each killed by its intended test: remove the hook's deny; compare base.sha instead of base.ref; drop graceful adoption; stop recording the base; drop the TOCTOU guard; accept a PR with no resolvable base. The positive controls matter more than usual here — the test PR is deliberately non-docs (a docs-only PR short-circuits the whole gate and would never reach the base check) and the rest of the gate is unstubbed, so "the hook denied" alone proves nothing. Refs #632 Decisions-Edit: yes |
||
|
|
8103e34fff |
Merge pull request 'fix(648): an explicit jq version contract + one shared PR-file enumeration' (#658) from fix/648-649-jq-gates into main
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m41s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 16m49s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 18m37s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m11s
|
||
|
|
256cb0221b |
Merge pull request 'docs(ersatztv skill): record the #510 no-logo-no-bug policy and deco seeding recipe' (#659) from docs/510-skill-logo-bug-policy into main
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 15s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 29s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 30s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 19s
|
||
|
|
3684fd7ef6 |
Merge pull request 'docs(505): retire the stale "do NOT set QSV on jazz" rule in the ersatztv skill' (#657) from docs/505-skill-qsv-stale into main
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 24s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 27s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 15s
|
||
|
|
b99ba68b4b |
docs(ersatztv skill): record the #510 no-logo-no-bug policy and deco seeding recipe
review-verdict/h10 Review-verdict: MERGEABLE @ b99ba68
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 20s
PR Gates / Docs update reminder (pull_request) Successful in 20s
PR Gates / decisions lifecycle (pull_request) Successful in 20s
Review verdict / Set review-verdict status (pull_request) Successful in 1s
PR Gates / Script tests (pytest) (pull_request) Successful in 30s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The skill's Logo System section documented the #502/#525 chain but stopped before #510, so it described a behavior that no longer holds and omitted the one fact someone consulting it would most likely need: a channel with a watermark configured but no usable logo now renders NO on-screen bug, from every attachment point, and says so in the log. Adds, all discovered while working #510: - The unified policy plus the log strings to grep ("has no logo artwork", "no longer exists") before suspecting the ffmpeg pipeline. - That the deco path alone used to render the generated-initials nameplate and that it genuinely worked, with #652 as the revival path -- otherwise a future reader finds the nameplate in the code history and assumes it was dead. - The #653 carve-out: the song-progress overlay bypasses the resolver and is still unchecked, so "no usable logo means no bug" is a property of the selector, not of the whole app. - That /iptv/logos/gen is unauthenticated while the rest of /iptv is not, because ConditionalIptvAuthorizeFilter is a class attribute on IptvController and that route lives on ArtworkController. - The fully API-driven deco-watermark seeding recipe (watermarks -> decos/groups -> decos -> PUT decos/{id} -> PUT playouts/{id}/deco), which cost real discovery time this session, plus the reminder that branding is not testable through the troubleshooting-playback API. Docs-only leaf change to one skill file; no code, no server-state effect. Verified the YAML frontmatter still parses (name + description intact) since an unquoted ": " there silently disables a skill. refs #510 #652 #653 |
||
|
|
b255b7ffdc |
test(648): close the mutation gaps round 5 found — two tests passed for the wrong reason
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Successful in 17s
Review verdict / Set review-verdict status (pull_request) Successful in 31s
PR Gates / Script tests (pytest) (pull_request) Successful in 35s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m59s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 16m24s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 22m27s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
review-verdict/h10 Review-verdict: MERGEABLE @ b255b7f
Round 5 could not break the predicate itself: 28,930 real runs of the script across
14,465 crafted --version strings on bash 3.2.57 and 5.3.15 produced zero fail-opens, and
`{1,9}` is honoured on bash 3.2, so round 4's bound is not void on the authoring Macs.
What it did find is that two of round 4's changes were unpinned, and the tests that
looked like they covered them did not.
Reverting BOTH the first-line slice and `[[:blank:]]`→`[[:space:]]` together left the whole
suite green. The four filler cases are all killed by the SEPARATOR restriction alone, so
they attributed the fix to the wrong layer. Added three cases carrying the literal word
`version` (`jq\nversion\n9.9` and friends), which satisfy the separator rule and can only be
stopped by confining the parse to line one with a newline-free blank class.
The CR-strip test was worse: vacuous through two independent mechanisms. `str.splitlines()`
also splits on `\r`, so a per-line view dropped the stray CR; and `subprocess.run(text=True)`
translates `\r` to `\n` outright, so even a raw-string check on stdout was unfalsifiable.
The mutant demonstrably emits `... = jq-1.6<CR> (parsed 1.6; ...)` at the byte level while
the test reported green. Added `run_bytes()` and a bytes comparison.
Both gaps are now mutation-verified: reverting either change reddens exactly its own test.
Also records the operational edge this parser acquires in the follow-up: it is strictly
fail-closed by design, so once the floor mode gates the required check, a jq wrapper that
prints a banner line would deadlock merges. The fix there is to widen the accepted forms,
never to relax fail-closed.
Decisions-Edit: yes
|
||
|
|
807ebbd38e |
fix(648): round 4 — the round-1 fail-open was still reachable, via an over-long number
Round 4 found the round-1 MECHANISM alive in round 3's regex. The pattern guaranteed the
operands were digits but not that they fit `test`'s integer range, so a 23-digit major made
`[ "$major" -lt "$min_major" ]` error with "integer expression expected" — and `set -e`
exempts a failing command in an `if` condition, so the conditional read false and the floor
was never asserted. Exit 0. That is precisely what the empty string did in round 1: same
shape, third occurrence, same predicate.
Bounding the runs with {1,9} was not sufficient on its own. The pattern is unanchored at the
end, so `jq-1.99999999999999999999999` simply matched the first 9 digits of the minor and
compared THAT — a mis-parse that passes the floor rather than an error that skips it. The
trailing `([^0-9]|$)` is what actually closes it.
Second hole: `[[:space:]]` matches NEWLINES, so round 3's "anchor" still scanned the whole
output. `jq\n2.34: cannot load shared library` matched `jq`, crossed the newline as
separator, and parsed 2.34. Now the first line only, with `[[:blank:]]`.
Third: the separator class `[-[:blank:]]{1,4}` could be walked across filler —
`jq -- 2.34 (real jq-1.6)` parsed as 2.34, `jq<TAB><TAB>9.9` as 9.9. It is now one of the two
forms real jq emits: `jq-1.6` or `jq version 1.6` (a blank separator REQUIRES the literal
word `version`).
Verified across a 20-case matrix: every legitimate form still parses to the right numbers
(jq-1.6, jq version 1.6, jq-1.7.1, jq-1.6-dirty, jq-1.10 numerically, jq-1.6 (Debian 1.6-2.1),
jq-v1.6, JQ-1.6, jq-1.6.0, CRLF), and every constructed attack fails closed. Four mutations,
each reddening exactly its own tests. The real jq 1.8.2 on this machine still reports cleanly.
Also: the log line now interpolates the first line, so a multi-line --version cannot split the
single grep-able line the no-arg mode exists to emit.
None of these are reachable from a real jq build. They are recorded and fixed because the
guard's own stated invariant — never assert a floor against something it did not parse — was
still violable three rounds in, and the follow-up PR moves this exact code into the
branch-protection-required check.
|
||
|
|
4e094637c6 |
fix(648): the version parser was fail-OPEN on a jq that cannot start
Round 3, and it found that round 2's fix was a REGRESSION on the case that matters most. `raw=$(jq --version 2>&1 || true)` did two wrong things at once: folded stderr into the parse input and discarded the exit status. Combined with a pattern that matched the first <digits>.<digits> ANYWHERE, a jq broken by a glibc mismatch — which exits 127 and writes "version `GLIBC_2.34' not found" to stderr — parsed as version 2.34 and PASSED the floor. The strip-based parse this replaced failed CLOSED there. So the fix for a fail-open bug introduced a worse fail-open bug, in the one script whose entire purpose is to refuse to certify a version it did not parse. Same mechanism, second symptom: an unanchored match let a prefix outrank the real version. `2026.07.26 jq-1.6` parsed as 2026.07; a leading warning line carrying any number won too. Now: jq's exit status is captured explicitly (`$?` inside `if ! cmd` is the NEGATED status, so that needed care too), stderr is kept out of the parse, and the pattern is anchored to the leading `jq` token. Every legitimate form still parses — `jq-1.6`, `jq version 1.6`, `jq-1.7.1`, `jq-1.6-dirty`, `jq-1.6 (Debian 1.6-2.1)`, `jq-1.10` (numeric compare, so the two-digit minor is not read lexically). The tests could not have caught any of this: the shim always exited 0 and never wrote to stderr, so every case it could express was clean. It now takes stderr and an exit code, and the four new cases turn red under the exact mutation. Also: the drift guard now strips comment lines before matching. A future comment citing `pulls/$pr/files?limit=100` as an example of what not to do would otherwise have reddened script-tests — which, per this branch's own correction, blocks merges. And the record no longer over-corrects: the combined-status read is guarded by `if [ "$mwcs" != "true" ]`, so a red script-tests blocks the hook-mediated merge path, not literally every merge. Decisions-Edit: yes |
||
|
|
5e7623b8d5 |
fix(648,649): security-review round 2 — close the version-parse hole and the untested caller contract
Two real defects, and three docs claims that were simply wrong. jq-preflight.sh parsed the version by stripping around the first `-` and `.`, which assumed the format is exactly `jq-X.Y`. A build printing `jq version 1.6` left major empty; the sanity check concatenated major+minor into "6", which is non-empty and all-digits, so it PASSED. The floor comparison then ran `[ "" -lt 1 ]`, which errors — and `set -e` exempts a failing command in an `if` condition, so the conditional read false and the script exited 0 having asserted nothing, after printing a plausible "parsed" line. The silently-untested-axis failure this script exists to eliminate, reproduced inside the script itself. Now parsed by explicit regex, failing closed with a diagnosis when there is no <digits>.<digits> match. Also: `--expect` with no value exited 1 with empty output on both streams. The hook's exit-status check was pinned by nothing: mutating `if files=$(...)` into `files=$(...) || true; files_complete=yes` left the ENTIRE suite green. It survived only by redundancy — the script writes stdout once, right before exit 0, so failures also happen to yield empty stdout and `[ -n "$files" ]` catches it. Safe by accident, which is the exact criticism this branch levels at the old code. Four tests now pin it, with a stub that FAILS while emitting a docs-only list (the one case redundancy cannot absorb) plus a positive control proving the harness can see the difference. Verified: the mutation now turns exactly those tests red. Docs corrections. The record claimed the --expect pin was safe because script-tests is "advisory, not a required check" — false. The merge-consent hook reads the COMBINED status (ci.advisory-red-blocks-the-merge-gate, #598), so firing the tripwire blocks every non-docs-only merge until someone re-pins. Kept anyway, for a stated reason, but no longer described as free. The record also asserted in the present tense that review-verdict.yml checks out the base ref; it has no checkout step at all, so that is now a future-tense requirement on the follow-up. And the documented .status allow-list named GitHub's `removed`, which the code rejects. The drift-guard regex anchored on `?limit=`, so a re-inlined copy written `files?page=1&limit=50` would have walked past it. Decisions-Edit: yes |
||
|
|
2c10f057b8 |
fix(648,649): stage the enforced-gate wiring behind the scripts it calls
Splits the review-verdict.yml rewiring out of this PR. That workflow checks out the PR's BASE ref — deliberately, so a PR cannot rewrite the gate that judges it — and the base is main, which does not yet contain scripts/pr-changed-files.sh or scripts/jq-preflight.sh. Wiring it here would make the job exit 127 on its own PR and block the merge gate through the combined status, which reads red jobs as blocking. So this PR lands the scripts, their tests, the hook rewiring and the script-tests jq pin; the follow-up points review-verdict.yml at them once they exist on main. The two tests that asserted on review-verdict.yml are scoped accordingly, each carrying the reason. test_review_verdict_never_pins_a_jq_version is asserted NOW rather than in the follow-up, so the no-pin constraint on the required check is already enforced when the wiring lands. Decisions-Edit: yes |
||
|
|
63fa81fbb5 |
docs(648,649): the jq contract + the shared PR-file enumeration record
Adds docs/ci-cd.md "The jq contract" (1.6 floor, the three divergent constructs, and the deliberate pin-vs-floor asymmetry with its merge-deadlock reason), plus two decision records: ci.jq-version-contract and ci.shared-pr-file-enumeration. ci.script-tests-job stops restating the three jq rules and points at the new record. Also corrects the script-tests preflight description: it is now two steps (git presence, then jq VERSION via scripts/jq-preflight.sh --expect 1.6), not one. A literal NUL byte had crept into the ci-cd.md paragraph describing jq 1.6's NUL truncation — which git treats as a binary file. Replaced with the literal text. Decisions-Edit: yes |
||
|
|
2fd798cccf |
fix(648,649): one shared PR-file enumeration + an explicit jq version contract
#649 — the enforced review-verdict.yml guard had drifted strictly WEAKER than the advisory merge-consent hook: four rounds of #643 hardening landed on the copy whose failures produce a human prompt, and never reached the copy that writes the branch-protection-required review-verdict/h10 status. Its fail-closed behaviour on a garbage response was also incidental (an empty `n` erroring a bash conditional to false), not designed. Extract scripts/pr-changed-files.sh as the single implementation both call. Shared MECHANISM, not policy: the two docs-only allow-lists differ deliberately and stay separate. review-verdict.yml now checks out the BASE ref, never the PR head, so a PR cannot rewrite the gate that judges it. #648 — baking jq into docker/ci/Dockerfile provably cannot cover the gate that broke: review-verdict.yml is runs-on:small with no toolchain pin, so it gets the host's jq 1.6 (checked, not assumed). Add scripts/jq-preflight.sh: floor+observable everywhere, and a --expect tripwire on script-tests only — pinning the required merge check would deadlock every merge on a jq bump. Verified by mutation: six guards individually broken, each turning exactly its own test red, then restored byte-identical. fixes #648 fixes #649 |
||
|
|
e4c0db7702 |
Merge pull request 'fix(510): one watermark resolver for all four attachment points' (#655) from fix/510-watermark-resolution-policy into main
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m28s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Failing after 16m31s
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
|
||
|
|
c0376dcbce |
Merge pull request 'fix(644): stop seven SPA list loads truncating silently — one shared pager, bounded media pickers' (#656) from fix/644-spa-paging into main
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Has been cancelled
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
|
||
|
|
06e8181dee |
docs(505): retire the stale "do NOT set QSV on jazz" rule in the ersatztv skill
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 14s
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Successful in 19s
Review verdict / Set review-verdict status (pull_request) Successful in 3s
PR Gates / Script tests (pytest) (pull_request) Successful in 37s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
review-verdict/h10 Review-verdict: MERGEABLE @ 06e8181
The skill told sessions to keep jazz on HardwareAcceleration=3 (Vaapi) and "do NOT set QSV", citing the 2026-07-20 cold-start regression and the fact that one column governed both decode and encode. #498 fixed exactly that by adding QsvPreferNativeDecoder (VA-API decode + QSV encode, the Jellyfin split), and prod has run that way since. Verified live on jazz 2026-07-26: the single FFmpegProfile used by all 43 channels is HardwareAcceleration=1 (Qsv), QsvPreferNativeDecoder=1, QsvExtraHardwareFrames=64. A session following the old note would have "corrected" a working prod profile back to VAAPI. Also records the two QSV traps already paid for in code so they are not re-derived: the extra_hw_frames=0 zero-segment failure (#523/#529) and the vpp_qsv=tonemap silent no-op (#505), including that the same tonemap trap applies to Jellyfin's EnableVppTonemapping on this host. refs #505 |
||
|
|
6dbc072837 |
Merge pull request 'fix(505): tonemap QSV HDR through OpenCL — vpp_qsv=tonemap is a silent no-op' (#654) from fix/505-qsv-native-decode-tonemap into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 14s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 12s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
|
||
|
|
bc1a37ff01 |
fix(510): pin the blank-guard's is-Custom discriminator, verified by mutation
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
PR Gates / Docs update reminder (pull_request) Successful in 16s
Review verdict / Set review-verdict status (pull_request) Successful in 4s
PR Gates / decisions lifecycle (pull_request) Successful in 24s
PR Gates / Script tests (pytest) (pull_request) Successful in 35s
review-verdict/h10 Review-verdict: MERGEABLE @ bc1a37f
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m32s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m57s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 16m17s
Round-4 review found the fall-through tests, while now falsifiable, still did
not pin the whole guard. Two gaps, both closed and both verified by running the
mutation rather than by asserting the test would catch it:
1. All three fall-through tests used only " ", so narrowing
IsNullOrWhiteSpace to `image == " "` would have passed while breaking
fall-through for null and "" -- and null is the form the API actually
persists. Parameterized over null, "" and " ".
2. Nothing pinned the guard's `ImageSource is Custom` clause. This is the
sharper of the two: a ChannelLogo watermark's Image is NORMALLY blank
(the API writes Image = null for every non-Custom source), so dropping the
discriminator would send every playout-item ChannelLogo watermark down the
fall-through path instead of resolving the channel's own logo -- with a
fully green suite. Added
Blank_Image_ChannelLogo_Playout_Item_Watermark_Should_Win_And_Not_Fall_Through,
which distinguishes the two levels by watermark Id so a fall-through is
observable even though both resolve to the same cached path.
Mutation results (each mutation applied on its own, then reverted):
drop `is Custom` from the guard -> 1 failure, and the new test is the ONLY
test that catches it
IsNullOrWhiteSpace -> == " " -> the null and "" parameterized cases fail
Negative control re-measured on the final 32-case fixture: 19 fail against the
origin/main resolver. The 13 that pass both ways pin deliberately preserved
behavior plus the positive control, which the record now states explicitly
along with the mutation table.
This round's lesson, recorded in the record: a test's NAME is not evidence it
pins what it claims, and a whole-file revert cannot show that a test aimed at a
specific clause actually reaches that clause -- only mutating the clause can.
Gates: 2661 tests green across 4 projects, 225/225 script tests (the gate I
skipped before the last push), decisions-validate OK, format exit 0, no BOMs.
refs #510
|
||
|
|
d189d17157 |
fix(510): quote the record's rule scalar — unquoted " #653" was a YAML comment
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 19s
PR Gates / Docs update reminder (pull_request) Successful in 22s
review-verdict/h10 Awaiting review verdict for d189d17
PR Gates / decisions lifecycle (pull_request) Successful in 24s
Review verdict / Set review-verdict status (pull_request) Successful in 4s
PR Gates / Script tests (pytest) (pull_request) Successful in 33s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 18s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m37s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 20s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 16m50s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m59s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
CI's script-tests job (test_frontmatter_reader_matches_pyyaml_on_every_real_record) caught that the dependency-free hand parser and PyYAML disagreed on this record's `rule:` field. Cause: `rule:` was an unquoted plain YAML scalar containing " #653)", and in a plain scalar " #" starts a comment. PyYAML therefore truncated the value at "...(the song-progress overlay," and silently dropped the rest of the clause -- including the scope carve-out naming #653, which is the whole point of that sentence. The generated catalog row was built from the truncated value, so the active-decisions view was quietly missing it. Same trap as fix(631) (a record TITLE whose unquoted " #" was a comment). The `signals:` and `mechanics:` fields were already single-quoted, which is why only `rule:` diverged; `title:` is safe because its "(#510)" has no space before the "#". Single-quoted `rule:` and regenerated the catalog, which now carries the full sentence. Worth recording: I ran scripts/decisions_validate.py locally but NOT scripts/tests, so the gate that catches exactly this class was the one local check I skipped. Touching the decisions corpus means running the pytest suite, not just the validator -- the validator reads through the hand parser and so cannot see a hand-parser-vs-PyYAML divergence by construction. Local: 225/225 script tests pass, decisions-validate OK. refs #510 |
||
|
|
daedf003e5 |
fix(644): round-3 review — split truncated/incomplete picker hints, F2 out-of-list gaps, F3 abort warns, F4 aria wiring, F5 FillerPresetsScreen tests
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 14s
PR Gates / Docs update reminder (pull_request) Successful in 24s
PR Gates / decisions lifecycle (pull_request) Successful in 28s
Review verdict / Set review-verdict status (pull_request) Successful in 6s
PR Gates / Script tests (pytest) (pull_request) Successful in 33s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m17s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m19s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 16m24s
review-verdict/h10 Review-verdict: MERGEABLE @ daedf00
Addresses the round-3 cold re-review's five low-severity findings on #644's client-side paging fix: - F1: `loadPickerOptions` (RerunCollectionsScreen, PlaylistsScreen) returned one `truncated: boolean` for two different conditions — a real Class B cap hit vs an unconverged Class A `loadAllPages` load — so an incomplete multi-collection load rendered the self-contradictory "Showing the first 47 of 47 — use search to narrow." Replaced with a `hint: 'incomplete' | 'none' | 'truncated'` discriminator and distinct copy per value; 'incomplete' matches the wording already used by the Class A list-load warn Badge. - F2: mirrored the out-of-list current-selection injection (RerunCollectionsScreen/PlaylistsScreen's `selectedInList` prepend) into FillerPresetsScreen and ScheduleItemInspector's rerun-collection picker, so an id outside the loaded page still renders as selected instead of misrepresenting the stored value as "(none)". - F3: gated the `console.warn` on an incomplete Class A load with `!signal?.aborted` in the `multi` branches (RerunCollectionsScreen, PlaylistsScreen) and SchedulesScreen.loadAllRerunCollections, so a superseded/aborted load (Retry, or a type switch mid-load) no longer logs a false warning. - F4: added `Select`'s `ariaDescribedBy` prop and wired the truncation/incomplete hint span to it via `useId()` in RerunCollectionsScreen and PlaylistsScreen, so screen readers announce the hint (FillerPresetsScreen already routed it through `Row help=`). - F5: added FillerPresetsScreen.test.tsx (previously untested) covering the Class B single-request guarantee, the truncation hint's totalCount>100/<=100 boundary, and the F2 injection; added the two assertions the re-review found missing anywhere in the suite — the Class A `incomplete` warn Badge actually rendering, and a screen-level seqRef stale-overwrite race — to RerunCollectionsScreen.test.tsx. Updates docs/spa-conventions.md §3b and the spa.list-completeness-vs-bounded-pickers decision record to describe the hint discriminator. Decisions-Edit: yes |
||
|
|
edf8be4b5e |
fix(510): re-review round — make two review-added tests actually falsifiable
PR Gates / Docs update reminder (pull_request) Successful in 18s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 18s
review-verdict/h10 Awaiting review verdict for edf8be4
PR Gates / decisions lifecycle (pull_request) Successful in 20s
Review verdict / Set review-verdict status (pull_request) Successful in 3s
PR Gates / Script tests (pytest) (pull_request) Failing after 38s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 16m40s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 17m20s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Re-review of the previous fix commit found that two tests added to close round-1 findings could not fail. Both verified before fixing: 1. Missing_But_Named_Custom_Playout_Item_Watermark_Should_Not_Fall_Through gave the channel-level fallback the SAME missing custom path as the playout-item watermark, so a wrongly-widened guard would have fallen through to a fallback that also resolved to None -- the assertion held either way. The fallback is now an independently resolvable ChannelLogo whose cached file exists, so a fall-through returns it and fails the test. Added the matching positive control (blank -> falls through and DOES return that logo), so the pair shows the guard distinguishes blank from unresolvable instead of both landing on None. 2. Deco_With_One_Valid_And_One_Missing_Watermark... asserted a filtered list length while the routing claim the decision record cited it for lives in FFmpegLibraryProcessService.CanUseFFmpegNativeWatermark, which the test never called. It now calls the real predicate. Also, three wrong claims of my own: 3. The Resource arm comment said "nothing in the app writes a Resource watermark to the database". False -- CreateWatermarkHandler and UpdateWatermarkHandler persist whatever ImageSource the request names, so a Resource watermark IS creatable through the API, always with Image = null. That is precisely why the new null guard is load-bearing, so the comment was arguing for its own removal. 4. "One resolver and no per-caller policy" contradicted the surviving playout-item blank-Custom fall-through documented a few lines later. Reworded in both the record and the XML docs: one resolver, and exactly one piece of per-caller policy which lives in the CALLER. 5. The record's "12 of 18 new tests fail pre-fix" was stale. Re-measured against the final fixture: 19 of 29. The other 10 pass both ways by design because they pin preserved behavior, which the record now says explicitly rather than leaving the gap to be read as weakness. Removed the vacuous generated-URL test rather than keeping it with an honest comment -- an empty list trivially contains no URL, so it implied coverage it never had. Its assertion is folded into the sibling test that has a real arrangement. Gates: 2772 tests green across 5 projects, dotnet format exit 0, no BOMs, decisions-validate OK, live-E2E re-run against this binary (0 changed pixels, nameplate absent, warning emitted). refs #510 |
||
|
|
ee66cb7459 |
fix(505): address cold-review findings — retag on tonemap, subtitle scale, anamorphic
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 20s
PR Gates / Docs update reminder (pull_request) Successful in 22s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 24s
PR Gates / decisions lifecycle (pull_request) Successful in 30s
Review verdict / Set review-verdict status (pull_request) Successful in 9s
PR Gates / Script tests (pytest) (pull_request) Successful in 42s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m29s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 17m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 21m3s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 22m48s
review-verdict/h10 Review-verdict: MERGEABLE @ ee66cb7
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Independent cold review (Codex, no implementation role) found no Blocker and three real defects, all fixed here: HIGH — HDR was re-tagged bt709 only when the profile had NormalizeColors on. The colorspace filter sat behind desiredState.ColorsAreBt709, so an operator with normalization disabled got tonemapped SDR pixels still tagged bt2020 and the player converted them a second time. The guard is now "tonemapped || (ColorsAreBt709 && ...)". Deliberately NOT fixed by hoisting usesVppQsv out of the guard, which would force bt709 on scale-only non-HDR chains that legitimately opted out. MEDIUM — image subtitles stopped being resized. The subtitle canvas is scaled only when the video chain contains a recognized scale filter, and that predicate listed the QSV filters only; swapping ScaleQsvFilter for ScaleVaapiFilter left a 4K HDR + PGS source with a 720p video and a source-size subtitle overlay. VaapiPipelineBuilder already listed ScaleVaapiFilter; QsvPipelineBuilder does now. MEDIUM — anamorphic HDR now falls back to the software tonemap. ScaleQsvFilter is handed the SAR VideoStream calculates (it has a 0:0 fallback); ScaleVaapiFilter multiplies by ffmpeg's runtime `sar`, which differs when the decoded frame leaves SAR unspecified. Rather than ship a graph nobody has run, exclude anamorphic -- which leaves those sources exactly where they were before this change. LOW — tests now pin the exact validated graph as an ordered substring (the prior assertions would have passed with setFormat off, hwdownload dropped, or the wrong tonemap output format), assert against the vpp_qsv OPTION rather than a bare "tonemap=1" substring, and cover NormalizeColors=false, anamorphic and image subtitles. Each of the three fixes was negative-controlled: reverting it fails exactly one test, and no others. The remaining LOW (deriveDevice's defaulted bool is a future-call-site trap) is recorded as an accepted residual rather than fixed, since a named factory would push this diff into the VA-API pipeline for no behavior change. The record is 86 prose lines, over the 60-line ceiling. Declining to cut: every bullet is a distinct measured finding, which docs.corpus-size-signal names as a legitimate decline. Decisions-Edit: yes |
||
|
|
1a7f15fb27 |
fix(510): address independent review — Resource null guard, honest routing claim
Two independent reviews (cross-family Codex + cold Opus) both returned BLOCKED. Findings, all verified against source before acting: 1. Resource arm could throw ArgumentNullException (Codex, Medium). Making the channel/global Resource arm reachable exposed that CreateWatermarkHandler and UpdateWatermarkHandler write `Image = null` for EVERY non-Custom watermark, so an API-created Resource watermark reached Path.Combine(folder, null). Added the blank/null guard the arm never had. This was live at the playout-item level too, not just newly-reachable code. 2. "Routing is unaffected" was false (Codex, Low but sharp). The predicate is unchanged, but CanUseFFmpegNativeWatermark also tests Count == 1, and dropping an unresolvable watermark shortens the list. A deco with one valid and one missing permanent watermark now routes ffmpeg-native where it previously routed to the graphics engine. Intended, but observable -- so it is documented and pinned by a test rather than claimed away. 3. "Exactly one resolver" over-claimed (Opus, High). True of the selector, not the application: the song-progress overlay is built as a WatermarkOptions directly by the streaming and troubleshooting handlers, unchecked, and can still hand ffmpeg a nonexistent -i. Pre-existing; scoped the claim in the record and channels.md and filed #653. 4. Undeclared crash->degrade change (Opus, Medium). Channel/global Custom had no blank-image guard, so a cleared image hit ImageCache's fileName[..2] and threw out of stream startup. Now declared in the record and tested. 5. Contradictory rule text (Opus, Medium) -- the catalog one-liner said "always no bug" while the body documents the playout-item fall-through exception. Qualified; catalog regenerated. 6. History was wrong in both the record and the XML docs: the three precedence levels did NOT all check every source -- channel/global had no Resource arm and threw. Corrected. Tests: 30 in the fixture now (was 18). New coverage for the preserved blank-Custom fall-through (to channel AND to global), the complement case (missing-but-named must NOT fall through), null/blank Resource, and the valid+missing routing case. 17 of 24 failed against the pre-fix resolver before this round; the fixture stays mutation-sensitive. Also: hoisted the mock-filesystem Initialize() out of its loop so a multi-file case cannot silently seed only the last file, and marked the generated-URL test honestly as redundant-by-construction rather than claiming independent coverage. The decision record is now 81 prose lines, over the 60-line ceiling. Declared as a legitimate decline per docs.corpus-size-signal: the length is the review findings above, each a distinct fact, not redundancy. refs #510 #652 #653 |
||
|
|
94182cdd53 |
fix(644): split loadAllPages by list class; bound media-library pickers to one page
Cold adversarial review of
|
||
|
|
18c4f4e0b2 |
docs(505): correct the #498 record's escape-hatch claim, which was wrong the dangerous way
The qsv-decode-encode-split record told HDR-on-QSV users to set QsvPreferNativeDecoder OFF "to keep GPU tonemap". Measured on the Intel host, vpp_qsv=tonemap=1 is a silent no-op on pre-Gen11 graphics, so following that advice did not preserve GPU tonemapping -- it disabled tonemapping entirely and shipped untonemapped HDR. Prose correction only; the decision itself (the QsvPreferNativeDecoder toggle) is unchanged and stays active. The software-tonemap half of the bullet was always accurate and stands. Decisions-Edit: yes |
||
|
|
7c075ffa70 |
fix(505): gate the OpenCL tonemap on the ACTUAL frame location, not an enumeration
UseOpenClTonemap excluded the two known ways frames reach a hardware surface before the tonemap (the QSV decoder, deinterlace_qsv). That is only correct for as long as the enumeration stays complete -- and the failure mode if it ever stops being complete is a second hwupload stacked on frames that are already on a surface. Check currentState.FrameDataLocation directly instead. The route begins with hwupload, so "frames are in software" is the actual precondition; the specific exclusions stay as documentation of the known cases, but the state check is what makes it safe. A future filter landing ahead of the tonemap now degrades to the software tonemap rather than emitting a broken graph. |
||
|
|
41e2870113 |
fix(505): tonemap QSV HDR through OpenCL; vpp_qsv=tonemap is a silent no-op
#505 asked to route the #498 native-decode path through TonemapQsvFilter to move HDR tonemapping off the CPU. Measured on the Intel host (jazz: FFmpeg 8.1.2 / iHD 25.1.4 / UHD 630) against real HDR HEVC Main10, that filter is a SILENT no-op: a graph ending in vpp_qsv=tonemap=1 returns a frame byte-identical (same md5) to the same graph with no tonemap at all, with no warning and no error. QSV VPP tonemapping needs Gen11+; pre-Gen11 iHD ignores it. So the issue's premise was inverted, and the branch it wanted to extend was already broken: the existing DecoderHardwareAccelerationMode == Qsv path shipped untonemapped HDR whenever QsvPreferNativeDecoder was off -- which is exactly the escape hatch #498/#523 recommend. Prod was unaffected (native-decode is the default and took the working software branch). Tonemap on the GPU via OpenCL instead, the route VaapiPipelineBuilder already uses and the one Jellyfin uses. The scale has to run first, in scale_vaapi: tonemapping full-size is slower than the software path it replaces (15.5s wall for 12.5s of content, below realtime), while scale-first cuts total CPU ~60% (35.6s -> 14.1s) and lands at the no-tonemap wall-clock floor. A QSV surface maps to neither OpenCL nor VA-API, so the gate requires software frames: the QSV decoder and deinterlace_qsv both fall back to the software tonemap, slower but correct. TonemapQsvFilter is deleted -- a filter that silently does nothing is worse than no filter. Also fixes output tagging: the first end-to-end run was correctly tonemapped yet still announced bt2020 primaries, because SetPixelFormat's usesVppQsv predicate ("did a hardware filter strip color info") listed only the QSV filters. Both new filters are now in it. Validated end to end on jazz with the exact generated command: exit 0, YAVG 26.39 (software reference 26.6, untonemapped 44.3), and ffprobe reports bt709 space/transfer/primaries. fixes #505 |
||
|
|
9cbe70e486 |
fix(510): one watermark resolver for all four attachment points
WatermarkSelector resolved watermarks in two places with two policies. The three precedence levels (playout item, channel, global) existence-checked every image source and degraded to None; the deco path had its own copy of the same switch that returned whatever path it computed, unchecked. So one channel could disagree with itself about whether an on-screen bug rendered, based only on how the watermark was attached. #502 deferred this here but scoped it to ChannelLogo. It was never ChannelLogo-only: the deco path skipped the existence check for Custom and Resource too. Extract one ResolveWatermark used by all four sites. Severity is not cosmetic. A dead LOCAL path is not harmlessly skipped -- CanUseFFmpegNativeWatermark hands a single permanent watermark to ffmpeg as a bare -i argument and excludes only URLs, so the deco path could hand ffmpeg a nonexistent input file. The generated-initials nameplate was real: a live-E2E on a real transcoded frame confirmed it composited via the deco path (/iptv/logos/gen is on ArtworkController, which has no auth filter, so the container-internal self-fetch succeeded). The #502-era comment claiming "it has never rendered here" was wrong, and the new record says so. It is still removed: serving it means an HTTP fetch inside stream startup, which graphics.channel-logo-caching (#525) eliminated for logos, and it depends on #1's hardcoded localhost. Reviving it by caching the image instead is #652. Measured blast radius on prod: 0 Deco rows, 0 DecoWatermark rows, all 43 channels have logo artwork -- no rendered output changes. Preserved deliberately: a playout-item Custom watermark with a blank image still falls THROUGH to the channel/global watermark; unifying resolution must not change which watermark wins. Routing is untouched. Strict improvement: the channel and global arms previously threw NotSupportedException on a Resource watermark; they now resolve it. The default arm still throws so a new image source fails loudly. Tests: 18 new cases including a positive control and 8 deco-vs-channel parity cases. 12 of the 18 fail against the pre-fix resolver, which is what proves they are load-bearing rather than vacuous. fixes #510 |
||
|
|
fe342a6a0b |
fix(644): page SPA list loaders to completeness instead of inflating pageSize
Seven call sites (rerun-collections, multi-collections, library/browse) requested pageSize far above each endpoint's server-side MaxPageSize=100 clamp and took the single response page as the whole list, so rows past 100 silently vanished with no error or truncation indicator. Extract the loadAllRerunCollections pattern from SchedulesScreen (#634) into a shared, generic web/src/api/paging.ts::loadAllPages helper that pages against totalCount with an empty-page defensive break, and refactor SchedulesScreen plus the seven over-cap call sites in RerunCollectionsScreen, MultiCollectionsScreen, PlaylistsScreen, and FillerPresetsScreen to use it. Server caps are unchanged (api.search-allitems-paging precedent: client pages, server stays bounded). Document the convention in docs/spa-conventions.md §3b. |
||
|
|
34591c3ef6 |
Merge pull request 'fix(620): signal corpus size per RECORD; the aggregate becomes an untresholded trend' (#642) from fix/620-corpus-budget into main
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 16s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 28s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 31s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m25s
|
||
|
|
fefd11dffe |
fix(620): signal corpus size per RECORD; the aggregate becomes an unthresholded trend
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 20s
PR Gates / Docs update reminder (pull_request) Successful in 28s
PR Gates / decisions lifecycle (pull_request) Successful in 30s
Review verdict / Set review-verdict status (pull_request) Successful in 12s
PR Gates / Script tests (pytest) (pull_request) Successful in 42s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m31s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 18m33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 21m27s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 22m24s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
review-verdict/h10 Review-verdict: MERGEABLE @ fefd11d
Squashed from 7 commits (4 review rounds) to keep the rebase onto #621 tractable; the full round-by-round history is on PR #642. corpus was 5658/5600 — over budget and warning again — 3h35m after #619 put it at 5228, with nobody consolidating anything. So this does NOT re-baseline. An aggregate over a monotonically growing corpus can only ratchet; that is the "permanently red, therefore no signal" state #542 re-baselined away from, and growth is not even a smooth rate to plan against (the corpus FELL from 5089 to 5042 across four days, then gained 427 in one evening as two large records landed). Replaces it with a per-record prose ceiling (default 60), non-blocking, naming each record over it — not monotonic, so it can go red AND green, and it points at a file. The aggregate is still printed as an unthresholded trend notice, with record prose and non-record scaffolding reported separately because they are not the same unit. The GENERATED catalog is no longer counted at all: it gains one row per record and no consolidation can shrink it, which made the metric partly a record COUNT in a line-count costume. The calibration test took FOUR versions, and the failures are the durable lesson: v1 true by construction (`max(under) <= 60 < min(over)` over lists built by that test) v2 a gap WIDTH — a ceiling of 200 also sits in a wide gap, so it passed v3 fraction band + "clear air" vs the nearest record above — hostage to an unrelated record: one ordinary 62-line addition reddened it with the ceiling correctly placed, and the only remedy was to RAISE the ceiling. That is this very treadmill, as a hard failure in what #631 makes a blocking job. v4 `p90 <= ceiling <= p95` — the property stated directly and scale-free. Two rules recorded: a guard test must depend only on the thing it guards, and a threshold over a growing population must be expressed in that population's own terms. Candidates: all over-ceiling records assessed, each actioned or declined with a reason. The largest (scan.libraryfolder-unique-identity, 230 lines) is a legitimate DECLINE — a dozen-odd distinct traps whose only copy that is. Nothing pruned, so no archive or supersession was required. An automated redundancy metric is explicitly rejected. Also: `--budget` is accepted but announces its retirement rather than no-opping silently; the dead `budget_ok` parameter is gone; and five "untresholded" typos are fixed, one of which was propagating into the generated catalog row and MemPalace's per-key drawer. Refs #620 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
59558e134f |
Merge pull request 'fix(631): run scripts/tests in CI as its own job, not inside the flake-covered decisions-guard' (#637) from fix/631-script-tests-ci into main
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 20s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 35s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 6m51s
|
||
|
|
37fd30dce7 |
fix(631): quote a record title whose unquoted # was a YAML comment
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 19s
PR Gates / Docs update reminder (pull_request) Successful in 20s
PR Gates / decisions lifecycle (pull_request) Successful in 20s
Review verdict / Set review-verdict status (pull_request) Successful in 3s
PR Gates / Script tests (pytest) (pull_request) Successful in 37s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m29s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 20s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m41s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 15m57s
review-verdict/h10 Review-verdict: MERGEABLE @ 37fd30d
Third latent defect surfaced purely by running scripts/tests in CI — and this one was already red on origin/main, where test_frontmatter_reader_matches_pyyaml_on_every_real_record fails. `api/paging-zero-based.md` carried an UNQUOTED plain scalar title ending "... (#616, #633)". In YAML a space followed by `#` starts a COMMENT, so PyYAML reads the title as truncated at "(#616," while the hand-written dependency-free reader keeps the whole line. PyYAML is correct per spec; the record's title was genuinely losing "#633)" for any spec-compliant consumer, which includes MemPalace's ingest. Fixed by quoting the scalar; both parsers now agree byte-for-byte. Asked "instance or class?" before fixing: swept every frontmatter field of all 179 records under both parsers and compared. This is the ONLY divergence in the corpus, so a one-line fix is the right scope — no lint rule needed, and #621's structural guard already makes a parse FAILURE loud (this was a parse-to-different, which no structural check can see). 216 passed under jq 1.8.2 and jq 1.6. Refs #631 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bb1809fbf0 |
fix(647): the jq-1.6 fail-open was ALSO in review-verdict.yml — the enforced gate
Round-4 review caught the miss that matters most: #643 fixed the `jq -e`-on-empty fail-open in .claude/hooks/pretooluse-merge-consent.sh, but the SAME construct sits in .gitea/workflows/review-verdict.yml — and that is the copy that runs on the CI runner, where jq is 1.6, and that feeds the branch-protection-required review-verdict/h10 status. Reproduced: `printf "" | jq -e '.statuses | type == "array"'` exits 4 on jq 1.8.2 (guard fires, correct) and 0 on jq 1.6 (guard passes). So on a transient API error `statusjson` is empty, the guard lets it through, `existing` reads "", and the job posts `pending` — or for a bot/docs-only PR an exemption `success` — over a possibly-existing human verdict. That is precisely what the comment three lines above it says must never happen. The hook version was harmless in practice because it runs on a dev Mac with jq 1.8. This one is live. Fixed identically, with a comment naming why the sibling fix missed it, and the same hardening applied to the changed-files read in the same workflow. Also from round 4: - LOW, reproduced — an ARRAY-valued `.status` dodged the closed allow-list. `index` is polymorphic: with an array argument it does SUBSEQUENCE matching, so `[...,"renamed",...] | index(["renamed"])` is truthy while `.status == "renamed"` is false — the row passed the allow-list AND skipped the `previous_filename REQUIRED` clause. The same `git mv code -> docs/` dodge the closed set exists to block, one type away. Now requires `.status` to be a string first; mutation-verified. - The record now carries all THREE jq rules rather than the one, and notes that the durable fix is pinning/preflighting the runner's jq version rather than patching constructs one at a time (tracked on #647). 200 tests pass under BOTH jq 1.8.2 and jq 1.6. Refs #647, #643, #631 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7265fba36d |
fix(647): the H10 verdict classifier was inert on jq 1.6 — the runner's version
Turning on the scripts/tests suite in CI immediately paid for itself: measured on origin/main, 61 of 178 tests FAIL under jq 1.6, which is what the CI runner ships. They pass on a dev Mac's jq 1.8.2, which is why this was invisible — and the suite has never run anywhere else, which is exactly #631's thesis. Two defects in scripts/check-review-verdict.sh (from #629, the single source of truth for H10 verdict classification): 1. `contains("<NUL>")` is TRUE FOR EVERY STRING on jq 1.6 — the escape truncates the literal to the empty string, and every string contains "". So the body guard errored "NUL in body" on every comment and the H10 grammar was entirely inert on the runner. Verified against both binaries: 1.6 says true for "hello", 1.7+ says false. Replaced with `(explode | index(0)) != null`, which involves no regex engine and agrees on both. 2. A parse error was indistinguishable from "no output". The script used jq's exit code to separate malformed input from a legitimately empty comment list, treating 4 as benign — but jq >= 1.7 exits 5 on a parse error while 1.6 exits 4, the same code both use for "filter produced no output". On 1.6 a garbage API response therefore returned `absent` instead of an input error. Fixed with an explicit `jq empty` pre-check, which is non-zero iff the input does not parse regardless of output volume. Severity: fail-closed, not exploitable. The classifier is only invoked from the merge-consent hook, which runs on the dev machine (jq 1.8.2), so the live gate is unaffected. The cost is that #629's hardening was inert on the runner and would have stayed invisible. 198 tests now pass under BOTH jq 1.8.2 and jq 1.6 (was 138/60 split under 1.6). This is the third distinct jq-1.6 divergence found in this codebase today (the first was #643's `jq -e` on empty input). The rule: a shell gate's behaviour is a function of its interpreter's version — test against the version CI actually runs, or pin it. Refs #647, #631 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f4473926d4 |
fix(643): close the status-string dodge, pin the allow-list anchors, fix two doc claims
Round-3 review (MERGEABLE, all findings Low/Nit) — applied anyway, because each is the
over-claim or unpinned-anchor family this PR keeps hitting.
LOW — the `renamed => previous_filename REQUIRED` clause matched `.status` by exact
lowercase string, so any other value took the `else true` branch: a row with
`"Renamed"`, or with no status at all, validated fine and silently dropped its SOURCE
path, letting `git mv ErsatzTV/Program.cs -> docs/a.md` read as docs-only. `.status` is
now checked against a closed set and an unknown value fails closed.
Two things that fix taught me, both caught by my own positive control rather than by
review:
1. The first predicate was WRONG in a way that gated everything: inside
`[...] | index(.status)`, jq's `.` is the ARRAY, so `.status` was null and every
row failed. `$s` is now bound from the row before the context switches. A
security check that rejects everything looks identical to a working one from the
failing side — only test_gitea_real_status_values_are_accepted caught it.
2. The set includes BOTH `changed` and `modified`. Live Gitea 1.25.4 emits `changed`,
but a closed allow-list built from the wrong vocabulary is worse than the hole it
closes: it would gate every genuine docs-only PR. The property wanted is "reject
what we don't recognise", not "enumerate one version exactly".
LOW — three allow-list anchors had no test at all: dropping `^` from the `docs/`
alternative (`ErsatzTV/docs/Program.cs` would exempt), dropping `$` from `.md`
(`x.md.cs` would exempt), and dropping the non-empty-list guard. Since the round-3 `..`
finding WAS an anchor subversion, they are now pinned; all three mutation-verified.
NIT — docs/ci-cd.md called this job "a checkout plus a pure-stdlib pytest run", which
the same file contradicts 450 lines later and which this PR's own record names as the
bug that turned the job red on its first CI run. Also replaced a frozen "111 tests" with
an explicitly indicative figure — the suite is ~190 after rebasing onto main, and a
frozen count has rotted four times this session.
198 passed under both jq 1.8.2 and jq 1.6.
Refs #643, #631
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
54c875414c |
fix(643): validate every field the extraction consumes; fix a vacuous test
Re-review of the previous fix commit found it incomplete and its test vacuous. Both reproduced before fixing. MEDIUM — the validation domain did not match the CONSUMPTION domain. `chunk` emits `(.previous_filename // empty)` for EVERY row regardless of status, but the guard validated that field only when `.status == "renamed"`. So a row marked `modified` — or Gitea's distinct `copied` — carrying a newline in previous_filename was still exempted. Verified EXEMPT for both statuses against the previous commit. Now validated whenever present, with the `renamed` => REQUIRED clause kept on top. MEDIUM — test_newline_in_previous_filename_is_also_rejected asserted the right outcome without exercising the mechanism: its payload's second segment was `ErsatzTV/Program.cs`, which the allow-list rejects on its own, so the test passed with the newline guard entirely removed. That is why the hole above went unnoticed — the same filter-hides-the-defect trap the guard itself is about. Payload changed to a segment that PASSES the allow-list, so the test now discriminates, plus parametrized regressions for modified/copied/added. LOW — `..` components rejected. The allow-list anchors `^docs/`, so `docs/../ErsatzTV/Program.cs` matched it (reproduced). Git will not produce such a path, but this guard exists to fail closed on unexpected 2xx shapes. Added a positive control (a legitimate docs->docs rename still exempts) so the tightened row schema cannot be satisfied by never exempting anything. Severity calibrated in the record: the docs-only exemption ends in `decide allow ""`, a passthrough to the normal permission prompt, NOT an auto-grant. Every bypass here downgrades a mechanical deny/ask to a human prompt; none can silently self-merge. Real, worth fixing, but not what an earlier framing of #643 implied. All mutation-verified; 123 passed under BOTH jq 1.8.2 and jq 1.6. Refs #643, #631 |
||
|
|
c046add10a |
fix(643): close two more fail-opens in the docs-only enumeration, found by cold review
An independent cross-family review of the jq-1.6 fix found two further ways the docs-only exemption can fire over an incomplete file list — both reachable with NO transport error, so neither had anything to do with the original bug. 1. HIGH — a path containing a newline. `chunk` flattens paths into newline-delimited text before the allow-list grep, so a filename of "safe.md\ndocs/Program.cs" splits into two lines that BOTH match the allow-list, while the real single path ends in .cs. Git permits newlines in filenames and the reviewer reproduced the bypass against this hook. Now rejected outright at the row-schema guard, on both `filename` and `previous_filename`: no docs path contains a control character, so failing closed costs nothing. 2. HIGH — a short page read as the last page. `n < 50` assumed the server's page size is the 50 we requested, but Gitea caps `limit` at the server-wide MAX_RESPONSE_ITEMS (default 50, configurable) and may return fewer. A 30-row docs page followed by a page of code completed the enumeration over a PARTIAL list. Only a validated EMPTY page may now terminate it; the page<=40 cap still fails closed, and the cost is one extra request. 3. MEDIUM — the enumeration was not bound to one head. Paging is several round-trips, so a force-push between them assembles a list belonging to no single commit: page 1 from head A plus a short docs tail from head B, with B's code page never read. The head sha is re-read after enumeration and the exemption refused if it moved. All three mutation-verified: reverting each fix reddens exactly its own test and nothing else. A positive control (short page then empty page) pins that the stricter terminator still exempts a genuinely docs-only PR, so "never terminate early" cannot be satisfied by never exempting anything. 118 passed under BOTH jq 1.8.2 and jq 1.6. The record now states the generalisable lesson: every defect here was an exhaustiveness failure in an enumeration whose completeness is load-bearing. When a security decision depends on having seen ALL of something, the termination condition must be positive and explicit, never inferred from a proxy. Refs #643, #631 |
||
|
|
5f068a2488 |
fix(631): close a fail-open in the merge-consent gate that only CI could see
Running scripts/tests in CI for the first time turned up a real defect in the gate itself, not just in the harness. `jq -e` over EMPTY input exits 4 on jq >= 1.7 but 0 on jq 1.6 — verified against both binaries, not inferred. The docs-only pagination guard in pretooluse-merge-consent.sh leaned on that exit status to reject a transport failure. On jq 1.6, which the CI runner ships: page 2 errors -> gq returns empty -> jq guard wrongly PASSES -> n is empty so [ "$n" -lt 50 ] errors into false -> the loop walks PAST the failed page -> page 3 legitimately returns [] -> files_complete=yes over a PARTIAL list -> the docs-only exemption fires over unread pages that may be pure code. That is the very defect the guard's own comment describes, reintroduced one layer down by a jq version difference. Fixed by rejecting an empty body explicitly rather than inferring it from jq's exit status. The same hardening is applied to the review-verdict status read, which fell through to `vstate=""` -> deny (fail-CLOSED, so never a hole) but would have surfaced the wrong message. Why it survived: the existing transport-failure test asserts the right thing but can only observe the bug where jq is 1.6, so it passes on a developer Mac with the bug fully present — and the suite had never run anywhere else. The new test removes that dependency by shimming ONLY jq 1.6's empty-input exit status, so it pins the property on any host. Mutation-verified: revert the fix and it goes red on jq 1.8.2. The shim is deliberately narrow (no `-n`): a broader first version swallowed the `jq -n` calls `decide` uses to build its JSON, so the hook emitted nothing and every decision read as a passthrough — the verifier manufacturing the exemption it was meant to disprove. test_jq16_shim_actually_reproduces_the_quirk now pins the shim itself, `-n` case included. Suite: 113 passed under jq 1.8.2 AND under jq 1.6. Refs #631 |
||
|
|
73577f484f |
ci(631): install pyyaml too — the suite is not pure-stdlib, and CI proved it
First CI run went red on `ModuleNotFoundError: No module named 'yaml'` at collection: test_migration_equivalence.py imports scripts/migrate_decisions_split.py, which uses PyYAML by design. It passed locally only because this machine already had PyYAML. The original claim came from reading decisions_lib/decisions_validate/build_catalog and the test files themselves — which is precisely the set that does NOT import yaml. Replaced with an AST import scan over all of scripts/, which gives the complete third-party set: pytest and yaml. (etv_client is imported only by scripts/scripted-schedules/entrypoint.py, a container entrypoint pytest never collects.) This does not contradict the dependency-free decisions READ path: decisions_lib._read_frontmatter is hand-written exactly so validation runs where nothing is installed. The one-shot WRITE path is allowed PyYAML, and its test needs it. Verified the way it should have been the first time: a clean venv with pytest alone reproduces CI's collection error; adding pyyaml gives 111 passed. Comments, ci-cd.md and the decision record all corrected to drop the "pure stdlib" claim. Refs #631 |
||
|
|
c0f4a52d7a |
ci(631): preflight jq/git in script-tests, and reflow the header comment
test_post_review_verdict.py and test_merge_consent_exemption.py exec the real post-review-verdict.sh and pretooluse-merge-consent.sh, which shell out to jq ~26 times. Those tests shim `curl` on PATH but NOT `jq`, and no existing small-lane job proves jq is present on that image — so a missing jq would land as ~20 opaque assertion failures rather than a diagnosis. Checks rather than installs: #390 deliberately removed run-time apt-get from CI, so the correct fix for a genuine miss is baking the tool into the runner image, and the error message says so. Refs #631 |
||
|
|
69d8d3ccfe |
ci(631): run scripts/tests in CI as its own job, not inside the flake-covered decisions-guard
Nothing executed scripts/tests/. No workflow and no Husky hook invoked pytest: decisions-guard runs decisions_validate.py and build_decisions_catalog.py directly (the code, never its tests) and the `test` job is dotnet test only. The 111 tests guarding the decision corpus, the #610 migration-equivalence harness, the merge-consent exemption logic and the #622 review-verdict poster were enforced only by whoever remembered to run pytest locally. Adds a `script-tests` job to pr-checks.yml rather than the step inside decisions-guard the issue proposed. decisions-guard is covered by ci.decisions-lifecycle-flake, a standing instruction that a lone `decisions lifecycle` red is a known flake and must not be investigated — so a genuine pytest regression there would surface as exactly the red every session is told to wave through. A gate inherits the credibility of the job it lives in. Runs unconditionally: the suite executes the real post-review-verdict.sh and pretooluse-merge-consent.sh, so a `scripts/**` path filter would miss a .claude/hooks/** edit. ~10s, pure stdlib + pytest. Negative control verified locally (broken test -> exit 1); CI-red measurement follows on a scratch PR per the Done-when. Refs #631 |
||
|
|
eb339084f9 |
Merge pull request 'fix(621): make an unparseable decision record loud instead of silently invisible' (#641) from fix/621-record-parse-guard into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m2s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 16m33s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 18m7s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 6m57s
|
||
|
|
4fd806bebf |
Merge pull request 'fix(633): document the 0-based paging contract on the OpenAPI parameters' (#646) from fix/633-openapi-paging-descriptions into main
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Has been cancelled
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been cancelled
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been cancelled
|
||
|
|
5f3623b321 |
Merge pull request 'fix(634): page the rerun-collection picker to completeness' (#645) from fix/634-rerun-picker-paging into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 12s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 14s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 33s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
|
||
|
|
9949703585 |
docs(621): state the mutation count precisely — it depends on which mutation
Review verdict / Set review-verdict status (pull_request) Successful in 4s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
PR Gates / decisions lifecycle (pull_request) Successful in 19s
PR Gates / Docs update reminder (pull_request) Successful in 21s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m38s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 11s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 21m20s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 17m45s
review-verdict/h10 Review-verdict: MERGEABLE @ 9949703
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The record said "10 tests go red"; on the rebased base a full short-circuit reddens 11. Both numbers are real and measure different mutations: neutering only the scan loop leaves the empty-wing check live (10), while returning [] from the whole function disables that too (11). The reviewer flagged the phrasing as ambiguous before the count also drifted. Now states the mutation alongside the number, since a bare count is exactly the kind of frozen figure this session has watched rot four times. Refs #621 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3d720a6bc1 |
fix(621): exempt by exact path, not basename — my "no basename exemption" claim was false
review-verdict/h10 Awaiting review verdict for 3d720a6
Review verdict / Set review-verdict status (pull_request) Successful in 2s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 14s
PR Gates / Docs update reminder (pull_request) Successful in 14s
PR Gates / decisions lifecycle (pull_request) Successful in 28s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m48s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 17m57s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 22m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Re-review found the basename exemption was not actually removed: the code still dropped
any wing-root README.md, and the docstring defended that with "no such file exists
today; the catalog README lives in TOPIC_DIR". That is false — docs/decisions/archive/
README.md exists and parses to []. So the rule text in the record, the catalog row and
ci-cd.md all asserted something the shipped code contradicted.
It was also a live hole, not just wrong prose: dl.active_files() globs RECORDS_DIR.rglob
with NO filter, so a future docs/decisions/records/README.md would be a corpus source
while exempt from the guard — verbatim the hole this PR cites as the reason to stop
using _NON_DECISION_FILES, reintroduced one directory over.
Now exempt by exact RELATIVE PATH ({archive/README.md}), with a test pinning that the
same basename in the ACTIVE wing is still faulted. Mutation-verified.
DISAGREEING with one review finding, deliberately. It asked for the empty-wing
anti-vacuity check to be symmetric across both wings. The semantics differ: zero ACTIVE
records means the scan measured nothing and any clean result is vacuous, while zero
ARCHIVED records is a normal state — nothing superseded yet, true of any young repo and
every fresh clone before the first supersession. Making it symmetric fails a correct
corpus (it reddened 7 tests). The asymmetry stays, with the reason in the code and a
test pinning it so nobody "fixes" it back.
127 passed.
Refs #621
|