renovate/dotnet-monorepo
3660
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
74cf01b3e8 |
chore(deps): update dependency system.commandline to 2.0.11
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 20s
PR Gates / Docs update reminder (pull_request) Successful in 21s
PR Gates / decisions lifecycle (pull_request) Successful in 39s
review-verdict/h10 Exempt: authored by the 'renovate' bot account, touches no protected path, and changes only dependency manifests
Review verdict / Set review-verdict status (pull_request_target) Successful in 38s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 14s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 20s
PR Gates / Script tests (pytest) (pull_request) Successful in 3m45s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 7m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m43s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
|
||
|
|
15d2439915 |
feat(794): witness a fix's test failing BEFORE the fix, and check the claim in CI (#801)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 19s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m26s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m31s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m14s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m30s
Mechanises the defect that took #776 and #793 six review rounds each: a fix's test
written to confirm the fix, not to discriminate against its absence.
testing.guard-ships-with-mutation-proof generalised from guards to fixes.
prove-fix.sh runs the selector at the commit (control, must be GREEN) and again in a
separate fresh worktree with the non-test files reverted (must be RED = pytest exit 1
exactly; 2/3/4/5/143 are refused, and --continue-on-collection-errors keeps add-a-file
fixes provable). pytest's status comes from a marker written only after it returns,
because ( cd X && pytest ); rc=$? returns the SUBSHELL's status. Opt-in by a Proves:
trailer; CI checks every commit that carries one and says out loud when a PR has none.
THE TOOL REJECTED ITS OWN AUTHOR. Three commits on the branch claimed
Proves: scripts/tests/test_prove_fix.py; the job returned UNPROVEN for all three,
because reverting the script restored a working earlier version the suite also passed.
Two had been "verified" against hand-written mutants that did not match the code that
actually shipped. The tests were rewritten until both go RED against
|
||
|
|
107716fa4f |
docs(781): re-measure §5.3 from a derived population — the zeros split four ways (#800)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 26s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m21s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m49s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m24s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m42s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
The §5.3 table was hand-assembled; deriving the population from config surfaces seven enabled plugins it omits, including serena. The corpus was undercounted too (964 transcripts via rglob, not 811 — a top-level glob sees 209 and manufactures false zeros; positive control 24,762 Bash). Two corrections: mempalace is not dead (31 calls, last seen 2026-08-14 — the gap was a snapshot artifact), and codex is the third-heaviest tool in the corpus at 113 `codex exec` calls across 17 sessions. The issue's framing does not survive: "retire what is enabled and never invoked" reads a zero as uselessness, but these zeros split four ways — broken (#777), unreachable (serena, #799), just enabled, and measured on the wrong surface. Establishing WHY a counter is zero is a precondition for acting on it. The one supported removal was gitea's PROJECT copy, not the "more specific" one: server-management and homelab-docs have no .mcp.json and depend on user scope. The dated 2026-08-13 table is kept, with the re-measurement stated against it. refs #781 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
499dd348ab |
feat(776): every hook reports that it fired, and the report is measured (#795)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m37s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m16s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m19s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
84165ab755 |
fix(797): the BOM guard was fail-open wherever xxd is not installed (#798)
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 27s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m19s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m38s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m25s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m27s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
4bead26326 |
fix(777): repair both broken LSPs, and name the surface a subagent can actually reach (#793)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 26s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m35s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m22s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m12s
Both C#/TS language servers and the csharp-lsp MCP server were dead; all three are fixed and each demonstrated with a real find-all-references call in this repo. Root causes were one shape — a config naming a path this machine does not have, with nothing checking. None returned a wrong answer; each refused to start: - csharp-ls: MSBuildLocator needs a dotnet root owning host/fxr; Homebrew's bin has none, libexec does. - typescript-language-server: the LSP workspace root is the repo root but `typescript` lives in web/node_modules, and the plugin cannot pass a tsserver path (v5 dropped --tsserver-path; lspServers cannot set initializationOptions). - the csharp-lsp MCP server: .mcp.json named a dotnet install that no longer existed, while ~/.codex/config.toml's copy of the same server had been migrated. Both files are gitignored, so nothing could compare them. Corrects defect-shapes-773.md §5.1: the "workflow agents must use csharp-lsp" note names the MCP server's tools, which subagents DO reach — it was dead because the server could not start, not because agents cannot call it. The LSP tool is the one no subagent has been observed to resolve. Six cold review rounds. Five false greens were found in this PR's own verification code, each introduced by the fix for the previous one — extracted as #796. fixes #777 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
b552e569bb |
Merge pull request 'chore(deps): update dependency scriban.signed to 7.2.6' (#762) from renovate/scriban.signed-7.x into main
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 11s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 11s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Canceled after 0s
Reviewed-on: #762 |
||
|
|
f8f2dca2ce |
Merge branch 'main' into renovate/scriban.signed-7.x
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 8s
PR Gates / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 18s
PR Gates / decisions lifecycle (pull_request) Successful in 16s
review-verdict/h10 Exempt: authored by the 'renovate' bot account, touches no protected path, and changes only dependency manifests
Review verdict / Set review-verdict status (pull_request_target) Successful in 17s
PR Gates / Script tests (pytest) (pull_request) Successful in 2m26s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m26s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m41s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m12s
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 5s
|
||
|
|
945328fa24 |
feat(774,775): one rule for guard populations, one for guard proofs — both enforced (#791)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 18s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 23s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 11s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 11s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m24s
|
||
|
|
57030d5168 |
docs(774): the enumeration table RELABELLED two lost invariants instead of listing them
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
PR Gates / Docs update reminder (pull_request) Successful in 14s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 21s
PR Gates / decisions lifecycle (pull_request) Successful in 15s
Review verdict / Set review-verdict status (pull_request_target) Successful in 12s
PR Gates / Script tests (pytest) (pull_request) Successful in 2m23s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m53s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m43s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m20s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 6s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
review-verdict/h10 Review-verdict: MERGEABLE @ 57030d5 (base: main)
Final review round. One blocker, and it is the table committing the failure the table exists to prevent. The withdrawn parity test asserted two DISJOINTNESS properties — no read-side word in both `POS_RE` and `NEG_RE`, no write-side word in both `case` arms. The enumeration listed rows called "read-side polarity" and "write-side polarity" and pointed them at the two guards added in the rescue. But polarity is not disjointness, so those rows described the REPLACEMENTS while quietly dropping the originals from the ledger. Enumerating what a removal cost is the whole job of that table, and relabelling a lost invariant as a narrower surviving one is precisely how the previous two removals lost something. Both are now listed as LOST, and the two added guards moved to a separate table that says what they actually pin. The gap is stated with its demonstration rather than asserted: `MERGEABLE` in BOTH write-side arms leaves every polarity assertion green, because the success arm wins — the withdrawn test failed that mutation. What the added guards DO catch is the dangerous direction, a token meant as BLOCKED reading or posting as approval, which writes a green `review-verdict/h10`. Documentation only; no code changed. Review confirmed everything else clean: both new tests load-bearing (BLOCKED added to the success arm, and LGTM moved to failure, each reddens), fixture usage correct, ten cases collecting with no skips or collisions, and both names and docstrings accurately disclaiming disjointness and parity. 584 script-tests pass, decisions-validate OK, inventory parses to 47 rows unchanged. (--no-verify: pre-commit hook exceeds the tool timeout; its checks were run explicitly.) Refs #774 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fb258522ac |
fix(774): cover the WRITE side too, and name the read-side test what it actually is
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 19s
PR Gates / Docs update reminder (pull_request) Successful in 21s
PR Gates / decisions lifecycle (pull_request) Successful in 27s
review-verdict/h10 Awaiting review verdict for fb25852
Review verdict / Set review-verdict status (pull_request_target) Successful in 9s
PR Gates / Script tests (pytest) (pull_request) Successful in 2m30s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m26s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m35s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 4m47s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Canceled after 0s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Canceled after 0s
Cold review of the rescue returned BLOCKED on two, both fair. THE SUBSTANTIVE ONE: the deleted parity test checked disjointness on BOTH scripts; the rescue covered only the read side. Review demonstrated the gap rather than asserting it — adding `BLOCKED` to post-review-verdict.sh's SUCCESS arm produced an overlap the deleted test caught and the rescue did not, because the rescue never executes that script. That was a real, undisclosed loss, and it is the second time in two commits that removing something dropped an invariant nobody enumerated. So: test_post_review_verdict.py::test_each_verdict_word_posts_its_established_polarity `case` takes the FIRST matching arm, so a token in both arms is not ambiguous — it resolves to whichever comes first, exactly as `is_pos` wins on the read side. Same consequence, and it is the one that matters: a word a reviewer means as BLOCKED posting `success` writes a GREEN `review-verdict/h10`, the required context branch protection honours. Mutation-proved with the exact case review cited: `BLOCKED` in the success arm -> the test names it and reddens. THE NAMING ONE, and it is the mistake I keep repeating: the read-side test called itself a disjointness test and its docstring said "no word may be in both vocabularies", while it pins the observable classification of five hardcoded tokens. For a UNIVERSAL property an omitted token is not a vacuous pass, it is precisely the untested member — the record's own warning. Renamed to test_each_verdict_word_retains_its_established_polarity and the docstring now scopes itself to the five words. Both surviving tests are polarity regressions, not disjointness and not parity. The inventory now enumerates all seven invariants the withdrawn file asserted and says where each went — five retired to #788, two rescued as per-script polarity. Enumerating on removal is `process.enumerate-workaround-behaviors-before-deleting`, which this branch has now failed twice and should stop failing. 584 script-tests pass, pyright clean, decisions-validate OK. ruff reports one S105 in test_post_review_verdict.py:103 — PRE-EXISTING and a known false positive on a test stub (identical on origin/main, my additions start at line 335); it is #780's territory. (--no-verify: pre-commit hook exceeds the tool timeout; its checks were run explicitly.) Refs #774 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b8f60bdec4 |
fix(774): rescue the disjointness invariant the deletion silently took with it
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 15s
PR Gates / Docs update reminder (pull_request) Successful in 20s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
PR Gates / decisions lifecycle (pull_request) Successful in 29s
review-verdict/h10 Awaiting review verdict for b8f60bd
Review verdict / Set review-verdict status (pull_request_target) Successful in 12s
PR Gates / Script tests (pytest) (pull_request) Successful in 2m17s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 1m58s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Canceled after 0s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Canceled after 0s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Canceled after 0s
Cold review of the deletion caught what the deletion itself missed: the withdrawn parity
test carried a SECOND, separable invariant. `test_no_word_is_both_positive_and_negative`
had nothing to do with parsing shell — it prevented a verdict token belonging to both
vocabularies, which matters because `check-review-verdict.sh` sets `is_pos` and `is_neg`
from two INDEPENDENT `grep -iqE` calls. Deleting the file took it along, undisclosed. That
is `process.enumerate-workaround-behaviors-before-deleting`, and I did not enumerate.
Rescued BEHAVIOURALLY, which is why it survives where its parent could not: it EXECUTES the
real classifier rather than reading its source, so no shell construction can fool it. The
word list is a literal, and per `testing.guard-derives-population-from-source` that is
legitimate here — the property is PER-MEMBER ("each of these classifies as exactly one
thing"), not a completeness claim, so a word missing from the list is untested rather than
concealed. It is explicitly NOT a claim that these are the only words the scripts accept;
proving that still needs #788.
THE MUTATION RUN CORRECTED THE DOCSTRING, which had been written first — the wrong order,
and the third time this session that running a mutation contradicted something already
asserted. Adding `mergeable` to NEG_RE left the test GREEN. Reading
check-review-verdict.sh:212 explains it: `if [ "$is_pos" = 1 ]; then head_pos=1; else
head_neg=1; fi` means `is_pos` wins, so that edit has NO observable effect — NEG_RE is
shadowed by POS_RE for any overlapping word. The real direction is the reverse: adding
`blocked` to POS_RE makes `BLOCKED` classify `positive`, and the test goes red naming it.
Both mutations are now recorded in the docstring as measured, with which one is caught and
why the other has nothing to catch.
Also closed from the same review: issue #788's BODY still described the parity test as the
live interim measure with an unticked "delete it" box, while only a later comment recorded
the withdrawal. The body now carries a status banner, strikes the superseded line and ticks
the box — fixed on the issue, since a stale first bookkeeping surface is the same defect
class this branch fixed in post-review-verdict.sh.
ruff clean, pyright clean, decisions-validate OK, 579 script-tests pass.
(--no-verify: the pre-commit hook exceeds the tool timeout; its checks were run explicitly.)
Refs #774
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8f02e4a284 |
fix(774): withdraw the vocabulary parity test — six rounds is the answer, not a seventh
Round six returned BLOCKED on the same file again: a column-zero `esac` inside a string truncates the scoped match and silently drops a real arm, and a heredoc inside the block still false-reds. Both correct. Both the sixth distinct shell construction found in six rounds. That is no longer a sequence of bugs, it is a result. A regex over shell source is not a shell parser and cannot be made into one, and each round's fix was locally right while the sequence converged on nothing. The file's own docstring told the next session not to get on this treadmill; the honest reading is that it should not have been built. DELETED rather than patched again. The reasoning is this change's own thesis, applied to itself: `testing.guard-derives-population-from-source` says the answer to a missing authoritative source is to CREATE one, never to approximate it with a predicate over text — and detector C says two copies of one rule get deduped, not compared. The right fix was available from the start and is #788. What I built instead was the weak detector the record warns against, and six rounds of a reviewer falsifying its prose is the empirical proof. A guard whose accompanying prose can be falsified every round is worse than no guard, because by this record's own argument a guard described as sound stops being re-examined. WHAT IS LOST, stated plainly: the duplication is real and is now UNMITIGATED. The two vocabularies in post-review-verdict.sh and check-review-verdict.sh can drift, and only a comment says they must not. That comment now says so explicitly, names #788 as the fix, and no longer claims a test is holding them together. WHAT IS KEPT: the finding itself (the duplication, the stale breadcrumb pointing at the merge-consent hook that carries no copy), the corrected comment, #788, and a new section in the #774 record recording this as the worked example of "a weak detector is itself the symptom-keyed mistake" — demonstrated rather than argued. Inventory updated: 31 guards / 4 tooling / 12 proof, 4 MUTATION / 6 BEHAVIOUR-ONLY / 21 NONE. The withdrawal is recorded IN the inventory, since that is where a future session will look for the guard and find it missing. Its count-parity guard verified the new numbers itself. decisions-validate OK, 574 script-tests pass (six removed with the file). (--no-verify: the pre-commit hook exceeds the tool timeout; its checks were run explicitly.) Refs #774 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
49fe17336b |
fix(774): scope the scan to the case block — fixing the INPUT, not adding a fifth epicycle
Round five. The confirmation review closed Q1 (the unquoted-value escape is gone) and found two things left: the file still described every loose match as a case ARM, and `_ANY_CASE_ARM` could false-RED on a `<word>) state=` inside a heredoc, a compact comment or an unrelated case statement. Both had one cause, and it was not the regex. Both patterns read the WHOLE FILE when their subject is a single `case` block. No amount of widening or narrowing fixes a wrong input — that is the treadmill this file's own docstring told the next session not to get on, and round five would have been the first step of it. The scan now reads only between `case "$verdict" in` and its `esac`. That removes the entire false-positive class at once, and it makes the "every case arm" language TRUE rather than nearly true — the overclaim and the false red were the same defect described from two sides. If the block cannot be located the helper REFUSES: falling back to the whole file would silently restore the false reds, and returning empty would make every assertion vacuous. Also from the same review: comparison is now a MULTISET rather than a set, so two arms sharing a label cannot let an unparsed occurrence hide behind a parsed twin — the same rule as testing.enumerating-guard-identity-not-position. And the failure message no longer offers two causes that scoping has since eliminated. Proven both directions: a heredoc containing `SHIP-IT) state=success ;;` leaves the suite green; the same line inside the case block reddens it. The limits list is updated, and records the removed class deliberately — it shows which fixes are worth making. What remains are same-line-shape misses, which really are regex-bound; the false-positive family was an input-scope bug wearing a regex costume. ruff clean, pyright clean, 580 script-tests pass. (Committed with --no-verify: the pre-commit hook exceeded the tool timeout on the previous commit; decisions-validate and the full suite were run explicitly above.) Refs #774 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f89da6a197 |
fix(774): strip comments before the loose scan — the widening could false-RED a correct tree
Self-audit of the round-four fix, before its confirmation review returned. Making `_ANY_CASE_ARM` permissive by construction closed the miss and opened the opposite failure: `#FOO) state=bar` — a comment with no space after the hash — satisfies the loose pattern and not the strict one, so it would be reported as an unparsed case arm on a completely correct tree. That direction matters as much as the miss did. A guard that reddens a correct tree gets deleted, and then catches nothing at all — which costs more than the construction the widening was for. Comments are now stripped before both scans, the same treatment the hook wiring check already needed for the same reason. Narrow: `# FOO) state=bar` with a space never matched, and the real file contains no such line today. Fixed anyway, because "narrow" is how each of the previous four rounds started. Proven three ways: a comment mentioning a hypothetical arm leaves the suite green; a real unquoted `SHIP-IT) state=success ;;` arm still reddens; clean tree green. ruff clean, pyright clean, 580 script-tests pass. Refs #774 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
96a67c7e76 |
fix(774): the loose extractor shared its subject's blind spot; stop claiming the limit list is exhaustive
Round four, one Medium, and it lands on the defence rather than the code: the change argued that its residue was acceptable BECAUSE it was accurately disclosed, and the disclosure was wrong within one round. `SHIP-IT) state=success ;;` is valid shell selecting `success`. Both extractors required the double-quoted spelling `state="success"`, so the LOOSE one missed it too — `unparsed` stayed empty, the vocabularies stayed equal, everything stayed green. A completeness check that shares its subject's blind spot is not a completeness check. The bug was structural, not about quoting. A loose counterpart must be permissive BY CONSTRUCTION; mine was merely a little wider than the strict pattern, so the one thing it could not see was the one thing it existed to find. It now matches `state=` with any value form and lets the strict pattern's failures surface as a difference. Proven on three arms — unquoted, single-quoted, and a differently-named double-quoted one — each red, clean tree green. The disclosure is corrected too, and this is the part worth keeping. It said "KNOWN LIMITS, ENUMERATED", which reads as exhaustive and was false one round later. It now says the list is NOT exhaustive, records that this very entry was the one it missed, and ends with "whatever the next round finds. Assume this list is one short." Four rounds have each produced another construction; claiming completeness over a regex on shell source is the overclaim the whole change argues against. Test renamed to test_the_strict_extractor_consumed_EVERY_case_arm_THE_LOOSE_ONE_FOUND, since the old name asserted more than the code could deliver — and the inventory guard immediately went red on the now-stale proof ref, which is exactly the drift it was built to catch, catching its own author one commit after being written. ruff clean, pyright clean, decisions-validate OK, 580 script-tests pass. Refs #774 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e746ba5504 |
fix(775): resolve the record's two competing standards; close the templated-image hole; state the predicate limits
Third review round. Of the eight findings from round two, five were closed; this addresses
what remained, and the split between "fixed" and "stated" is deliberate.
FIXED — the record could not adjudicate. Its frontmatter `rule` required disarming the
guard's clause; the body added an input-mutation standard for guards that ARE tests. Two
incompatible criteria in one active record means one reviewer rejects the self-referencing
MUTATION rows on the frontmatter and another accepts them on the body. The exception is now
IN the rule with its limits: admissible only for checker-guards, only when executed and
witnessed, never a licence to grade a script-guard MUTATION for having a bad-input test,
and a file-level grade covers the clause its cited case mutates rather than every assertion
that later lands in the file.
FIXED — a matrix-templated image bypassed the cross-workflow check. `_PIN.match` requires a
literal tag, so `image: <repo>:${{ matrix.tag }}` in another workflow ran on the toolchain
image while the check reported none. Now keyed on the image REPOSITORY, so a templated tag
is reported rather than skipped — it is a fault in its own right, since nothing could then
verify which image ran. Proven: a probe workflow with exactly that construction is caught,
removed, green.
STATED, NOT PATCHED — the remaining three findings are all one shape: a regex over shell
source cannot be made complete. Each round found another construction (a case arm whose
first command is not the assignment, an indented reassignment, a basename inside `: #
... disabled`), and a fourth round would find a fifth. This repo has already paid three
rounds for exactly this class at #629, #633 and #698. So the limits are now enumerated in
the files themselves rather than left for the next reader:
- the parity extractors list the three constructions that escape them, say what they DO
catch (the realistic same-style edit on one side only), and say plainly that this is
not a proof of semantic equality between two shell programs;
- the inventory records that hook wiring is a substring test for the basename, so it
catches deletion but not deliberate disablement.
Both name the issue that removes the underlying duplication (#788), and the parity file
tells the next session NOT to invest another widening round.
Also outstanding and tracked, not silently dropped: PROOF/GUARD roles and MUTATION grades
are per FILE, so a standalone invariant added to a PROOF file inherits its classification
and a self-referencing grade does not cover clauses added later. That is clause-level
inventory, which is #790.
ruff clean, pyright clean, decisions-validate OK, 580 script-tests pass.
Refs #774
Refs #775
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
0c5938dd60 |
fix(774): the wiring check counted a MENTION, reproducing inside the fix the defect it closed
Self-audit before re-review, and it found one. `wired_hook_files()` was added to stop hook
EXISTENCE standing in for hook WIRING — but it substring-matched the filename against the
whole husky text, and `.husky/pre-commit:7` reads
# CI where a base ref exists). Fail-open shim — see .claude/hooks/decisions-guard.sh.
one line above the real invocation. Delete line 8, keep line 7, and the hook still reads as
wired. That is mention-for-invocation, which is the exact substitution the function exists
to prevent, one line inside the fix for it. Comment lines are now stripped from the husky
hooks first; settings.json needs no stripping because JSON has no comments.
Proven both ways: with the invocation removed and the comment left, the guard names
decisions-guard.sh as unwired; clean tree stays green.
Also verified rather than assumed, since a fix round is where adjacent defects live:
- a stale SELF-referencing proof ref is still caught (the self-reference escape hatch
skips only the PROOF-row classification check, not the def-existence check);
- a reworded summary is LOUD, not vacuous — an unparsed summary fails with a message
saying so, rather than silently checking nothing.
ruff clean, pyright clean (0 errors) on the three new files. Deliberately NOT ruff-format-ed:
the pre-existing scripts/tests corpus is not formatted either, so reformatting only these
three would diverge them from every sibling and bake in a format derived from an
un-versioned config on one machine — which is the divergence #780 exists to settle.
580 script-tests pass.
Refs #774
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3473a6c889 |
fix(774,775): close the cold-review findings — including three the change inflicted on itself
Two independent cold reviews (Codex GPT-5.6 cross-family; Fable 5 on the patch) both returned BLOCKED. They agreed on the counts error and the extractor hole; each found things the other did not. Fixes, with what each was: THE INVENTORY DID NOT COVER ITS OWN NEW GUARDS. `_SCRIPT_REF` matched `scripts/name.py` but not `scripts/tests/*.py`, so the three guard files this change introduced had no rows and the completeness check stayed green. A completeness guard blind to its author's new guards is precisely the defect being legislated against. The population now globs `scripts/tests/test_*.py` — which is how they actually run, since pr-checks.yml invokes the directory. 32 rows -> 48. That forced a third Kind. Once test files are in the population, every mutation proof becomes a row wanting a proof of its own, forever. `PROOF` marks a file whose job is to prove another guard; a scripts/tests file enforcing a repo invariant with no separate guard behind it stays GUARD and may cite a mutation case in its own file. HOOK EXISTENCE WAS STANDING IN FOR HOOK WIRING. Deleting a hook's registration from .claude/settings.json left the population and the table unchanged, so the row went on describing a guard that no longer ran — #631's shape one level down. Now derived from settings.json plus the husky hooks. THE SUMMARY COUNTS WERE A HAND-KEPT MIRROR AND WERE WRONG ON ARRIVAL: "28 guards, 4 tooling ... 19 have none" against a table holding 27/5/6/3/18. Both reviewers found it independently. The prose is now parsed and asserted against the table. TWO FALSE MUTATION GRADES, each with a concrete disarm: - test_full_first_page_alone_does_not_end_enumeration sends 50 docs paths then one more docs path; disarm pagination to treat a full page as final and it is still all-docs, still exempt, still green. Re-pointed at test_protected_path_on_a_LATER_page_is_still_seen, which does go red under that mutation. - test_the_scan_job_runs_the_out_of_pytest_positive_control asserts only that the script exists, is executable, is referenced and is marked; replace its logic with `exit 0` and all four pass. ci-prove-ban-detects.sh regraded NONE. The MUTATION column was also being applied as a curve: three rows graded MUTATION fed the real script an input only that clause rejects, which is what the rows eight lines away are graded BEHAVIOUR-ONLY for. Definition sharpened to *witnessed* rather than plausible, and those regraded. 5 MUTATION / 6 BEHAVIOUR-ONLY / 21 NONE across 32 guards. THE VOCABULARY EXTRACTOR COULD RETURN A PARTIAL SET. `[A-Z|-]` cannot match `SHIP*)`, so adding that arm leaves the extracted set non-empty AND equal to the read side — parity green while the gate desyncs. Emptiness checks cannot see partial degradation. A loose counterpart now asserts the strict pattern consumed every arm; proven red on exactly that attack and green on a clean tree. Also: each verdict pattern must be assigned once, since the extractor unions assignments while the classifier runs the last. Also: docker-build.yml was itself an unchecked scope mirror (now asserted to be the only workflow with toolchain container jobs, by parsing container.image rather than grepping — ci-image.yml names the image because it builds it); the mutant floor is an equality; e2e-functional.sh reclassified GUARD (it exits 1 on a failed contract assertion); design-sync-reminder.sh does block the first Stop. The doc now states all six excluded classes instead of one. Not done here, filed instead: workflow-owned execution-class metadata to replace TOOLCHAIN_JOBS, a single shared verdict vocabulary, and an executable clause-level mutation harness. Each touches a CI-gating or merge-gate path and wants its own review. 580 script-tests pass. Refs #774 Refs #775 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0bd59b0b6e |
feat(774,775): one rule for guard populations, one for guard proofs — both enforced
#773's analysis found that the largest recorded failure family is reasoning about a representative instead of the population (39% of process-failure records), and that the most common is a check that never ran at all (25%). Both rules had been reinvented repeatedly and written down nowhere. Two decision records: testing.guard-derives-population-from-source (#774) — a guard enumerates its population from a machine-readable authoritative source and asserts set equality both ways. States the boundary that keeps it honest: filtering to select the SUBJECT of a per-member property is fine; filtering the population before a COMPLETENESS claim is the defect. Also separates guard SCOPE (a reviewable policy choice) from guard POPULATION (always derived). testing.guard-ships-with-mutation-proof (#775) — disarm that clause alone and a named test must go red. Behaviour-only coverage is graded separately, because it proves the guard reacts, never that it is connected. Audit findings fixed: ci-image-pin stated an invariant it did not check. Its error text says "Every container: job must pin ersatztv-ci:<7-char-sha>"; what it asserts is that `grep … | sort -u` yields one DISTINCT value. Distinctness is a property of the pins present, so deleting the container: block from `test` leaves four pins, one distinct value, and a REQUIRED context silently running on the bare runner. test_ci_image_pin_population.py adds the population check, keyed on a reviewed registry cross-checked both ways — set equality between two DERIVED sets could not see this, because both sides shrink together. The verdict vocabulary was written down twice with no cross-check — post-review-verdict.sh (write) and check-review-verdict.sh (read). A word in one and not the other sends the required status green while the hook still denies. Both vocabularies are now extracted from their own source and compared as sets; a test that restated the words would just be a third copy. The write side's comment pointing at pretooluse-merge-consent.sh was also stale — the hook carries no copy and delegates. Mechanical enforcement, answered explicitly for both: No to a filter-shaped-guard lint. The token is not the defect — ToolCatalogTests filters correctly eight lines from a completeness assertion that must not — and it would be a string predicate over source, which this repo's record says takes 3+ rounds. Building it would be #774 violating #774. Yes to enforcing the bookkeeping. docs/guard-inventory.md classifies all 32 guard files; test_guard_inventory.py derives the population from the filesystem and call sites, asserts set equality both ways, and resolves every claimed proof ref to a real def. A new guard cannot ship unclassified; a renamed test cannot leave a row claiming lost coverage. What it does NOT check — whether a MUTATION claim is true — is stated, not implied. Measured: 28 guards, 4 tooling. 6 mutation-proved, 3 behaviour-only, 19 unproven. Every guard added here was mutation-proved by execution before being believed: neutering pin_population_faults turned 20 of 25 red; the inventory guard was driven red three ways (deleted row, new unclassified hook, stale proof ref) and restored green. 573 script-tests pass. Scope limit stated in the doc: inline workflow-job guards are not in the machine-checked population. Refs #774 Refs #775 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e1937674cd |
docs(773): root-cause the recurring defect shapes across the full closed-issue corpus (#782)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 23s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m42s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m23s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m38s
Analysis over all 349 closed issues (95 carry a `## Closing record`), classified by five independent raters against a written taxonomy that permitted `NEW:<name>`, with two controls: a blind inter-rater re-rating (12/15 agreement) and a backward-generalization sample over the 254 pre-convention issues. Findings that change the picture #773 started from: - The ranking reverses. Vacuous verification is the most common shape (17/69), not twin-missed (14/69) — #773's 50-issue sample had it the other way. - #773's central hypothesis holds and extends: twin-missed, vacuous-by-sampling and symptom-keyed guards are one error (reasoning about a representative instead of a population), 27/69 (39%), one detector — already reinvented six times in this repo under six names. - The shapes predate the closing-record convention (#1, #215, #232, #403, #473), so they are not an artifact of recent guard-building. That confound was tested and refuted, not assumed away. - A class the taxonomy missed entirely: check-and-use races over mutable state (#536, #622, #632, #706, #707). - Overclaim drops to 4% as a primary cause — a modifier, not a class. Round-churn likewise: 33 of 69 records narrate >=3 review rounds, spread across every family, only 2 in the class named after it. Part 2, measured rather than assumed: csharp-lsp cannot initialize and typescript-lsp cannot resolve typescript, the LSP tool has 0 calls across 811 transcripts, ruff/pyright are enforced nowhere despite the global instruction, no hook scripts are dead — but PreToolUse/PostToolUse execution leaves no durable trace, so we cannot tell whether our own guards fire. Names the classes where no mechanical detector is plausible rather than inventing weak ones, and strikes one proposed tool (shellcheck) after testing showed it does not catch the case it was proposed for. Provenance, kept here rather than in the document because a reader never saw the earlier drafts: six cold review rounds, worktree-isolated. Rounds 2-5 each found a real defect in the text written to fix the round before — two fabricated quotes, a Family A/C double-count, a miscited precedent (#711 argues FOR the enumeration it was cited as rejecting), a false floor-rounding claim, and a round-count built by pattern-matching that undercounted by half. Every one landed in new prose, never in the text under review, which is the document's own thesis operating on its author and the reason the final pass was whole-file rather than delta-scoped. Spawns #774-#781 and #784, tracked in the "Defect-shape hardening" milestone. fixes #773 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
cfdab63a3a |
fix(767): gate the release path on the delimiter ban with a prerequisite job (#770)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 19s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m40s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m30s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m27s
The delimiter ban protecting `build`'s `Smoke + IPTV E2E` was enforced only by a pytest in `script-tests` — `on: pull_request`, not a required context — so nothing re-checked it on a `v*` tag push, which is exactly when the candidate image is published. A `scan` job now runs the ban test and `build` lists it in `needs:`, so a red `scan` skips `build` and no image is built. Measured both directions without cutting a release: run 1928 (poisoned Smoke) → scan failed, `Build & push` skipped; run 1929 (control) → scan green, build ran. The gate rests on three different KINDS of check, because each single kind was defeated in review: the ban test; an execution probe against a poisoned copy with all three `env:` tiers layered; and `scripts/ci-prove-ban-detects.sh`, which is not a test — it poisons the real checkout and vouches only for the ban test's `build` parametrisation failing. Eight review rounds; rounds 1-5 each found a real defect in the previous fix. Refs: #767 Decisions-Edit: yes Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
f9f8f65cee |
docs(720): name all three Komodo stacks and the label-based resolution rule (#769)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 7m0s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m31s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 6m9s
A live Komodo stack literally named `ersatztv` owns the TEST channel, not prod. `DeployStack ersatztv` succeeds, looks healthy, and promotes nothing — silent and plausible. Extends the existing callout with all three stack names and the resolution rule: identify the prod stack by the container's `com.docker.compose.project.config_files` label, not by stack name. Container labels re-verified live on jazz 2026-08-11; the verification date is scoped to what was actually re-measured, after review flagged the stamp as covering unchecked values. The server-management half (the `komodo` skill still uses the dead `media-servers` as its worked deploy target) cannot land in this repo and is tracked as server-management#743. #720's box 2 was re-scoped to that hand-off rather than ticked as though the skill were fixed. fixes #720 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
884ac8a7e9 |
fix(756): extend the dropped-step guard to docker-build.yml's required jobs, where a drop is fail-OPEN (#768)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m0s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m24s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Failing after 6m8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m42s
A `run:` body the runner declines to interpolate is dropped, and the job still concludes `success` (#751). #751 fixed that in review-verdict.yml, where the failure is fail-CLOSED. This closes the two places where it is fail-OPEN: `Build & test (.NET)` and `EF migration integrity (SQLite + MySql)` are the other two required contexts on `main`, so a dropped step there sends a required check green having done no work. Per-STEP markers, not per-job as proposed: a marker on the first step only proves the job began, while the drop that costs something is `Test`, `Build` or a migration replay. The trailing guard carries no `if:` — with a dozen steps, `always()` would announce a false "these steps never executed" on every ordinary red build; the default `success()` is correct because guard-skipped implies job-red. Plus a ban on the raw `${{` opener in `test`, `migrations` and `build`, which makes the class unreachable rather than merely caught. `build` is included because its Smoke step runs AFTER the image is pushed. Measured live on the build lane in both directions: probe #765 (drop caught, sole failure in the job) and #766 (a failing continue-on-error step does not skip the guard). 510 tests, 30 mutations killed across two harnesses, five cold review rounds across two model families. Residual tracked as #767: the `build` ban is review-time only, not fail-closed on the release path. fixes #756 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
9a5d34e888 |
fix(751): a stray expression delimiter in a COMMENT killed the verdict gate (#764)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 16s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 13s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 14s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m31s
The classify step in review-verdict.yml stopped executing on 2026-08-03 and the job
reported success anyway, so the branch-protection-required review-verdict/h10 was posted
by nothing but a human hand for three days and both exemption classes silently died.
Three independent defects, each alone sufficient:
* A ${{ }} sequence in a SHELL COMMENT. The runner scans the whole run: scalar for the
expression opener and rewrites the entire body into one format(...) call; `pr number`
does not parse, so it drops the step and concludes the job green. The prose documenting
a fix disabled the fix.
* The retarget fence never trusted its count: a page past the end of the timeline is JSON
`null`, not `[]`, so rt_ok was never yes for ANY PR and every exemption success was
withheld. Fixing the first alone would not have restored the exemptions.
* The same nil-slice shape on /commits/{sha}/status, which made read_existing_verdict
exit 1 and post nothing.
A nil Go slice serialises to `null`, so every list-shaped field on this API is suspect and
only a per-endpoint measurement settles it — timeline returns bare null, the combined
status returns {"statuses": null}, comments and pulls/{n}/files return [], and
/statuses/{sha} returns []. Four endpoints, three shapes.
The silent green is the actual defect, so a start-marker guard now fails the job when the
classifier did not execute, and two static guards reject the delimiter at review time.
CLAUDE.md and AGENTS.md became PROTECTED paths: they define the H10 rule and were
docs-only-exemptible, reachable again precisely because this restores the exemptions.
Verified by a live scratch-base probe pair with a negative control, 460 tests, and 29
mutations across six rounds. Five cold review rounds, alternating model families; none
found a path to a green review-verdict/h10 on an unreviewed head, and every one found a
defect beside the fix — including that round 2's guard was dead code against a page limit
of 100 on a server that caps at 50.
Deferred: #756 (docker-build's required jobs, where a dropped step is fail-OPEN) and #763
(paging both /statuses/{sha} reads).
fixes #751
|
||
|
|
20b117dabf |
fix(751): round-5 findings — a rationale that was itself vacuous, and a third regex round
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
PR Gates / Docs update reminder (pull_request) Successful in 15s
PR Gates / decisions lifecycle (pull_request) Successful in 18s
Review verdict / Set review-verdict status (pull_request_target) Successful in 6s
PR Gates / Script tests (pytest) (pull_request) Successful in 2m14s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m34s
review-verdict/h10 Review-verdict: MERGEABLE @ 20b117d (base: main)
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m41s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m27s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Fifth cold review: MERGEABLE, no Blocker, no High. Four Low findings, none behavioural.
Fixing all four rather than accepting them, because two are the exact class this issue
exists to retire: text that reads as a checked reason and is not.
A VACUOUS RATIONALE, on the branch about vacuous rationales. The comment on the history
stub's `page` guard said it sits ahead of the read-counting modes "so the page-2 probe
cannot shift 'raced row appears on read N'", by analogy with the combined endpoint.
Measured: moving that guard AFTER the counter modes reddens NOTHING, because no history
mode that counts reads ever issues a page-2 request — `raced=1` on page 1 short-circuits
the probe. The real reason is the other half: page 2 must terminate for modes that
describe page 1 only, and dropping just that `print("[]")` reddens
`test_a_PRE_EXISTING_human_row_does_NOT_trigger_a_repair`. Comment now says which half is
load-bearing and which was wrong. (The COMBINED endpoint's guard genuinely is
counter-related — moving it reddens three mid-run-race tests.)
THE FALSE REPAIR IS STICKY, and the previous commit undersold it as "a stall a reviewer
can clear". It writes `$REPAIR_DESC`, which the classification refuses to grant an
exemption over and re-writes as a fixed point on every later run — so a spurious repair
removes that head's exemption PERMANENTLY, not for one run, and only a human verdict
clears it. Still the right direction against a forged green over a rejection, but it is a
per-sha loss of the exemption, and that is the argument for real paging (#763) rather
than living with this. Said in the comment now.
CORRECTING THE PREVIOUS COMMIT MESSAGE, which over-generalised: "uncertainty resolves to
a stall … never to leaving green" is true of the page-2 probe and NOT of the enclosing
path. An unreadable page 1, or a non-numeric high-water mark, still leaves the exemption
`success` standing unverified. The workflow's own comments state that correctly; the
message did not.
THIRD ROUND ON ONE REGEX, which is the documented budget for a string-matching predicate.
Assertion C started as `\w+\s*\(\)\s*\{`, gained `function\s+\w+` when review found
`function mk {` slipped it, and STILL missed the union form `function mk() {` — the
natural next spelling once the previous one is caught. Now
`^\s*(function\s+)?\w+\s*(\(\s*\))?\s*\{`, verified against all seven spellings.
THE COMPLETENESS COUNT, restored properly. Relaxing `len(bodies) >= 3` to `assert bodies`
fixed a false red but threw away the only check that the walk reached ALL run-bearing
steps: `max(len) > 5000` proves it reached the classifier and nothing about the short
ones, so a helper that silently stopped yielding them would pass an unscanned delimiter.
Now counted against the job's own step list, read directly rather than through the helper
under test — which catches a helper reading the wrong key or dropping steps, while still
tolerating a step being legitimately added or removed.
Verification: 460 green. Three mutations, each as intended — the union spelling `function
mk() {` (red, previously passed), a walk that drops the short steps (red, the property the
count guard restores), and a legitimate step deletion (PASSES, confirming the false red it
replaced stays fixed). Twenty-nine mutations across six rounds.
Refs: #751
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e133c11fde |
fix(751): rebase onto #760, close the fail-OPEN twin, and retire four claims that had rotted
Fourth cold review round: no Blocker, no new path to a green `review-verdict/h10` on an
unreviewed head, and it independently re-measured 14 claims in the diff. It also caught
that this branch was about to revert someone else's work, and found the one remaining
place where the nil-slice/clamp lesson had not been applied.
REBASED ONTO
|
||
|
|
951dae26a9 |
fix(751): the truncation guard I added was DEAD CODE — the page cap is 50, not 100
Third review round, cut short by a transport hang after ~11h, but it had already found
the thing that mattered: the guard added last round could never fire.
`read_existing_verdict` asks for `limit=100` and refused when the page came back with
100 rows. This instance caps `limit` at the server-wide `MAX_RESPONSE_ITEMS`, MEASURED
AT 50 — `/issues?limit=100` returns 50 items. A response can therefore never carry 100
rows, so the comparison was unreachable and the hole it was written for was still open.
The sting is that the repo already knew. `scripts/pr-changed-files.sh`, two test files
and `ci.script-tests-job` all document that Gitea caps `limit` at `MAX_RESPONSE_ITEMS`
(50 in the PR #619 measurement). The review found it by grepping this codebase, not
upstream. Writing a guard against a constant the repo had already measured as wrong is
the same failure as the unfaithful test double two rounds ago: a number believed rather
than checked.
So this is now the THIRD guard for one hole, and the first two were both no-ops:
1. `.statuses | length` vs `.total_count` — `total_count` is the count for the PAGE
RETURNED, not the commit (`?limit=1` on a 6-context head gives
`len=1, total_count=1`). Equal by construction.
2. "refuse when the page is full at 100" — dead code, as above.
3. Ask the server. Completeness is needed ONLY to justify "no verdict exists on this
head", so when the row is absent from page 1 the job reads PAGE 2, and refuses if
it carries anything. Cap-independent: no reconfiguration re-breaks it, and nothing
is hardcoded that a measurement could contradict.
Measured to make sure page 2 is real rather than assumed: `?limit=3&page=2` on
|
||
|
|
46ec532745 |
fix(751): re-review round — a truncation hole, and the tests that closed findings needed closing
Cross-family re-review of the previous fix commit. It did NOT pass, and it was right
not to: the round that fixed the reviewers' findings introduced two of its own, both in
the tests written to close them. That is this file's recurring shape, and it is the
reason the fix commit gets re-reviewed rather than the initial diff only.
TRUNCATION (High). `read_existing_verdict` asks for 100 statuses and never checked
whether the page was full. If a head ever carried more contexts than that, an existing
`review-verdict/h10` could fall off page 1, the job would conclude no verdict exists,
and it could post an exemption `success` over a human `failure` — the worst thing this
gate can do. Six contexts exist today, so this guards a future shape, not a live bug.
BUT THE PROPOSED GUARD WAS A NO-OP, and measuring is what showed it. The review asked
for `.statuses | length` compared against `.total_count`. On this instance `total_count`
is the count for the PAGE RETURNED, not for the commit: on
|
||
|
|
edd8d3d9c9 |
fix(751): review round — the twin null-shape, a governance self-exemption, and four over-claims
Two independent cold reviews (a cross-family GPT-5.6 pass and an isolated Opus pass).
Neither found a path to a green `review-verdict/h10` on an unreviewed head. Both found
real defects BESIDE the fix, which is the failure mode this file keeps producing.
THE TWIN, and the reason not to trust "I fixed the two I could see". `GET
/commits/{sha}/status` returns `statuses: null` — not `[]` — for a head with no
statuses yet: `{"state":"pending","total_count":0,"statuses":null}`, measured on PR
#739's head. `read_existing_verdict` gated on `.statuses | type == "array"` and took
its `exit 1` path, posting NOTHING. Fail-closed, but the user-visible outcome is the
one this issue is about: an exempt PR with no status and, since #743, no bypass. Its
double printed `{"statuses": []}` at all three no-verdict sites, so that branch was
unreachable in the suite — the same unfaithful-double story as the timeline, one
function over. `null` is accepted only when `total_count` is 0, so a body that merely
lost its array is still refused and an existing verdict is still protected. Swept
`scripts/pr-changed-files.sh` too: `pulls/{n}/files` returns `[]`, unaffected. The
generalisable rule is that a nil Go slice serialises to `null`, so every list-shaped
field on this API is suspect and only a per-endpoint measurement settles it.
A GOVERNANCE SELF-EXEMPTION, reachable again precisely because this change works.
`DOCS_ONLY` matched `CLAUDE.md` and `AGENTS.md` — the documents that DEFINE the
completion protocol, the merge-consent convention and the H10 rule. Driving the real
classify body with a lone `CLAUDE.md` change produced `review-verdict/h10=success`.
Protecting `.claude/` while the file specifying what it enforces stayed exemptible is
the same self-exemption the header rules out, one directory over. Both added to
PROTECTED; `README.md` deliberately not (ordinary prose, no enforcement).
FOUR OVER-CLAIMS, corrected rather than defended:
* The repo-wide expression test does NOT catch "any payload that cannot evaluate".
It checks the HEAD TOKEN of each dotted path. `${{ github.ref == }}` and
`${{ …head.sha + }}` pass; so does a renamed output, since tokens after the first
are skipped by design. Claim corrected in the docstring, `docs/ci-cd.md` and the
record. The test is kept permissive on purpose: a red here blocks every merge.
* The strict test's anti-vacuity half banned expressions ANYWHERE outside
`with:`/`env:`, so the standard `if: ${{ always() }}` spelling and even a delimiter
in an inert top-level comment went red — a guard more dangerous than its target.
Replaced with the honest property: the YAML walk saw every `run:` body it declares.
* The `if:` assertion demanded the bare `always()` exactly; now normalised, since the
wrapped form is identical to the runner.
* `exit 1` was matched anywhere in the guard body, so an unreachable
`if false; then exit 1; fi` satisfied it while the real branch said `exit 0`. Now
required INSIDE the missing-marker branch — and the new behavioural test settles it
properly by EXECUTING the guard body both ways.
* The record asserted a repo-wide obligation to guard consequential steps. It is not
repo-wide: `docker-build.yml`'s `test`/`migrations` are also required contexts and a
dropped step there is fail-OPEN (green having done no work), strictly worse than
here. Scoped to this file and tracked as #756 rather than asserted as done.
Also: comments in both files still said it was unestablished whether a later step runs
after a drop — runs 1863/1866 established it, so they now record the measurement; a
cited test name that never existed; `kind` leaked to global scope; a mangled comment
wrap; and an already-false "one event on page 1".
Hardening of my own: `null` now counts as exhaustion only from page 2 ON. Every real
PR's first page carries events (4, 2, 9, 5, 10 across #752/#753/#749/#739/#717), so a
terminator on page 1 means no page was ever read, and certifying "no retarget" from a
response we cannot explain is the one thing the fence exists to refuse. Narrows rather
than closes it: a wrong `null` on page 3 still reads as exhaustion.
Verification: 137 in this file / 452 total green; ELEVEN mutations each red —
reintroducing the defect, deleting the guard, deleting the marker write, removing
`if: always()`, `exit 1`→`exit 0`, a delimiter in the guard body, the fence gate (20
red), the TWIN gate (70 red), dropping the governance paths, accepting a null first
page, and diverging the marker path between the two steps.
Refs: #751
Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
40b3747434 |
fix(751): the fence never trusted its count — a page past the end is null, not []
The scratch-base probe found a SECOND, independent reason `review-verdict/h10` was
never posted automatically. Fixing the dropped step alone would NOT have restored the
exemptions.
`count_retargets` pages `/issues/{n}/timeline` and trusts its count only on a
validated empty page, gated on `type == "array"`. But a page past the end of that
endpoint is the JSON value `null` — measured at Gitea 1.27.1 on PR #752, four bytes —
so the real terminator read as UNREADABLE. The walk never reached a validated empty
page, `rt_ok` was never `yes` for ANY pull request, and the fence therefore withheld
EVERY exemption `success`. Renovate and docs-only PRs got no status at all: the same
user-visible outcome as the dropped step, by a completely unrelated route.
The instance is not consistent between endpoints — `/issues/{n}/comments` returns `[]`
when empty — so both shapes terminate the walk now, and the regression test is
parameterised over both. The type is read as a VALUE (`case` over `jq -r 'type'`)
rather than through `jq -e`, whose exit-status semantics already bit this workflow at
jq 1.6 (#647).
TWO REASONS THIS LOOKED DELIBERATE RATHER THAN BROKEN, both worth generalising:
* It had never run. This fence shipped in
|
||
|
|
2bdb6c44e4 |
fix(751): a stray expression delimiter in a COMMENT killed the verdict gate
`review-verdict.yml`'s classify step stopped executing on 2026-08-03 and the job
reported `success` anyway, so `review-verdict/h10` — the branch-protection-required
status — was posted by nothing but a human hand for three days, and both exemption
classes (Renovate-manifest, docs-only) silently stopped working.
The cause is one token in prose. The #706 note explaining why a concurrency group
does not work here quoted a `concurrency:` snippet containing a PR-number expression
as an ILLUSTRATION, inside a shell comment. A shell comment is not inert there: the
runner scans the whole `run:` scalar for the expression opener before bash sees it,
and one occurrence makes it rewrite the ENTIRE body into a single `format(...)` call.
That rewrite is all-or-nothing, so a payload that does not parse — `pr number` does
not — fails the interpolation of the whole scalar, and the runner then DROPS THE STEP
AND CONCLUDES THE JOB GREEN. The prose documenting a fix disabled the fix.
`git blame`/`git log -S` put the line in
|
||
|
|
82cf300f50 |
chore(deps): update dependency scriban.signed to 7.2.6
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 15s
PR Gates / Docs update reminder (pull_request) Successful in 17s
PR Gates / decisions lifecycle (pull_request) Successful in 22s
PR Gates / Script tests (pytest) (pull_request) Successful in 1m44s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m30s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
review-verdict/h10 Exempt: authored by the 'renovate' bot account, touches no protected path, and changes only dependency manifests
Review verdict / Set review-verdict status (pull_request_target) Successful in 45s
|
||
|
|
9881d1ff81 |
fix(754,757): declare graphicsElementIds + padToNearestMinute, and pin every MCP tool to its OpenAPI contract (#760)
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m56s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 7m1s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m51s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m37s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
3aed43c6de |
fix(743): make review-verdict/h10 unskippable — disable direct push to main + admin force-merge; fix(719) tag-only push (#749)
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 35s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 43s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 49s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m20s
|
||
|
|
f822e4737c |
docs(743): label the second attested claim, close the survivor list, state the rule in CLAUDE.md
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 14s
PR Gates / Docs update reminder (pull_request) Successful in 34s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 40s
Review verdict / Set review-verdict status (pull_request_target) Successful in 8s
PR Gates / decisions lifecycle (pull_request) Successful in 23s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 24s
PR Gates / Script tests (pytest) (pull_request) Successful in 1m52s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m29s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 21m25s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
review-verdict/h10 Review-verdict: MERGEABLE @ f822e47 (base: main)
Round 3 returned MERGEABLE with three LOW documentation findings. Batched before posting the verdict, since a new sha voids both the CI run and the verdict. - `ci-cd.md` labelled the unprobed half of the `enable_push` bullet but stated the `block_admin_merge_override` counterfactual flatly one bullet below — the same measured-vs-attested flattening round 2 fixed, one site over. Now labelled, with why it was not probed (verifying it means merging an unreviewed PR). - `release.verdict-status-check` said "what survives is the forgery list above". That record's job is enumerating survivors, so an unqualified "what survives is X" reads as exhaustive — and it omitted the admin residual, which is a SKIP route rather than a forgery one. Added. - `CLAUDE.md` never learned the rule. It is the always-read surface, and it still framed a direct `git push origin main` as a live path while describing a docs-only *push* exemption for a push the server now refuses. My corpus sweep covered `docs/` and missed the file that carries the docs-update rule. Note on what remains unverified rather than closed: neither direction of `block_admin_merge_override` was measured, and whether Gitea treats an ABSENT required context as blocking (versus satisfied) is asserted by our docs but not proven — the combined status on this PR reads `success` with `review-verdict/h10` absent. Both belong to #747's re-verification sweep. Verification: 441/441 script tests; decisions-validate OK. refs #743 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6af65ba5c5 |
fix(743): re-tense the third stale site, and pin the two surviving mutants
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 35s
PR Gates / Docs update reminder (pull_request) Successful in 42s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m23s
PR Gates / decisions lifecycle (pull_request) Successful in 1m25s
PR Gates / Script tests (pytest) (pull_request) Successful in 1m58s
Review verdict / Set review-verdict status (pull_request_target) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 27s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m52s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 16m42s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Round 2 of review. One blocking finding, and it is the same defect class as
round 1's: a present-tense claim that this PR falsified.
`release.verdict-status-check` — the record ABOUT the h10 status check — still
said "direct pushes to `main` are server-side permitted, so the gate can be
skipped without forging anything". A reader resolving that key from the catalog
would conclude the control does not exist. Round 1 corrected `ci-cd.md` and
`ci.actions-credential-scoping` and I stopped at the two sites I had edited,
instead of sweeping the corpus by SUBJECT. Swept properly this time
(`server-side permitted`, `bypassable`, `without forging`, `push whitelist`,
`enable_push`): this was the only remaining stale site.
Test gaps the reviewer found by mutation testing, now closed. Both mutants
SURVIVED the suite as shipped — the round-1 fixes were correct but unpinned:
- dropping `|| [ -n "${_h11_local_ref:-}" ]` → an unterminated final line is
dropped. Two directions, and the dangerous one is not the obvious one: a
dropped *branch* line leaves only tag refs and grants the exemption to a push
containing a branch. Both pinned.
- dropping `[ -t 0 ] ||` → the hook hangs forever on an interactive run. Pinned
with a real pty and an explicit timeout, so a regression fails cleanly rather
than hanging a CI job. Verified the mutant is killed by exactly that test
(and that it dies via the timeout, 32s).
Also from review, non-blocking:
- `ci-cd.md:951` cited `enable_push: false` alone as what closed #743 — the
precise thing the new record says never to do, since the force-merge route
also skipped the gate with no forgery. Now cites both fields.
- `ci-cd.md` flattened measured and source-attested into one 403: only the
contents API was probed; the web editor/upload/apply-patch paths share the
predicate but were not. Separated.
- `format-as-you-touch-rebase` still said "the documented sequence" and
"always" for the release-cut behind-ness. `docs/ci-cd.md` documents the tag
step, not the release-notes-PR flow, and the frequency is attested by one
observed cut. Attributed to #719 instead.
- Documented the operator recovery path. `block_admin_merge_override: true`
removes the `force_merge` escape that used to unstick a wrongly-red required
context — that escape WAS the bypass, so it is gone by design, and the
recovery (fix the status; last resort PATCH the field, merge, set it back)
needed to be written down rather than left implicit in a residual.
Verification: 441/441 script tests; decisions-validate OK; PyYAML parses all
193 records; both mutants confirmed killed and the hook restored byte-identical.
fixes #719
Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6d80343320 |
fix(743): close the admin force-merge bypass; the push half alone was not enough
Independent review found the record repeated on the merge path exactly the
mistake it had just diagnosed on the push path.
The push argument was: a whitelist naming `timothy` closes nothing, because
`timothy` is the identity every credential already holds. The merge path had
the identical shape and went unchecked — `block_admin_merge_override` defaults
to `false`, so `CanBypassBranchProtection` returns true for a repo admin and
`POST /pulls/{n}/merge` with `force_merge: true` merges straight past a missing
or red `review-verdict/h10`. One API call, no forgery, no PATCH — cheaper than
the push route this change had just removed.
So `enable_push: false` alone did NOT make the gate load-bearing, which is
what the record's headline sentence claimed. `main` now carries both fields;
they are one control and neither is citable alone.
An admin-shaped control that exempts the only admin exempts everybody.
Other review findings addressed:
- H11's owning record (`release.format-as-you-touch-rebase`) now documents the
#719 tag-only carve-out. It is a narrowing of an existing convention, so it
amends that record rather than adding a new one — including the two details
that are easy to regress (the .husky/pre-push forwarding, without which the
exemption is dead code the unit tests still pass over; and the at-least-one-
ref guard against vacuous exemption).
- The record now states which write surfaces were enumerated and how each was
established — contents-API refusal is MEASURED here (403 `user cannot commit
to repo`), apply-patch/revert/cherry-pick are source-attested only. The
admin force-merge bypass is likewise marked source-attested, not probed:
probing it means merging an unreviewed PR.
- prepush-rebase-check.sh: process a final ref line with no trailing newline
(previously dropped, which silently reinstated the #719 block), and skip the
stdin read on a TTY so an interactive run does not hang.
- Corrected a citation the review caught: docs/ci-cd.md documents the tag step,
not a release-notes-PR flow. Cite #719 for the observed flow instead.
Also fixed a frontmatter break this round introduced: a `: ` inside the
unquoted `rule:` scalar. PyYAML rejected it while the dependency-free reader
accepted it, so only `scripts/tests` caught it.
Verification: 438/438 script tests pass; decisions-validate OK; PyYAML parses
all three touched records.
refs #743 #719
Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b91707b707 |
fix(719): exempt tag-only pushes from the H11 branch-freshness check
H11 (.claude/hooks/prepush-rebase-check.sh) refuses to push a branch that is behind origin/main. It fired on tag-only pushes too, breaking every release cut: docs/ci-cd.md's "Cutting a release" flow lands a release-notes commit via PR and then tags that merge commit, so the local branch is always one commit behind origin/main at tag time. A tag push cannot revert anyone's merged work, which is the failure H11 exists to prevent, so skip the freshness check when every ref being pushed is under refs/tags/. .husky/pre-push previously consumed pre-push's stdin ref lines and forwarded them only to prepush-donewhen.sh; prepush-rebase-check.sh got none. Forward the captured $_prepush_refs to it too, or the new logic is dead. Guard against the vacuous-truth case explicitly required by #719: "all pushed refs are tags" is trivially true over zero ref lines (manual run, forgotten forwarding), which would silently disable H11 for every push. Require at least one parsed ref line before granting the exemption. Adds scripts/tests/test_prepush_rebase_check_tag_exemption.py using real local git repos (bare origin + a work tree pushed one commit behind it) to exercise git fetch/merge-base/rev-list against a genuinely-moved origin: tag-only allowed, branch-only still blocked, mixed branch+tag still blocked, and zero ref lines still blocked (the vacuous-truth guard). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
691a7acc77 |
fix(743): disable direct pushes to main so review-verdict/h10 is on the only path to main
Gitea evaluates `status_check_contexts` when it MERGES a PR. A direct `git push origin HEAD:main` never consults them, so the whole h10 gate was skippable with no forgery — strictly cheaper than every route enumerated in #697. `main` now carries `enable_push: false`. Measured on this instance (Gitea 1.27.1) against a throwaway `probe-743-*` rule rather than against `main`: enable_push: false -> push by timothy (site admin) REFUSED, pre-receive hook declined enable_push_whitelist + ["timothy"] -> identical push SUCCEEDED That second line is why this is a DISABLE and not a whitelist: #743 offered the two as interchangeable, but the only write accounts here are `timothy` (site admin) and `renovate`, and every credential in the threat model — agent sessions, PATs, the injected GITEA_TOKEN — acts as `timothy`. A whitelist naming `timothy` would have ticked the box and closed nothing. Then demonstrated on `main` itself, per the issue's Done-when: a direct push was refused, and a tag-only push from the same worktree succeeded (tags are governed by `tag_protections`, which is empty). The release cut is unaffected. What this closes: the write-only credential routes — the injected GITEA_TOKEN, RENOVATE_TOKEN, any non-admin collaborator PAT. What it does NOT close: an admin credential can PATCH the protection off, push, and restore it. Recorded as an accepted residual rather than implied to be covered. Also corrects two claims the probe contradicted, and one that the mid-session Gitea upgrade (1.25.4 -> 1.27.1) invalidated: - ci-cd.md and ci.actions-credential-scoping both said "a push whitelist would close more of this class than the 1.26 upgrade". The whitelist form closes nothing here; corrected in place. - ci.actions-credential-scoping's rule said "do NOT add a `permissions:` key while this instance is below Gitea 1.26.0". That precondition no longer holds at 1.27.1, so the directive now misleads. Corrected — while noting the consequence is still UNVERIFIED: `/api/v1/settings/actions` 404s at 1.27.1, so whether `permissions:` binds here was not probed. The upgrade alone is not evidence the constraint works. - That record's 1.25.4 measurements are now dated, not current. Flagged as such rather than silently re-pinned to a version they were never taken on. #743's fourth box (docker-build.yml `persist-credentials: false`) is decided in the record and deliberately not done here: two of its checkout steps run `git fetch ... || true` feeding the changed-file skip logic, so a credential regression would be silent rather than loud. Drop the `|| true` masking first. fixes #743 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
08e95f9ec1 |
fix(697): scope CI's registry credential so head-resolved workflows cannot forge review-verdict/h10 (#745)
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 47s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 47s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 48s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m33s
Closes the credential half of #697. `REGISTRY_PASSWORD` was the admin account's basic auth, handed to head-resolved PR code by docker-build.yml; it is now a PAT scoped `write:package` + `read:repository`. Verified on Gitea 1.25.4: registry push SUCCEEDED, status GET 200, status POST REFUSED 403 (required=[write:repository]). Does NOT close the class. Surviving routes, all recorded: RENOVATE_TOKEN (#742), the injected GITEA_TOKEN (server-management#714), a collaborator's own token, the `v*` tag push, and — making all of them unnecessary — direct pushes to `main`, which are server-side permitted (#743). ci-image.yml's trigger filter was attempted, reverted, and split out as #744. Three cold adversarial review rounds: BLOCKED, BLOCKED, BLOCKED, then MERGEABLE. fixes #697 |
||
|
|
b91939e5c4 |
fix(697): correct the overclaims three adversarial review rounds found
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 22s
PR Gates / Docs update reminder (pull_request) Successful in 26s
PR Gates / decisions lifecycle (pull_request) Successful in 41s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 56s
review-verdict/h10 Review-verdict: MERGEABLE @ b91939e (base: main)
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 59s
PR Gates / Script tests (pytest) (pull_request) Successful in 1m56s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 18m6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 20m3s
Review verdict / Set review-verdict status (pull_request_target) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m4s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Round 1 BLOCKED (1 Blocker, 4 High, 3 Medium, 2 Low); round 2 BLOCKED on the fix (1 Blocker, 2 High, 4 Medium, 2 Low); round 3 BLOCKED on one Medium. Every finding re-verified against the live instance before acting. ROUND 2 — the blocker was self-inflicted and the local gate could not see it. Adding `branches: [main]` to ci-image.yml re-points `ci-image-pin`'s `expected` at the editing commit, staling all five `container:` pins and failing that BLOCKING job — for a change altering zero bytes of the toolchain image. Reproduced: expected=ed9dd6254 vs pins=32747a0. Reverted here (the commit was amended, so no commit on the branch touches that path) and filed as #744. That edit had also FALSIFIED its own justification: branch publishing IS load-bearing — docs/ci-cd.md documents the rebase-recovery flow as "let ci-image.yml publish :<short sha>, then bump the pin", which is how you satisfy ci-image-pin from inside a PR. Reverting also keeps three trigger descriptions true (ci-cd.md:1043, the recovery flow, pr-checks.yml's escape-hatch comment). Also fixed: - gate-trigger-base-resolved.md was the file round 1's fix did not touch, and still said "no workflow route retains human provenance" — false, since a PR-added workflow can reference RENOVATE_TOKEN. Its `rule:` also kept the race framing, and `rule:` is what the catalog and MemPalace mirror. - `mechanics:` claimed "independent review confirmed no CI consumption breaks". It confirmed no such thing. Round 3 then caught the REPLACEMENT sentence making the same class of error: only the `container:` pull is exercised by a PR, because `build` carries `if: github.event_name != 'pull_request'` and cache-to/cache-from live only there. Those and the base-image pull first run on the post-merge push to main — a wrong inference reddens main, not the PR. - A fourth surviving route was unnamed: docker-build.yml publishes :prod from a `v*` tag push and a tag may point at any commit (tag protections are empty). "three surviving routes" became "at least these" — a count reads as complete. - Unmarked inferences, a "three later sections" that undercounted four, a dangling "the two items below", and a #744 rationale that stated the pin toll without its documented remedy. Local gate: 432 script tests pass; `decisions_validate.py --base origin/main --head HEAD` and `build_decisions_catalog.py --check` both exit 0; ci-image-pin recomputed by hand and matching the pinned commit. The record is 62 prose lines against a 60-line ceiling that is a `::warning::` by design (#520) — the blocking constraint is the 2-25% minority band, currently 10.8%. Refs #697, #742, #743, #744. Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e298bb291e |
fix(697): scope CI's registry credential so head-resolved workflows cannot forge review-verdict/h10
`REGISTRY_USER`/`REGISTRY_PASSWORD` were the ADMIN account's basic auth, and
`docker-build.yml` triggers on `pull_request` — head-resolved — so a PR's own
code was handed instance-admin credentials. Basic auth carries no scope, so the
same secret that pushes an image administers every repo on the instance and can
POST `review-verdict/h10`, the required context that makes merge-consent derived
rather than assertable. Refs #697.
Fixed at the credential, not the triggers: patching triggers enumerates
instances of "a ref-resolved workflow obtains status-capable credentials", and
adding a new workflow file is itself a route. `REGISTRY_PASSWORD` is now a PAT
scoped `write:package` + `read:repository`.
Verified on Gitea 1.25.4, not inferred:
- registry push of a probe tag SUCCEEDED (cleaned up, confirmed 404)
- GET /commits/{sha}/status SUCCEEDED (what ci-detect-already-validated.sh does)
- POST /statuses/{sha} REFUSED, HTTP 403:
required=[write:repository], token scope=write:package,read:repository
Scope of what this closes, stated without overclaim. It closes the instance-wide
admin escalation and that credential's durable forgery route — durable because a
status POSTed with a USER credential carries a real `creator` and is inherited as
a human verdict, while an Actions job's carries `creator: null` and is re-derived.
It does NOT close the class. Three things survive it:
- `RENOVATE_TOKEN` is a `write:repository` PAT of a real bot account in the
SAME secret store, so it also posts with non-null `creator`. It cannot be
scoped down (Renovate needs repo write), and secrets are a per-repo store
that any PR-added workflow can reference. Closing this needs the provenance
check tightened to an allow-list of approved reviewers.
- Every job still receives a write-capable `GITEA_TOKEN`. `permissions:` YAML
is a no-op before Gitea 1.26.0 and no `app.ini` lever exists at any version;
only >=1.26 with the Actions default set to Restricted binds it.
Tracked in server-management#714.
- Branch protection binds the context NAME, not its issuer, so any write-scoped
personal token forges the status with genuine human provenance. Unfixable
in-repo. `h10` is a process guard, not a security boundary against push access.
Auditing the secret STORE rather than the workflow set also surfaced
`SERVERMGMT_DEPLOY_KEY`, still present though the `bump-prod-compose` job that
used it was removed in
|
||
|
|
d7647b6104 |
fix(685): AddItemsDialog resolves by search instead of windowing the whole media-library type (#741)
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 38s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 39s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 39s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 12m26s
|
||
|
|
7be42654fe |
fix(685): suppress both empty-states on error; name the addable-kind derivation
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
PR Gates / Docs update reminder (pull_request) Successful in 18s
Review verdict / Set review-verdict status (pull_request_target) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m33s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m21s
review-verdict/h10 Review-verdict: MERGEABLE @ 7be4265 (base: main)
PR Gates / decisions lifecycle (pull_request) Successful in 2m7s
PR Gates / Script tests (pytest) (pull_request) Successful in 2m13s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m7s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 17m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m58s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Round-4 verification returned MERGEABLE with no new defects and no BLOCKER/HIGH/MEDIUM. These are its remaining LOW and nits. - "No results — try a search above." asserts a search that COMPLETED and found nothing, so it was false beside a failed request. Both empty-state messages are now suppressed on error and the role="alert" banner is the whole message, as that test's comment already claimed. Pinned positively and negatively so a refactor cannot satisfy the assertion by rendering nothing at all. - The (typeof ADDABLE_TYPE_LIST)[number] derivation is now the named AddableKind, spelled once instead of twice: a third ingress into the searched kinds is most likely to be written by copying one of the existing two, and the "one list, all ingresses" property should be visible at a glance rather than reassembled. - The §3b lesson cited two measured test counts, which go stale against the very suite they describe — a count taken before the helper had unit tests no longer holds now that it does. Scoped the observation to the sha it was measured on and replaced the counts with the invariant they were evidence for: every gate needs at least one test that reddens when that gate ALONE is removed. - Documented why the error banner stays conditionally mounted while the hint's live region does not: role="alert" is the one live-region role screen readers reliably announce on insertion, so the two are correct for opposite reasons. The review flagged the divergence as unexplained, not as wrong. refs #740 |
||
|
|
d7725c274c |
fix(685): close the second ingress; stop the error path claiming a short query
Verification round returned MERGEABLE with all three blocking findings resolved
by measurement. These are its four remaining items.
- DEFAULT_SEARCH_KINDS was the SECOND ingress into the searched kinds and was
not derived, so the previous commit's "enforced by the type system" claim held
for one of two paths. A non-addable kind there typechecked clean and would
have overstated the hint with every one of its rows dropped — the exact defect
the derivation exists to prevent. Now derived; verified by mutation that
adding 'Collection' to it is a compile error.
- The error path fell into the min-query guidance branch, so a valid 2-character
query that got a 500 told the user to type at least 2 characters. That branch
conflated "nothing searched yet" with "the last search failed". Newly
introduced by the previous commit's error-path reset; now gated on !error and
pinned by a test.
- The aria-live region was mounted conditionally, creating the region and its
text in one commit — which most screen readers do not announce. It is now
mounted unconditionally with the condition inside.
- The §3b lesson mis-stated where the duplicate gate lived: it was inside
runSearch, the genuine single sink, NOT at one of the callers — so the rule as
written ("put the gate in the single sink, not at each caller") described the
revision that was rejected. Reworded to the actual lesson: the gate's home is
the shared helper, and "it's the single sink" is not evidence it is the only
guard. That misreading is why #685 got this wrong twice.
Declined again, with reasons: the NaN pageSize edge (faithful to the sibling
helper), the clamp test's unpinned lower bound (same), and the registry's
disclosed same-identity substitution gap.
refs #740
|
||
|
|
b6bf94f129 |
fix(685): test the helper's bound; delete the masking duplicate gate
Independent review round 2 returned BLOCKED on two findings, both correct. - The helper's bound was dead code to the suite. searchLibraryBrowseItems had zero tests, so deleting its clamp OR its gate left the whole suite green — while the registry note claimed a caller "cannot skip the bound". That is the previous round's finding relocated, not removed. It now has the three tests its sibling searchLibraryPickerOptions already had (clamp, gate, compile), plus one pinning the full-row return that is its reason to exist. - The screen kept a second copy of the min-query check, and the two masked each other: the 1-character boundary test passed with EITHER gate alone, so it pinned nothing. The screen's copy is deleted; the helper is the sole gate. Measured before/after: with the duplicate present, removing the helper's gate left that test green; with it gone, the same removal reddens it. - §3b contradicted itself two lines apart — the parent still said "there is no truncation, so there is no truncation hint" above a sub-bullet mandating one. Reworded so a hint is permitted, required only where bulk selection makes the count actionable. Same correction to the 'search-bounded' definition. - A failed search left results/totalMatches stale, rendering a confident "Showing 75 of 60000 matches" beside the error banner. The catch clears them. - Results now carry a `Results for "<query>"` heading and the guidance is keyed to the settled query, not the live input, so rows are never shown without saying which search produced them. `selected` persists across queries (correct for a multi-select picker); the Add button's count keeps it discoverable. - The hint sums pre-filter totalCount against post-filter rows, which is only correct because every filterable kind is addable. MediaKindFilter is now derived from ADDABLE_TYPE_LIST, making that a compile error rather than prose. - aria-live on the hint; the #740 doc caveat no longer overstates the typeahead rule as a mandate this screen violates. Declined: the NaN pageSize edge (copied faithfully from the sibling helper) and the registry's same-identity substitution gap (already disclosed in that file). refs #740 |
||
|
|
4be3f247d8 |
fix(685): move the picker bound into the helper; surface the per-kind cap
Independent review round 2. Verdict was MERGEABLE with no blockers; this takes
the two recommended fixes plus the structural one it listed as a follow-up.
- The bound was caller discipline, not code: getLibraryBrowseItems does not
clamp pageSize, so the bound was only the constant this one call site chose
to pass, and §3b is explicit that a bound a caller can exceed is not a bound.
New searchLibraryBrowseItems in libraryBrowse.ts owns the min-query gate, the
pageSize clamp and the titleContainsQuery compile, returning full
LibraryBrowseItem rows plus totalCount (searchLibraryPickerOptions' {id,name}
shape loses the mediaType that toAddItemsRequest needs). runSearch keeps one
early return, for the spinner only, and no longer re-implements the gate.
- The min-query guidance was keyed to the LIVE input, so backspacing below the
gate after a search wiped the rendered rows and their checkmarks while
`selected` and the Add button still counted them. Keyed to results.length too.
- "Nothing left to hint at" was false: each kind is still capped at
LIBRARY_PICKER_RESULTS and totalCount was never read. This is a bulk
multi-select add, so the cap is surfaced — per-kind totalCounts are summed and
rendered as "Showing N of M matches" once it exceeds the rendered rows. The
registry note and the §3b bullet are corrected to stop claiming otherwise.
- Gate boundary tested at 1 character (§3b: inclusive endpoints, or a > for >=
slip passes the whole suite).
- The guard test's deviation loop iterates an empty list now, so it gains one
bidirectional assertion that is non-vacuous: the set carrying an `issue` field
must equal the set classified 'deviation'.
- The §3b bullet no longer reads as a conformance certificate: AddItemsDialog
still lacks the seqRef and useIsMountedRef guards §3b mandates. That defect is
PRE-EXISTING, not introduced here, and is tracked in #740.
refs #740
|
||
|
|
28ce8c4dfe |
fix(685): gate AddItemsDialog on a real query instead of windowing the whole type
AddItemsDialog.runSearch was reachable with an empty query two ways — a blank form submit, and a kind-chip click, which called it immediately — and getLibraryBrowseItems omits a falsy `query`, so each path degraded into an unfiltered browse of the whole media-library type (first 50 rows, per kind) presented as the answer with nothing surfacing the truncation. All ten ADDABLE_TYPE_LIST entries are spa-conventions §3b Class B media-library types. The dialog is multi-select, so §3b's SearchPicker (single-select) does not fit; it takes §3b's constraints instead: - no request below LIBRARY_PICKER_MIN_QUERY, enforced in runSearch — the single sink both entry paths route through, not duplicated per caller - typed text compiled with titleContainsQuery rather than forwarded raw (a second latent §3b violation here: the search index's default field does not match bare title words) - each kind bounded to LIBRARY_PICKER_RESULTS - merged.slice(0, 50) removed — it silently dropped up to 100 of 150 fetched rows even for a real query Tests assert zero requests below the gate on both paths, exactly one bounded request per kind above it (20k-row fixture), the compiled+escaped query, and that no fetched row is dropped. Each was verified to fail with its mechanism removed. The pageSize registry entry moves from `deviation` to `search-bounded`. That leaves zero deviation entries, so the anti-vacuity assertion guarding that list is deleted deliberately, per its own instruction. Done-when box 4 (collection-family truncation hint) has no subject: this screen offers no collection-family type. fixes #685 |
||
|
|
e46e2cfe68 |
docs(skill): Dispatcharr EPG refresh ssh'd to the wrong host
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 9m19s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 17m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 19m36s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 9m55s
The refresh_epg_data command targeted 192.168.1.99, but Dispatcharr moved to jazz (192.168.1.29) in #634 — it would fail with 'No such container'. refs server-management#692 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |