7d81f85b38bf5ec66abc2fb76385fee11d1eb18d
123
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7d81f85b38 |
fix(778): make the by-name pin falsifiable — it was filtering on the property it asserted
Confirmation pass returned BLOCKED, and its lead finding is the one worth having.
The test forbidding the by-name lookup recorded URLs from INSIDE the
`endswith("/branch_protections")` branch, so the only URLs it could ever record were
ones that already satisfied the assertion. A by-name request was invisible to the very
test written to forbid it. Cold review proved it by reintroducing the lookup in the
hook: the suite stayed 33/33 green. That is the filter-on-the-asserted-property defect
this PR's sibling record exists to describe, committed inside the guard against it —
and the commit message had called the twin "pinned so it cannot come back".
The recorder now sees every branch-protection URL whatever its shape. Re-verified by
the same mutation: reintroducing a by-name call reddens exactly the two tests that
forbid it.
Also from that pass:
- an HTTP 404 on the LIST read reached the "the full rule list was read and none
matches" deny — a claim about a read that never happened. Gitea answers 404 for a
repo that is absent or invisible to the credential, so the classifier's own verdict
is now the sentinel `nomatch` and HTTP failures reach the ask;
- two comment blocks still described the deleted by-name endpoint as live, one of them
asserting the classifier "is never reached at all";
- the decision record still documented `branch_protections/{base}` and its 404
semantics as the mechanism, in the record this PR authored — now rewritten to the
list endpoint, with why reading the LIST is the load-bearing choice;
- seven assertions on a string the hook no longer emits, and three test
names/docstrings describing the removed 404 flow;
- an unused fixture helper, and 79 lines left over-indented by the removed nesting.
724 tests green.
refs #778
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f3863c3720 |
fix(778): delete the by-name lookup — the precedence check was guarding the dead path
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 20s
PR Gates / Docs update reminder (pull_request) Successful in 19s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 18s
review-verdict/h10 Awaiting review verdict for f3863c3
Review verdict / Set review-verdict status (pull_request_target) Successful in 14s
PR Gates / decisions lifecycle (pull_request) Successful in 21s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m21s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m20s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m2s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
Final review returned MERGEABLE with no Blocker and no High. Its one Medium is taken,
and it is my own recurring trap for the third time in this PR: fix one path, then check
its TWIN.
The hook looked a rule up by NAME first and enumerated the rule list only on a 404.
But `branch_protections/{name}` is an exact DB lookup — `GetProtectedBranchRuleByName`
— which performs no matching and knows nothing about precedence. A 200 from it means
"a rule with this NAME exists and lists this context", never "this context is required
on this branch". So the precedence argument added last round guarded the 404 path while
the 200 path granted without it — and since this repo's rule IS named `main`, the by-name
lookup always returns 200. The hardened code was dead and the unhardened code was live.
Given a rule `main` requiring review-verdict/h10 and a rule `m*` with better Priority
that does not, Gitea applies `m*`; the by-name hit on `main` saw h10 and granted anyway.
Fixed by DELETING the twin rather than documenting it: one fetch of the full list, one
classifier, one argument, no second path to keep in step. Two things fall out for free
— the ref no longer reaches a URL segment, so the percent-encoding hazard is removed by
construction rather than escaped (its test is replaced by one asserting no ref reaches
the URL at all), and every case the classifier already covered now applies to the live
path instead of an unreachable one.
Verified against the live Gitea: the classifier returns `exact` -> rule `main` ->
enable_status_check=true, h10 present. A new test pins that the precedence check runs
even when an exactly-named rule exists, and asserts the by-name endpoint is never
requested, so the split cannot come back silently.
The grant string now states what was actually established — read from the full rule
list, matched with Gitea's own plain-vs-glob split, refusing wherever precedence or
folding is not derivable — rather than the stronger "confirmed required" it claimed
while consulting a single named rule.
724 tests green.
refs #778
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c7bea972d6 |
fix(778): close the last three residuals — ambiguous exact rules, non-ASCII folds, one unpinned test
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
PR Gates / Docs update reminder (pull_request) Successful in 15s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 21s
PR Gates / decisions lifecycle (pull_request) Successful in 27s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
review-verdict/h10 Awaiting review verdict for c7bea97
Review verdict / Set review-verdict status (pull_request_target) Successful in 20s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m17s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m4s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m15s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m41s
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
Confirmation review returned MERGEABLE at
|
||
|
|
356cddbb59 |
fix(778): pin the arm order, and correct a wrong explanation I had just written
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 18s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 27s
PR Gates / Docs update reminder (pull_request) Successful in 14s
PR Gates / decisions lifecycle (pull_request) Successful in 20s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 18s
review-verdict/h10 Awaiting review verdict for 356cddb
Review verdict / Set review-verdict status (pull_request_target) Successful in 44s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m17s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m3s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m12s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m1s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
Two self-found defects while pre-empting the confirmation round's own questions. THE REORDER WAS UNPINNED. Swapping the classifier arms back to exact-first left all 29 tests green, so the previous commit's central change was invisible to the suite — an unproven change shipping under a green run. The missing fixture is the one that distinguishes the orders: a list holding BOTH an exactly-named rule that requires `review-verdict/h10` AND a glob rule that could also govern the base and does not. Exact-first inspects the rule that requires h10 and auto-grants; undecidable-first asks. Now mutation-proven in both directions. I HAD MISDIAGNOSED THE TRAP, and asserted the wrong cause in a comment. A three-line repro disproves "an EXIT trap suppresses output" — it does not. The real mechanism is that this file already owns its EXIT trap: `scripts/hook-fire-log.sh` installs `trap 'etv_hook_fire_end "$?"' EXIT` (#776), and in capture mode that handler is what REPLAYS the decision JSON to stdout. A second `trap ... EXIT` silently replaces it, so the decision is captured and never emitted. The trap slot is a single shared resource and the sourced library claimed it first. That second one is the shape this whole PR is about, committed against my own work: an explanation that fits the symptom, is written down as fact, and is wrong. It would have told the next person the construct is unusable rather than that the slot is taken. refs #778 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d8ff6fa229 |
fix(778): ask before trusting an exact rule, and make the tests tell classified from crashed
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 15s
PR Gates / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 22s
PR Gates / decisions lifecycle (pull_request) Successful in 22s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
review-verdict/h10 Awaiting review verdict for d8ff6fa
Review verdict / Set review-verdict status (pull_request_target) Successful in 14s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m55s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Canceled after 7m39s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 0s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 0s
Build ErsatzTV Image / 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
Ninth cold review: MERGEABLE, no Blocker, no High. Its three Mediums taken anyway,
because each was a one-line fix retiring the last "asserted rather than verified"
surface in the file whose whole subject is that shape.
M1 (backslash missing from the metacharacter class) was already closed in
|
||
|
|
cd1b286378 |
fix(778): backslash is a metacharacter too, or the none arm's proof does not hold
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
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 17s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 14s
review-verdict/h10 Awaiting review verdict for cd1b286
Review verdict / Set review-verdict status (pull_request_target) Successful in 16s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m12s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m56s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m18s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m49s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Self-found while stress-testing the superset claim I introduced one commit earlier —
the crux the new classifier's safety rests on.
`none` authorises a DENY on the stated grounds that nothing can possibly govern this
base, so its premise must hold unconditionally, not usually. The superset was "literal
prefix + .* + literal suffix", which is sound for every glob dialect EXCEPT one case:
gobwas/glob reads `\{` as a LITERAL brace, so a rule `a\{b` governs the base `a{b`,
while a superset treating `\` as an ordinary character builds `a\.*b`, misses, and
denies a base that is in fact protected.
Verified before and after: with `\` outside the metacharacter class the classifier
answered `none` for that pair; with it inside, `undecidable` -> ask. 18 adversarial
rule/base pairs (brace alternation, negated and ranged classes, `**`, leading and
trailing metacharacters, unicode, empty alternation) all answer `undecidable`, never
`none`, so no dialect-matching case falls through the deny arm.
Git ref rules make this nearly unreachable — a branch name may not contain `*`, `?`,
`[` or `\` — but `{` IS legal in one, and "nearly unreachable" is not the standard for
the arm that issues a deny. Checked with `git check-ref-format` rather than assumed.
refs #778
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
456042ee51 |
fix(778): stop claiming parity with Gitea's glob matcher — classify, or ask
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 27s
PR Gates / Docs update reminder (pull_request) Successful in 29s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 23s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 15s
PR Gates / decisions lifecycle (pull_request) Successful in 28s
review-verdict/h10 Awaiting review verdict for 456042e
Review verdict / Set review-verdict status (pull_request_target) Successful in 15s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m56s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Canceled after 7m16s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 0s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 0s
Build ErsatzTV Image / 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
Eighth cold review: no Blocker, no High. Three Medium, two Low, one Nit.
MEDIUM — the substantive one. The glob fallback asserted it matched rules "the same
way Gitea applies them", and it does not. Gitea compiles a rule name with gobwas/glob
and a `/` separator: its `*` does NOT cross a slash, `?`/`[…]`/`{a,b}` are wildcards,
and a plain name is folded case-insensitively. Mine used `.*` for `*` and escaped the
rest. The divergence has a false-OPEN direction — `release/*` does not govern
`release/26/hotfix` in Gitea, but `release/.*` matched it here, which would auto-grant
a scheduled merge on a base where the check is not required. That is #622's hole,
reached through the block written to close it, via exactly the failure this PR
records: a claim about an external system asserted rather than verified.
Reimplementing somebody else's glob dialect would be a second copy of a parser, which
this repo has already withdrawn a change for. So the classification is three-way and
each arm is safe WITHOUT knowing the dialect: an exact non-glob name folded
case-insensitively is decidable; a glob rule that could govern the base is
UNDECIDABLE and asks; and "could" is tested with a provable superset of any glob
dialect — literal prefix, `.*`, literal suffix — so if even that cannot match, no
dialect can. Over-matching would grant on unestablished protection; under-matching
would deny with a false cause. Asking is the only answer honest in both directions,
and it is rare: this repo's rule is the plain name `main`.
MEDIUM — a count that was wrong the moment it was written ("46 of the 69 rows are
N/A"; it is 44). It was added by the same commit that demoted two rows. That is the
FOURTH stale number in this change, in the deliverable whose own record argues against
hand-maintained counts. Removed rather than corrected, with the reason stated.
MEDIUM — `migration-smoke.sh` still said "Same shape" as `security-scan.sh`, whose
note had just been rewritten to the opposite conclusion, so the backreference had
silently inverted. It is the same pull-then-run over a mutable tag and deserves the
grade MORE, not less: `security-scan.sh` boots a throwaway container and authorizes
nothing, while this gates a production stack recreation. Regraded.
LOW/NIT: an `end <= start` guard that `str.index(…, start)` makes unreachable by
construction is replaced with the reachable failure it was describing; a docstring
still named a fixture from an earlier draft; a reflowed `#707.` was line-initial.
Two mutations were ineffective on the first attempt — one left the `decide ask`
continuation in place, the other had no test covering case-folding. Both redone; each
arm now reddens a named test.
Decisions-Edit: yes
refs #778
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
7db4101b24 |
fix(778): the escape emitted TWO backslashes, turning a false-open into a false DENY
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
PR Gates / Docs update reminder (pull_request) Successful in 10s
PR Gates / decisions lifecycle (pull_request) Successful in 24s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 25s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 21s
review-verdict/h10 Awaiting review verdict for 7db4101
Review verdict / Set review-verdict status (pull_request_target) Successful in 28s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m56s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m37s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m14s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m8s
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
Seventh cold review. One High, one Medium, three Low, seven Nit — all in the two newest commits, which is where every round of this PR has found its defects. HIGH, and it is my own fix from the previous commit. In jq source `"\\\\"` decodes to TWO backslashes, so escaping produced `\\.` — "a literal backslash, then any character" — instead of an escaped dot. Every rule name containing a metacharacter became UNMATCHABLE, and a rule named `a[b` crashed jq outright (swallowed by `|| true`). Verified: `release/26.*` no longer matched base `release/26.4`, so the fallback found nothing and hard-DENIED with the stated cause "has NO branch protection at all" — converting a false-open into a false deny, which the block's own comment calls the worse outcome. One character: `"\\" + .c`. Correct across 14 rule/base pairs. WHY MY TEST MISSED IT, which is the transferable part: it asserted only the NEGATIVE direction (`mai.` must not match `main`). A rule matched literally and a rule made unmatchable both fail to match the wrong base, so the assertion passed for the wrong reason. Only a rule that SHOULD match separates them, and there was no positive control. There is now — plus a char-class case — and both go red against the over-escaped version. That also needed a base containing a dot: a rule cannot carry a metacharacter and still match `main`, so the first attempt at the positive control was unsatisfiable by construction. MEDIUM — four live claims that the population "derives from the filesystem", left standing by the commit that replaced that mechanism: the guard's own docstring 45 lines above a comment shouting the opposite, the inventory heading 21 lines under "Every git-tracked file", the docs/README entry, and — worst — the record's `mechanics:` frontmatter, which is the copy the catalog and MemPalace mirror, so discovery would have returned the superseded lesson. All corrected. LOW/NIT: the URL-encoding test grepped the source for `@uri` (it now asserts the URL actually requested, and reddens when the encoding is removed); the hoist comment said "every path below" without noting the docs-only enumeration above it (bounded — that path is a passthrough to a human prompt, never a grant); a now-unreachable guard is annotated rather than left reading as live; `issue-qualification-audit.sh` was `N/A` while `select-queue.sh` was `UNSAFE-KNOWN` on the same argument, and `security-scan.sh` claimed "one step" for a pull-then-run over a mutable tag — both regraded; the `PINNED` definition now says what separates its second shape from an `N/A` "one step" row (the identifier's immutability, not the step count); the section parser raises a message naming both required headings instead of a bare ValueError; and the record's body is rewrapped. Decisions-Edit: yes refs #778 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
01de6cbd8b |
fix(778): * is the only wildcard — a rule name's regex metacharacters were live
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 21s
PR Gates / Docs update reminder (pull_request) Successful in 14s
PR Gates / decisions lifecycle (pull_request) Successful in 24s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
review-verdict/h10 Awaiting review verdict for 01de6cb
Review verdict / Set review-verdict status (pull_request_target) Successful in 32s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m51s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 4m27s
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
Found by attacking my own glob fallback from the previous commit before the reviewer got to it, which is the round where this PR's defects have landed every time. The 404 fallback matched a branch-protection rule by substituting `*` into a raw regex, leaving every other metacharacter active. Verified directly: a rule named `main.x` matched the base `mainax`, and `a+b` matched `aab`. The direction is the one that matters — a spurious match to some OTHER rule that happens to require `review-verdict/h10` reports this base as protected when nothing governs it, so a consent gate answers yes on evidence about a different branch. Each literal segment is now escaped before the pattern is assembled, so the wildcard survives and nothing else does. Verified across 11 rule/base pairs: metacharacters are literal, `*` still spans, exact and non-matches unaffected. The regression test needed two goes to stop being vacuous, both times for the same reason the rest of this PR keeps hitting: the stub never 404'd for the new mode, so the run denied earlier via the by-name lookup and never reached the fallback at all. It now goes red against the unescaped predecessor. refs #778 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4261f76dd2 |
fix(778): derive the population from git, not the disk — the guard was red on every dev checkout
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 22s
PR Gates / Docs update reminder (pull_request) Successful in 17s
PR Gates / decisions lifecycle (pull_request) Successful in 24s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 10s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Canceled after 45s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 0s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Canceled after 0s
Build ErsatzTV Image / 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
review-verdict/h10 Awaiting review verdict for 4261f76
PR Gates / Script tests (pytest) (pull_request) Canceled after 19s
Review verdict / Set review-verdict status (pull_request_target) Successful in 20s
Sixth cold review (a different reviewer, in-repo, worktree-isolated after the cross-family runs wedged twice on their sandbox). One High, one Medium, two Low, two Nit. All fixed. HIGH, and it is the third time this population has been wrong. `rglob` is recursive, so it also enumerated `.husky/_/` — 17 husky shims generated by `npm ci` via web/package.json's `prepare`, gitignored and untracked. The guard therefore derived 76 files against a 59-row table and was RED on every checkout that has run `npm ci`, while staying GREEN in CI, whose `script-tests` job checks out and pip-installs but never runs `npm ci`. A guard that fails everywhere except where it runs is the fastest possible route to "that test is always broken, ignore it" — on the artifact whose entire thesis is population correctness. Reproduced, then fixed at the source rather than with a fourth traversal patch: the population now comes from `git ls-files`. The index is authoritative, identical for CI and every checkout, and excludes untracked build output by construction instead of by an exclusion list someone must maintain. That is what this PR's own record says to do; the first three attempts each derived from whatever happened to be on disk. Three tests go red against the rglob predecessor. MEDIUM — twin-missed, in the fix from the previous round. Round 4 re-read the base before the branch-protection lookup, inside the scheduled branch only, leaving the #632 retarget DETECTION still reading the top-of-hook snapshot. The reviewer demonstrated it with this PR's own fixture: scheduled+retarget denied while immediate+retarget AUTO-GRANTED. The re-read is now hoisted above every base-dependent consumer, so one read serves both paths, and the duplicate is gone. Note for the record: the hoist is the load-bearing part — once `live_base` is fresh, #632's own comparison catches the retarget too, so the explicit deny only bites when no verdict records a base. The tests are scoped to exactly that case, because as first written they passed under mutation. LOW — a 404 from `branch_protections/<ref>` does not prove the branch is unprotected. Gitea keys that endpoint on the RULE name, so a base covered by a glob rule 404s while being fully protected, and an unencoded ref containing `/` (`release/26.4`) 404s because the path is malformed. Both produced a hard deny stating a specific, false cause — and a deny blocks outright rather than prompting. The ref is percent-encoded, and a 404 now consults the rule list before denying; an unreadable list asks. LOW/NIT — the scope prose attached the extension restriction to `scripts/` alone while the guard applied it everywhere (a `.py` hook would have joined the described scope and acquired no row); `.yaml` workflows are now in scope too. The `PINNED` definition required re-validation, which two legitimately-pinned rows do not do because their check and use are one step over an immutable event-payload sha. Row ordering restored. And once more, the recurring one: adding a scope TABLE to the doc made three prose rows parse as inventory sites — the parser reading its own documentation as data, the same defect as the UNSAFE-KNOWN check that once parsed the paragraph defining UNSAFE-KNOWN. Row parsing is now bounded to the inventory section explicitly. refs #778 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f1e9cce4ae |
fix(778): round 4 — the guard was checking a stale base, which is the defect it enforces against
PR Gates / decisions lifecycle (pull_request) Successful in 19s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 26s
PR Gates / Docs update reminder (pull_request) Successful in 23s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 21s
review-verdict/h10 Awaiting review verdict for f1e9cce
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 15s
Review verdict / Set review-verdict status (pull_request_target) Successful in 36s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m44s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m27s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m48s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m34s
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 6s
Fifth cold review: no Blockers, no Highs. 1 Medium, 1 Low, both fixed here. It also confirmed the #803 deferral is sound and re-verified every count (59 files, 69 rows, 3 PINNED; guards 34/5/15, proofs 8/6/20). MEDIUM — and it is the one worth the round. The branch-protection lookup used `$base_ref` from the PR snapshot taken at the TOP of the hook, and everything between is round trips (the file enumeration alone can be forty pages). A PERSISTENT retarget in that gap needs no ABA and no force-push: the lookup names the OLD base, confirms `review-verdict/h10` on a branch the PR no longer targets, and grants a scheduled merge onto one that may require nothing. The guard written to enforce "checking a stale identifier is not checking" was doing exactly that. The base is now re-read and compared immediately before the lookup; a move denies and names both branches. Mutation-proven. LOW — my caveat erred in the rare direction, understating a clause instead of overstating it. The scalar-row test's docstring called the whole `.statuses` member validation defence-in-depth because the #632 block masks it. That block validates `.context` and `.description` but NOT `.status`, so an object row with a numeric status passes it and does reach the new validator — where without the clause it becomes `vstate=7` and is reported as "the verdict is '7'" rather than as an unreadable payload. The caveat is now scoped to the payload rather than the clause, and the reachable case has its own test, also mutation-proven. The inventory row now conditions the guarantee on BOTH of its preconditions — the PR still targeting that base (fixed here) and the protection still standing (cannot be closed here, and said so). refs #778 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8e02b9961d |
fix(778): round 3 — fix the overclaim I left in the code, and defer the pre-existing ones to #803
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 18s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 19s
PR Gates / Docs update reminder (pull_request) Successful in 22s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 14s
PR Gates / decisions lifecycle (pull_request) Successful in 26s
review-verdict/h10 Awaiting review verdict for 8e02b99
Review verdict / Set review-verdict status (pull_request_target) Successful in 17s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m9s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m57s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m53s
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
Fourth cold review: no Blockers, 2 High / 2 Medium / 2 Low. It independently re-derived the population (59 files, 59 sites, 69 rows, 3 PINNED) and verified every numeric and factual claim in the diff, including the corrected confinement rationale. THE ONE THAT STINGS. The grant reason string still said a commit pushed before Gitea merges "will clear it and block the merge" — the exact sentence the new decision record quotes as THE overclaim this issue exists to remove. I documented it in three files and left it in the code a human actually reads. It now states the guarantee and its condition: the required check was confirmed rather than assumed, and it holds while that branch protection stands. FIXED HERE (all in files this PR already touches): - enable_status_check is validated as a BOOLEAN. `"true"` is not `true`, and comparing the string to `true` produced a confident deny from a payload never understood — the tri-state collapsing to two, the same defect as the contexts shape one line down. - `.statuses` members are validated, not just the array (see the honest caveat below). - the docs-reminder N/A rationale said "the job cannot fail and never reaches the combined status", which is false — any job's status joins the combined state. The true, narrower reason is that its fetch and diff are failure-swallowed, so the remote read can only change the warning's wording. - docs/README names the scripts/tests exclusion in BOTH statements. A VACUOUS TEST, CAUGHT BY ITS OWN MUTATION PROOF. The regression case for the `.statuses` member validation stays GREEN against the predecessor: the #632 base-retarget block runs first and already validates every member it consumes, so it catches the payload before the scheduled branch is reached. The two guards overlap — duplicate guards masking each other, again — which makes that finding LATENT, not live, and my added clause defence-in-depth rather than a fix. The test now asserts the observable contract (a decision is always emitted) and says plainly that it is not a mutation proof of the newer clause. Shipping it as one would have been the exact grade inflation round 2 rejected. DEFERRED to #803, with the reason stated rather than implied: a head-ABA (force-push H1 -> H2 -> H1 during pagination) defeats pr-changed-files.sh, and three OLDER contracts still assert more than the new inventory rows do. That residual predates #778 and lives in #707's mechanism; correcting another active decision record inside a PR already at four review rounds is how a scoped change stops being reviewable. The inventory rows are accurate today and now point at #803. refs #778 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
25048d9ea4 |
fix(778): round 3 — the fence covers BASE aliasing only, and stop citing a moving count
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 20s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
PR Gates / decisions lifecycle (pull_request) Successful in 25s
PR Gates / Docs update reminder (pull_request) Successful in 21s
review-verdict/h10 Awaiting review verdict for 25048d9
Review verdict / Set review-verdict status (pull_request_target) Successful in 19s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m1s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m33s
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) Canceled after 22s
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
Third cold review: no Blockers, 1 High / 5 Medium / 1 Low / 1 Nit. All accepted. It independently re-derived the 59-file population and matched it against `find`, so the traversal that was wrong in rounds 1 and 2 is now verified rather than argued. HIGH — the ABA claim was too broad. `ci.verdict-write-retarget-fence` counts `change_target_branch` events, so it fences the BASE axis and nothing else. A force-push H1 -> H2 -> H1 during pagination leaves the final `.head.sha` comparison equal while the middle pages were enumerated against H2, and no counter moves. Two rows implied the fence covered that; both now state the head residual as real and unfenced, with what closing it would take. Also: the record still said the scheduled-merge residual was "closed one layer down" by the branch protection an admin may have removed — the circular sentence that was rewritten in the inventory last round and left standing in its twin. The hook header still called an immediate merge "sound". Both now describe the bounded window. `docs-reminder` was over-demoted by grouping it with `decisions-guard`: it cannot fail its job, so it authorizes nothing and is N/A, while `decisions-guard` reaches the combined status. Split, per this file's own rule that differing classifications get separate rows. Over-demotion is a defect too — it makes the column noise. The scope heading and the docs/README entry now name the `scripts/tests/` exclusion explicitly, so nobody adds a remote-reading test executable expecting a red guard that stays green, and a wrong limit cross-reference is corrected. The exclusion's justification was also factually false — it claimed the only network calls were to PATH stubs, but test_hook_fire_log.py starts a real http.server on 127.0.0.1 and drives it with real curl. The exclusion stands on confinement, not on absence, and now says so. COUNTS. "4 of 68 PINNED" was wrong (3), and rewriting it as "3 of 68" went stale in the same commit when splitting a row moved the denominator to 69 — three stale figures in three rounds, in the record warning against exactly this. The exact denominator is gone: three rows survive as PINNED out of roughly seventy, and the load-bearing claim is "almost nothing is pinned", not an integer. A hand-maintained count is a second copy of the table; guard-inventory.md gives its counts an equality check because they ARE the point, and a rationale record should not pretend to. refs #778 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
84b4c11188 |
fix(778): round 2 — recursive population, and grade down five more overclaims
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 22s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 14s
PR Gates / Docs update reminder (pull_request) Successful in 23s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
PR Gates / decisions lifecycle (pull_request) Successful in 24s
review-verdict/h10 Awaiting review verdict for 84b4c11
Review verdict / Set review-verdict status (pull_request_target) Successful in 22s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m10s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m22s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 5m4s
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
Second cold review: no Blockers, 4 High / 1 Medium / 2 Low. All accepted. POPULATION, WRONG A SECOND TIME. Round 1 removed a content filter that had omitted `git fetch`. Round 2 found the replacement traversal used non-recursive `Path.glob`, so four nested files were still outside it — including scripts/scripted-schedules/entrypoint.py, which calls get_context() against a live ErsatzTV server and then drives define_content/reset_playout/build_playout off the result. Now rglob, with scripts/tests/ as the single stated DIRECTORY-level exclusion (a scope choice, reviewable in one line; not a predicate over content). Population 55 -> 59, rows 63 -> 68. The generalisation is in the record, because the deliverable made the same mistake twice: the scope may be hand-written, but anything narrowing the POPULATION has to be executed and its output compared against the filesystem — the members it drops are invisible by construction. That is the #774 rule turned on the artifact meant to enforce it. FIVE MORE OVERCLAIMS GRADED DOWN. Both merge-consent head/base rows (the hook returns `allow` and a separate call merges, so the window is small, not absent — "no async window" was simply false); the release smoke pull and the ci-image verify (the concurrency group is PER-REF, so a branch build and a tag build of one commit can publish the same :<short-sha>); and the workflow base-fetch rows, which are not advisory — the merge hook reads the COMBINED status, so any red context blocks the auto-grant. Also fixed a stale cross-reference where the enumeration row still said it "inherits that row's pins" from a row graded down in the same commit. Four PINNED rows survive out of 68. That ratio is the honest finding. CIRCULAR JUSTIFICATION REMOVED. The scheduled-merge row said its residual was "closed one layer down" by the very branch protection an admin may have removed. It is not closed, it is BOUNDED by a trust assumption, and the row now says so. Low: jq's `//` fires on `false` as well as null, so `status_check_contexts: false` was defaulted to [] and produced a confident deny from a shape never understood — absent and null are now defaulted explicitly, everything else is "unknown". And the title sentence claimed "every executable in this repo" while the guard covers four directories; both it and the docs/README entry now say what is actually enforced. refs #778 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a0849eec58 |
fix(778): address the cold review — drop the population filter, grade down three overclaims
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 18s
PR Gates / decisions lifecycle (pull_request) Successful in 24s
PR Gates / Docs update reminder (pull_request) Successful in 9s
review-verdict/h10 Awaiting review verdict for a0849ee
Review verdict / Set review-verdict status (pull_request_target) Successful in 17s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 13s
PR Gates / Script tests (pytest) (pull_request) Successful in 3m58s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m43s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m29s
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 6s
Independent cross-family review (Codex, cold brief, read-only worktree) returned BLOCKED with 9 findings. All 9 accepted; #5 partially, keeping one row PINNED with its justification stated. POPULATION (the finding that mattered most). The derivation filtered the scope by an outbound-network token list and argued that was a scope choice rather than a population filter. It omitted `git fetch` — this repo's most common remote read — so prepush-rebase-check.sh, which fetches origin/main and derives a PUSH DECISION, was structurally invisible to a guard claiming to cover "every executable that reads live remote state", along with three others. The defence offered was that over-inclusion is the safe direction; the filter also under-included. The content filter is gone: the population is now all 55 files in the scoped directories, and a file that reads no remote state carries an explicit N/A row. OVERCLAIMS, graded down. Three rows asserted more than the code does: - the scheduled-merge path was PINNED while the hook's own comment concedes the branch-protection read pins nothing → UNSAFE-KNOWN, "preflight, not a pin"; - pr-changed-files.sh was PINNED and claimed "any movement fails", but before-and-after equality is ABA-vulnerable (main → scratch → main) → UNSAFE-KNOWN, pointing at the caller-side event-count fence that does close it; - the CI toolchain image was PINNED on a mutable TAG, against this file's own definition naming a digest → UNSAFE-KNOWN. The release smoke pull stays PINNED: it pulls the tag the same concurrency-serialized job just pushed. The guard-inventory MUTATION regrade is reverted to BEHAVIOUR-ONLY (8/6/20). The review is right on species: the test feeds the real script an input the clause rejects, which this table explicitly defines as behaviour-only and has already regraded three rows for. A manually-executed disarm does not change what the test is. TWO REAL FAIL-OPENS FIXED: - jq `index()` on a STRING is substring search, so a status_check_contexts arriving as "prefix-review-verdict/h10-suffix" answered yes and would auto-grant. Membership is now exact equality over a value first proven to be an array of strings. - post-review-verdict.sh guarded both re-read comparisons with `[ -n "$x" ] &&`, so a 2xx body that merely omitted .head.sha or .base.ref made the check a no-op and the status was posted having confirmed nothing. That second fix carries a lesson worth the line: the obvious mutation (disarm the new `-z` arm) stays GREEN, because the unconditional `!=` also rejects empty — the two overlap, exactly the duplicate-guards-mask-each-other shape. The proof is taken against the REAL predecessor with the `-n` conjunct restored, which goes red showing returncode=0 and a status written. Also: 404 is now separated from 403/transport (an unprotected branch is the strongest form of the finding; `curl -sf` collapses both to an empty string), and the positive control asserts the decision is `allow` and that the endpoint was actually reached, rather than the absence of one phrase. refs #778 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fe578c5864 |
feat(778): name the check-and-use race, and verify the protection the merge gate rests on
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 20s
PR Gates / Docs update reminder (pull_request) Successful in 24s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 26s
PR Gates / decisions lifecycle (pull_request) Successful in 31s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 10s
review-verdict/h10 Awaiting review verdict for fe578c5
Review verdict / Set review-verdict status (pull_request_target) Successful in 14s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m19s
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) Canceled after 4s
#773 §3 Family D is the one class its taxonomy had no bucket for: a check and the action it authorizes are separated in time over state that can change in between, with nothing pinning a version (#536, #622, #632, #706, #707). The repo had already solved this twice without noticing it was one problem — a compare-exchange claim in-process (ffmpeg.work-ahead-slot-atomic) and RFC 7232 If-Match across /api/v1 (concurrency.ifmatch-rfc7232) — and then solved it a third time from scratch for the tooling at #706/#707. Hence a class-level record rather than a fourth per-instance one. What the enumeration actually found, which none of the five records predicted: the merge-consent hook's scheduled-auto-merge path is safe only because `review-verdict/h10` is a REQUIRED status check on main. That is branch-protection CONFIG, it lives outside this repo, nothing compared the two, and the hook asserted it in a comment AND in the grant reason a human reads. Switch the context off and every word of that sentence is false while the hook keeps printing it and keeps auto-granting. The hook now reads the branch protection and treats it as three outcomes: present proceeds, unreadable asks, absent denies. Two defects were caught by the new checks themselves rather than by review: - the population test found .gitea/workflows/dependency-scan.yml absent from the first draft of the inventory (a sixth workflow the recon slice never listed); - self-review found the guard denying with a confident wrong reason when jq errors one level down on a malformed contexts member, so the word is now matched exhaustively rather than compared against "yes". Same swallow that survived the first fix in the #632 base-change guard. Detector D has no plausible linter, so the detector is detector A applied to an enumerated inventory: docs/remote-state-inventory.md classifies every in-scope executable, and scripts/tests/test_remote_state_inventory.py derives the population from the filesystem and asserts set equality both ways. Deferred with reasons stated in the inventory: select-queue.sh (advisory, authorizes no write), ci-detect-already-validated.sh (skip elides re-validation only, the image still builds), review-verdict.yml's status POST (Gitea offers no conditional write; already fenced by #706's retarget counter). Mutation proofs witnessed for both new guards, clause-level, not whole-file. fixes #778 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
efc34a3481 |
fix(688): pin p95's inclusivity; drop a stale ratio and hedge the gap width
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 20s
PR Gates / Docs update reminder (pull_request) Successful in 32s
Review verdict / Set review-verdict status (pull_request_target) Successful in 11s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m34s
PR Gates / Script tests (pytest) (pull_request) Successful in 1m42s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 19s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 22m10s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 22m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
PR Gates / decisions lifecycle (pull_request) Successful in 14s
review-verdict/h10 Review-verdict: MERGEABLE @ efc34a3 (base: main)
Round 7's second reviewer returned MERGEABLE on the previous head after re-measuring every figure and running a 48-mutant battery — and reported ZERO wrong or unverified numbers, which ends this branch's five-commit streak of them. It also independently confirmed the round-6 adjudication: at `f394d6ce`, the sha the record cites, the #620-era distribution really is n=167, min 2, median 26, p90 52, next value 83. All five figures correct as written. This commit clears its four non-blocking items. - `marks_tail`'s UPPER inclusivity was the last meaningful surviving mutant: `ceiling <= p95` mutated to `<` survived the whole suite. Notice-only rather than blocking, but an unpinned boundary is how a documented claim quietly stops being true — the same defect the previous commit fixed for the coarse band. Both ends now pinned; verified the mutant fails. - "the largest by ~1.6x" was TRUE at `f394d6ce` (230/147 = 1.56) and is stale today (230/198 = 1.16). Unlike the consolidation table two paragraphs down, that sentence was never scoped to a sha — so rather than re-pin a number that will rot again, it now just says "the longest", which stays true however the tail moves. - The validator docstring asserted the 60->81 gap flatly; a 70-line record existed as recently as `8f6d4f443^`, so the gap's WIDTH is more volatile than that implied. Hedged to say it is the shape as measured today, not a constant. Nothing asserts it either way. - Rewrapped a mid-sentence line break left by the previous commit. Three surviving mutants are accepted and left: the crosscheck's not-a-mapping branch is unreachable from any fixture, the None -> "" normalisation only matters for an explicit YAML null no record has, and `_frontmatter_block` returning "" instead of None is a downstream no-op. Verification: 432 scripts/tests pass; ruff at baseline parity (47, and `ruff format --check` at parity 9/9); validator exits 0 with no drift notice; corpus at p90=60, 18/183, calibrated. Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0ff9671393 |
fix(688): pin the minority band's constants and inclusivity; two prose corrections
Review round 7. Its adjudication of the round-6 dispute went the branch's way — measured
at `f394d6ce`, the sha the record actually cites, the #620-era distribution is n=167,
min 2, median 26, p90 52. Round 6 had measured `fefd11dff` (p90 57), a different tree.
The number stays as written.
BLOCKING FINDING: the 2%/25% constants and their inclusive boundaries were not pinned at
all. Mutating 0.02 -> 0.03, 0.25 -> 0.30, or either `<=` to `<` passed all eight
calibration tests. Those are not free parameters — they ARE the documented CI-red
thresholds, so a silent shift would quietly falsify the 38/718 figures in
docs.corpus-size-signal and docs/ci-cd.md (a strict cap reds after 37 long additions, a
strict floor after 717 short ones).
test_the_minority_band_BOUNDARIES_are_exactly_where_documented pins all four. It uses
100-record fixtures so k over the ceiling IS k%, and both 2/100 and 25/100 are exactly
representable and compare equal to the constants — true boundary cases, not near-misses.
Verified by mutation: all four now fail it.
PROSE
- corpus-size-signal said what stays blocking is "what routine growth cannot break",
immediately before explaining that 38 routine additions break it. Now "what no SINGLE
ordinary addition can break", which is what is actually true.
- docs/ci-cd.md said the fine claim is "never asserted"; it is never asserted AGAINST THE
LIVE CORPUS, and IS asserted on synthetic distributions the tests own. Corrected — the
distinction is the whole design.
Correction to an earlier commit message in this branch (
|
||
|
|
2ff52d4236 |
fix(688): make the n oracle dynamic; correct a tense that asserted false history
Review round 6: one MERGEABLE with non-blocking prose, one NOT-MERGEABLE with a real test defect. Both addressed. THE n PIN DID NOT PIN ANYTHING. `assert n == 10` was checked against a fixture holding exactly ten records, so a mutation returning a constant 10 for EVERY input satisfied it — while changing the live denominator from 183 to 10, which is precisely the production defect the test was added to close. A single hardcoded count cannot tell "counts the input" from "returns this number". Now a dynamic oracle at two distinct cardinalities; verified the constant-n mutation fails it. "MOVED p90 by 21 lines" asserted a history I had not measured. 21 is TODAY's gap (60 -> 81). The actual #672 event was smaller — at that tree p90 was 60 with the next value 83, so the 62-line record moved p90 to 62 and reddened CI with a 2-line move. The capability claim is what matters and is true at both refs; the past tense was not. Changed to "can move" in the two places that asserted it, which also makes all four sites agree with docs/ci-cd.md and the validator docstring, both of which already said "could". A REVIEW FINDING I REJECTED, having measured it. Round 6 called "p90 52" wrong for the #620-era distribution, measuring 57. That measurement is at `fefd11dff`; the record cites `f394d6ce`, and at THAT sha p90 is exactly 52 (n=167, min 2, median 26). The number is correct as written and is unchanged. Recording the disagreement rather than silently keeping it: the reviewer measured a different tree than the one the claim names. Also corrected in this branch's own commit message trail: `b24c51ab5` said origin/main has three 59-line records; it has four 59s and two 60s (HEAD: four and three). The claim that survives, and the only one the code and docs now make, is that NOTHING sits between 61 and 80 at either ref — verified independently at both. Cosmetics from the same round: a dangling modifier in ceiling_calibration's docstring, a test_decisions_lib assertion message that said "field(s) differ" when faults can now also be rejections, and a sentence in corpus-size-signal that named the replacement test without saying what it asserts. Verification: 431 scripts/tests pass; ruff at baseline parity (47); validator exits 0 with no drift notice; corpus at p90=60, 18/183, calibrated. Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b24c51ab51 |
fix(688): stop enumerating multiplicities, pin n and the keyless filter, de-couple the vacuity floors
Review round 5. One reviewer returned MERGEABLE with prose findings; the other found four more, two of them real test gaps. Both are addressed here. THE MULTIPLICITIES WERE WRONG AGAIN — fourth commit running. The "measured" sequence 59, 59, 60, 60 -> 81 is measured nowhere: origin/main has 59, 59, 59, 60, 60 and HEAD has four 59s and three 60s. I had even tagged it `(measured)` in a canonical decision record. So this stops enumerating them. All four sites now state only the load-bearing, stable fact: the lengths climb to the ceiling and then jump STRAIGHT to 81 with nothing in between, so one record moves p90 by 21 lines. The multiplicities change with every record added; the gap is the point. This is the same "fix the boundary, not the site" move the tests got three rounds ago, applied to prose that had failed four times. TEST GAPS - Deleting the over-tight test removed the only pin on CeilingCalibration.n: a mutation returning n=1 passed all 19 relevant tests while printing a wrong denominator in the drift notice. Pinned. - The `if r.key` filter was load-bearing in production and unpinned: main() passes the UNFILTERED list (194 entries, 11 keyless, one a 106-line "Records formerly in this file" scaffolding block), while every test handed the function a pre-filtered list — oracle and production agreed only by accident. Pinned. - The --record-ceiling 0 arm's claim that it "cannot go vacuous for any non-empty corpus" was FALSE: an empty record body is validator-valid and record_prose_lines returns 0, so a corpus of empty-bodied records has no offender at 0. Now -1, which makes the claim true. - The three `len(recs) > 100` vacuity floors were themselves growth-coupled — 83 legitimate retirements would red them even with the ceiling still calibrated, which is the #688 class in the guard rather than the assertion. Lowered to >20 where a floor is meaningful, and to plain non-empty on the derived-ceiling test, whose derivations need nothing more. - test_main_FEEDS_the_crosscheck now compares against `set(record_wing_files())` instead of a hardcoded basename, killing the same mutation with zero corpus dependence. PROSE - "ordinary growth cannot cross it — NOT immune" contradicted itself in four places. Now: no SINGLE ordinary addition can cross it; this is measured headroom, not immunity. - "trimming or archiving 15" blurred two different denominators. Trimming leaves 3/183 = 1.64%; archiving leaves 3/168 = 1.79% because the denominator moves too. Both verified, both under the floor, now stated separately. Verification: 431 scripts/tests pass; ruff at baseline parity (47 — a 121-char docstring line briefly took it to 48 and is rewrapped); validator exits 0 with no drift notice; corpus at p90=60, 18/183, calibrated; record still 60 lines. Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c56dfdd539 |
fix(688): delete the last over-tight test, pin the crosscheck's INPUT, fix 4 prose defects
Review round 4, both reviewers. Both report the code path SOUND and the #688 coupling class analytically gone (rows proved, not merely observed green); one caught 28 of 30 mutations. What blocked was one over-tight test, two unpinned mutations, and prose — including two defects the PREVIOUS commit introduced while claiming to fix numbers. TESTS - Deleted test_adding_ordinary_records_cannot_RED_the_blocking_property. It appended two long records to the LIVE corpus and asserted flags_minority on the RESULT, so it crossed the cap two records before production does (56/221 vs 54/219) — a test named "cannot RED the blocking property" being a tighter tripwire than the property. Fourth instance of the #688 defect in this change. Deleted rather than tuned: both its jobs are already covered off live data (the synthetic v4/v5 contrast, and the deliberate live guard at the production threshold). - test_main_FEEDS_the_crosscheck_the_REAL_wing_files closes a mutation hole found by review: replacing `pyyaml_frontmatter_faults(record_wing_files())` with `...([])` in main() left the ENTIRE suite green. Both existing wiring tests monkeypatch the function, so they pinned that its RETURN reaches errs, never that its ARGUMENT is the corpus — the '#609 marker that printed OK while doing nothing' defect one level up, which is the exact thing the new record indicts. Verified: the mutation now fails this test. - test_main_actually_REPORTS_... went vacuous whenever the ceiling legitimately goes green (`False is False` passes with the whole warning branch deleted). Added an arm at --record-ceiling 0, which no non-empty corpus can make vacuous. - Pinned two surviving mutations: ceiling_calibration's n_over boundary (it recomputes the count, so oversized_records' exclusivity test does not cover it — `>` vs `>=` differs by the 3 records sitting exactly on the ceiling) and p95's quantile (the 95/5 fixture cannot tell 0.95 from 0.99). PROSE — two of these were introduced by the previous commit, whose stated job was fixing numbers. That is the pattern worth naming, not the individual typos. - "so ONE new record could move p90 lines" — the previous commit deleted the magnitude and left the sentence ungrammatical. Now "by 21 lines". - It also introduced a THIRD variant of the sequence it was correcting ("60, 60, 60") and missed a FOURTH site in ci-cd.md still saying "twenty lines". All four sites now read the measured 59, 59, 60, 60 -> 81, and 21 lines. - 59- and 60-line records were described as "above the ceiling"; they are at or below it. - "routine growth cannot cross it" overstated the bound: it is deliberately less sensitive, not immune. Reworded, and the THIRD and tightest arm is now documented wherever the other two appear: consolidating 15 of the 18 offenders drops below the 2% floor (verified: 3/183 = 1.64%). That is in real tension with test_oversized_records_can_go_green and is stated as accepted — at 3/183 the constant genuinely is mis-calibrated — with the remedy named: a consolidation PR that large should re-derive the ceiling in the same change. - Corrected a docstring that called the 999-ceiling failure "silently deleting the assertion"; it would go red, not silent. Verification: 430 scripts/tests pass; ruff at baseline parity (47); validator exits 0 with no drift notice; corpus at p90=60, 18/183, calibrated. The two new claims were measured, not assumed: the empty-list mutation fails the new test, and 15 consolidations reaches 1.64%. Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0db56c3ebd |
fix(688): remove the last live-corpus tripwires and three fabricated/wrong numbers
Review round 3, both reviewers, NOT-MERGEABLE. Nothing needed rework — the code path was found sound and mutation-sensitive (a 17-mutation battery caught every mutation with the semantically correct test). What was left were tripwires and prose. TRIPWIRES - `assert need > 20` was the tightest live-corpus assertion left in the blocking job: it reds after 16 over-ceiling additions, while `flags_minority` — the property #688 exists to protect — survives to 36. An arbitrary threshold on a live order statistic is the ratchet wearing a different hat. Removed; the measured headroom lives in prose, where being out of date costs a doc fix rather than someone else's red build. This also removes an unbounded `while` loop that HUNG the suite rather than failing it when the ratio could not reach the cap. - test_main_reports_ceiling_drift hardcoded ceiling 999, which is not guaranteed above p95: ten valid 1000-line records make 999 calibrated and silently delete the test's only assertion. Now derived as max+1, off the tail by definition. - test_main_actually_REPORTS_the_ceiling_and_the_trend required >=1 over-ceiling record. The ceiling is ALLOWED to go green (test_oversized_records_can_go_green says so), so that would red the blocking job the day someone consolidates the last offender — punishing exactly the work the warning asks for. Restated as an IFF. - test_no_budget_flag_means_no_retirement_warning asserted no bare "RETIRED" in stderr; a legitimate stale record whose TITLE contains the word reds it. Matched precisely now. - Added the >100-record vacuity guard its siblings carry to the derived-ceiling test. NUMBERS — all three were mine, and two are the failure mode this repo calls worse than no note at all (a confident claim that was never measured): - "the lengths above the ceiling ran 60, 61, 62, 63 then jumped to 81" is FABRICATED. No record of 61, 62 or 63 lines exists at origin/main, at the #672 sha, or at the #706 sha. Measured, the sequence is 59, 59, 60, 60 then 81 — a 21-line jump, so the conclusion was if anything understated. Corrected in all three places it was repeated, including the canonical v4 row of docs.corpus-size-signal. - The crosscheck record called `decisions-guard` a REQUIRED check — introduced by the previous commit in the sentence rewritten to fix an overclaim. Verified against Gitea branch protection: `main` requires exactly `Build & test (.NET)`, `EF migration integrity` and `review-verdict/h10`. NEITHER script-tests NOR decisions-guard is required; the record now says so. - docs.corpus-size-signal said 37 additions "to reach" the cap two paragraphs above 38 "below the cap" — a same-document numeric inconsistency of exactly the class this change set out to remove. Both now state 38 to BREACH, noting 37 lands on 0.25 and passes. - Also: the old bound's accepted range is 39..229 (not 43..229 — 43 is the NEW bound's lower edge); "95% over the ceiling" was 100%; `oversized_records` said the #620 distribution began at 0 lines where the record itself says 2. Verification: 428 scripts/tests pass; ruff at baseline parity (47); validator exits 0 with no drift notice; corpus at p90=60, 18/183 over the ceiling, record trimmed to 60 lines so main ships calibrated. Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b3a8826281 |
fix(688): stop asserting live-corpus order statistics anywhere in the suite
Review round 2 (both reviewers, independently) found the round-1 fix incomplete: the live-corpus coupling survived in two more tests. This is the THIRD instance of one defect class in this change, so the fix is to remove the coupling rather than patch another site. BLOCKER — test_adding_ordinary_records still asserted live order statistics. The `if before.marks_tail:` guard made the PRECONDITION conditional but left the CONCLUSION (`assert not after.marks_tail`) an assertion about the live p90. Verified: appending 16 ordinary 30-line records — nothing long, nothing unusual — makes both sides true and fires it, reddening the blocking job for an unrelated author. Exactly what #688 exists to abolish. The v4-vs-v5 contrast moved to test_v4_would_have_reddened_where_v5_holds, built on a distribution the test OWNS, reproducing the shape that matters (a sparse gap just above the ceiling). The real-corpus test now asserts only the robust claims: the additions were counted, v5 holds, and the measured headroom. Same treatment for the "bad ceiling" teeth test, which hard-coded that 200/229/230 stay rejected on the live corpus — three new 200+ line records flip it. Teeth now demonstrated synthetically; the only live-corpus assertion left is that today's ceiling is accepted, which needs 38 over-ceiling or 718 short additions to break. The IFF drift test could lose its quiet branch: one 61-line record makes BOTH the 60 and 999 ceilings drift, at which point an UNCONDITIONAL notice would pass. Both ceilings are now DERIVED — p90 itself (always calibrated, since p90 <= p90 <= p95) and max+1 (always off the tail) — so each branch is guaranteed by construction, and the test asserts it exercised both. Added the missing regression test for the typed-mapping-key TypeError: removing `key=str` now fails a test instead of only a manual probe. Corrected against measurement: the v5 row of the record's own version table still stated the REJECTED first-draft bound (`0 < f < 1/3`) — the canonical artefact contradicting both the code and its own next paragraph; accepted range is 43..180, not "roughly 45..150"; breaching the cap takes 38 additions, not 37 (37 lands exactly on 0.25, which passes under `<=`); "5x headroom below the floor" was inverted; ci-cd.md said four versions "all ratcheted" when v1 was vacuous and v2 accepted an absurd ceiling; and the crosscheck record overstated protection — script-tests is NOT a required check, so "no broken record has reached main" is procedural, not structural. Evidence the coupling is actually gone: mid-fix the corpus sat at marks_tail=False (an edit pushed this branch's own record to 61 lines, moving p90) and the suite stayed fully green. Under the old assertions that state reddened CI. The record is trimmed back to 60 so main ships calibrated and no drift notice nags. Verification: 428 scripts/tests pass; ruff at baseline parity (47); validator exits 0 with no drift notice; corpus at p90=60, 18/183 over the ceiling. Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
80818aa294 |
fix(674,688): address independent review — restore the coarse bound's teeth
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 33s
PR Gates / Docs update reminder (pull_request) Successful in 31s
PR Gates / decisions lifecycle (pull_request) Successful in 41s
Review verdict / Set review-verdict status (pull_request_target) Successful in 12s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m14s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m32s
PR Gates / Script tests (pytest) (pull_request) Successful in 2m8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m53s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 20m30s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 23m19s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Two independent cold reviews (one cross-family) agreed on the top two findings. 1. The #688 fix was defeated by its own complement test. test_main_is_QUIET_about_ drift asserted the drift notice was ABSENT while running main() over the LIVE corpus — whose failure condition is bit-for-bit v4's assertion, in the same blocking job, three functions down. p90 sat exactly on 60, so one over-ceiling record would have reddened it. Replaced with an IFF test that uses ceiling_calibration as its oracle, so it asserts the WIRING rather than the corpus's current state, plus a guard that at least one branch fires. 2. The coarse bound was nearly unfalsifiable. `0 < fraction_over < 1/3` accepted EVERY ceiling from 39 to 229 on the real corpus — including the ceiling of 200 my own docstring offered as the case it catches, because one 230-line record keeps the count nonzero. That claim was simply false and is corrected. The floor is now a FRACTION (2%) and the cap 25%, which rejects 200/229/230 and 20, and accepts roughly 45..150. Headroom measured, not estimated: 37 consecutive over-ceiling additions, against ONE record to break v4. 3. yaml.safe_load raises a bare ValueError, not a YAMLError, on a well-shaped but impossible date (stale-after: 2026-06-31), which escaped as a traceback and killed the validator on any machine with PyYAML. The except is now deliberately broad, with a test. 4. PyYAML returns TYPED mapping keys, so a stray `1: x` made sorted(set|set) raise TypeError. Sorted with key=str. 5. The headroom prose was arithmetically wrong (~42/~40 where the real values are 63/64; each addition moves numerator AND denominator) and the record counts were stale. Corrected against measurement. 6. test_adding_ordinary_records passed identically with its two additions removed. It now asserts the additions were counted, and that they break the v4 property while leaving v5 satisfied — guarded by `if`, never asserted, since whether v4 currently holds is a fact about the live distribution and asserting it would rebuild the ratchet. Also recorded honestly in docs.frontmatter-pyyaml-crosscheck: decisions-guard installs no PyYAML, so in CI the cross-check always skips and script-tests already caught both hazards — the CI delta is close to zero and the real fix is the local loop plus the tool/suite agreement. And the new record was trimmed 64 -> 58 prose lines: at 64 it moved p90 to 64 by itself, i.e. this PR would have reddened the old blocking job. That is now cited in the test as the live demonstration. Verification: 426 scripts/tests pass; ruff at baseline parity (47); validator exits 0; corpus back to p90=60, 18/183 over the ceiling, marks_tail and flags_minority both true. Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |