b1d5fbefcba02fdc6c19fef85cec1c4e82fc8dea
27
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cf5f42edf9 |
fix(858,859): a rule the classifier cannot read is not a rule that matches nothing (#897)
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 / CI toolchain image resolves (push) Successful in 10s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 27s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 10m49s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m41s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 7m18s
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 1m26s
#859 was filed as a wrong STATED CAUSE. It was masking a live false-open in the merge gate. Gitea reports a GLOB branch-protection rule with an EMPTY `branch_name` — the canonical name lives only in `rule_name`. Measured 2026-08-30 on a scratch repo against 1.27.1. jq's `//` fires on null and false but NOT on `""`, so `(.branch_name // .rule_name // "")` resolved every glob rule to the empty string — a name with no metacharacters — and the glob test, the entire basis of the classifier's undecidable-first ordering, never saw it. Measured on the predecessor: glob `m*` (not requiring review-verdict/h10) beside plain `main` (requiring it) resolved to `exact` on `main` and AUTO-GRANTED a scheduled merge, while Gitea — ordering by Priority then plain-name-ness — may be applying `m*`. That is #622's hole, reached through the ordering written to close it. Mirror case: a glob alone resolved to `none` and DENIED about a rule that provably governs the base. A name is now a non-empty string. Each field resolves to a NAME, a SKIP (absent/null/ empty — fall through), or POISON (present, wrong type — poisons whichever field carries it). A rule with no usable name is a distinct `unreadable` verdict with its own operator cause, instead of feeding `none`, whose whole authority is "the full rule list was read and none matches". The short-circuit is STRUCTURAL: jq binds `as` eagerly, so the flat form still evaluated `offs`/`nonascii` on the bad name and died before reaching the arm meant to prevent that. Also #859: `branch_protections` is fetched ONCE per run, not twice. The round trip is the smaller half — it is mutable config, so two reads can disagree and the two arms then decide about different repo states with neither able to notice. #858: `verdict_script` resolves from `$repo_root`, not `$CLAUDE_PROJECT_DIR`. And the finding that mattered more — `ETV_HOOK_FIRE_LIB` is `. `-SOURCED, so it is CODE running before stdin is read and before `decide` exists. A first draft exempted it as "telemetry, not a predicate"; cold review refuted that by execution: a decoy hook-fire-log.sh in an env-var-named tree printing an allow and exiting 0 GRANTS THE MERGE, bypassing every check. Classify a path by how it is CONSUMED, never by what it is called. This hook's copy is self-located; the other twelve are #891 (high/security), which records the reachable case — husky launches the prepush hooks by RELATIVE path, so the two roots diverge there. check-required-contexts.sh gains an array-type gate (a JSON object previously printed `nomatch`, a positive claim about server config from a body it cannot consume). Verification: 1377 passed / 2 skipped; 11 declared mutants, 11 detected, disjoint reddened sets; classifier executed across jq 1.8.2 and 1.6 with identical results; both env-var tests ship a negative control, because the passing outcome is also what an inert decoy produces. Four cold review rounds plus a bounded prose check. Every round found defects the previous round's fixes introduced — a type conflation that re-opened the auto-grant, a comment asserting the opposite of the line its own commit changed, and a corrected sentence whose identical twin survived in the same diff. Docs: new record `process.hook-resolves-inputs-from-repo-root`; both inline sites cite it rather than arguing it twice. docs/remote-state-inventory.md's row for the second read updated. Follow-ups filed: #891 (the other 12 hooks), #895 ("all N tests green" claims). fixes #858 fixes #859 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
0e40ac283b |
fix(870): an empty timeline page is not exhaustion — the walk reads to its cap (#896)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 5s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 10m21s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m56s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 7m2s
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) Failing after 1m35s
`count_pr_mutations` treated an empty page past page 1 as proof it had reached the end of the PR timeline. Gitea does not mean that: `ListIssueCommentsAndTimeline` applies the LIMIT/OFFSET in `FindComments` at the DATABASE level and filters AFTERWARDS, dropping `CommentTypeCode` rows and inaccessible cross-references into a nil slice that serializes as bare `null`. A page of 50 inline review comments is byte-identical to a page past the end while later pages still hold events, and rows are ASCENDING, so the events a fence looks for are the furthest from page 1. Fifty comments, which a PR author can create on their own PR, truncated both walks at the same place: both counts agreed, the sha comparison agreed, and an ABA force-push yielded an exemption `success` over a diff no single head justified. The walk no longer infers the end from an empty page BEFORE its cap. Such a page is skipped; the loop reads every page to its 20-page cap and trusts the counts only when the LAST page came back empty. An empty FIRST page and any unreadable shape still end the walk untrusted. NARROWED, NOT CLOSED, and the docs say so in one unit: the page-20 terminator is still trusted for the same unprovable reason, so the defeat now costs a timeline of over 1000 rows rather than ~100, with the same 50-row filtered block pinned to offsets 950..999. Measured at Gitea 1.27.1, ruling out the cheaper fixes: `X-Total-Count` on this endpoint is the post-filter length of the PAGE, not a total (`?limit=1` returns 1 on a 14-row timeline), while `/activities/feeds` returns a true total; `limit` clamps to 50; the only query params are `since`, `before`, `page`, `limit`, so the paged and serialized sets cannot be made to agree. Also: each page bounded `--connect-timeout 5 --max-time 15` and retried once, mirroring `page_statuses`, because the walk went from ~2 requests to a fixed 20 and the third call site runs after the exemption `success` is posted. Costs stated rather than hidden — worst case 40 requests and 20 sleeps, wall-clock pessimum 620s per walk, and the suite roughly doubled (202s -> 474s). Seven tests, each mutation-witnessed red; three reproduce the defeat against the shipped predecessor. Two independent cold reviews plus a re-review of the fix: no Blocker or High in the code. Their real finding was prose claiming the hole was closed, and cost arithmetic wrong twice. One reviewer claim was refuted by execution. Fixes #870 Refs: #803, #706, #664, #751, #893 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
28e82fcb59 |
fix(849): round 4 — a regression round 3 introduced, and the clauses it left unproven
A third cold review, which ran the mutants itself, found one measured direction regression against `origin/main`, one ordering inversion, and four clauses this branch claims as fixes that survived mutation of their own text. ## The regression Round 3 type-tested the four consumed fields of the existing `h10` row and resolved a failure to `""`. For `.creator` that means "no creator" — unattributable — which is a LICENCE TO RE-DERIVE. Measured, same fixture, both bodies: a head carrying `h10=failure` with `"creator": 7` posts `Exempt: docs-only change` here and posted NOTHING on `main`, which died on `.creator.login` before any write. Fail-closed became fail-open. The rationale that produced it came from #763, whose site is the POST-WRITE filter: there, dying leaves a green already published, so dropping the row is the safe direction. Here the alternative is dying BEFORE any write. The deferral rationale did not transfer — which is the shape this repo has a record for. A wrong TYPE is now distinguished from a legitimately ABSENT value: `null` is the machine creator, an unset description and every field of the `{}` no-verdict row; anything else is unknown state and takes the route an unreadable ELEMENT already took. ## The ordering inversion `mark_declined_row_if_any` was scoped to "the head carries any row", so it fired on a head carrying `$REPAIR_DESC` and replaced the human-only marker with the machine-clearable one — inverting the ordering the SAME commit added a floor to protect at the repair site. One mechanism, three writers, and only two had the rule. It also buried a verdict an ALLOW-LISTED reviewer wrote for another base. "Declined" is decided against this event's `$BASE_REF`, so such a row is still the right answer for the base it names and the successor run for that base short-circuits on it; burying it costs a manual re-post on an ordinary retarget-onto-the-reviewed-base flow. Membership is tested on the raw creator, not on `ex_human`, which the base check has already cleared — the question is who wrote the row, not whether it governs this diff. ## The unproven clauses Four claims survived mutation, including the headline one. The witness fixture had been designed AROUND its own discriminator — its comment said a seed with an unrelated id "would make this run carry the sentinel forward … and the guard under test would never be reached", which is a description of the test not reaching it. Eleven proofs added, covering the witness-by-id, the head arm's own call site (two callers of one helper, one fixture), the mark helper's result propagation, and the round-4 behaviour above. `raced_why`'s human value is a named constant now: it is the one such value that is also a PREDICATE, compared twice, and a drift in either copy silently downgrades the human `::error::` — the only message that tells a reviewer their verdict was buried. ## Docs The renamed sentinel literal in two places; three documents still asserting the fence "writes NOTHING"; the record's `mechanics:` still describing round 2's witness; the replacement-site list, which had grown by four; a residual pointing "below" at something above it; and `CLAUDE.md`'s "closed", which is stronger than the record it points at — that record lists six residuals including both endpoints failing at once. The proof inventory is stated as an invariant (every clause with a predecessor is mutated back to it) rather than a count that rots. refs #849 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF |
||
|
|
957a328f33 |
fix(849): round 2 — the uncertainty paths that still resolved toward success
Two independent cold reviews (Codex/GPT-5.6 cross-family, and a cold Claude reviewer in
its own worktree) converged on the same class: paths where "this job cannot establish
what is on the head" still resolved by leaving the head alone, which protects a real
verdict and leaves a forged one.
Behaviour:
1. The four page-2 completeness refusals now replace the unknown state too. They were
excluded on the reasoning that the probe fires when NO row for this context was on page
1, so there is no green of any provenance to leave standing — self-contradictory, since
the only reason page 2 is read is that the row may be beyond page 1, which the probe's
own message says. Accepted cost, stated in the record: a head with more CONTEXTS than
the 50-row cap stalls every run; measured 2026-08-29, this repo puts 8 on a `main` head,
and that case already stalled with an ABSENT check.
2. The no-mark downgrade covers every re-derivable write, not only `success`. Restricting
it analysed the wrong PR: the damaging case is one that IS exemptible and got the
generic `pending` only from a transient enumeration failure. That description carries no
marker, nothing verifies it without a mark, and the next run re-derives it into the
exemption with the human row below its own mark — route 2's damage through route 1's
condition. `$REPAIR_DESC` stays exempt, being stronger and not re-derivable.
3. The fence branch that cannot trust its retarget count while holding a derived `success`
writes the sentinel instead of abstaining. It is reached only after the classification
DECLINED to inherit the row the head carries, so posting nothing left that row current;
the message said the context "stays absent", true only of a head that had none.
4. Reconciliation needs a WITNESS: it may clear only over a complete history containing the
sentinel's own row. `ex_unverified` means the combined endpoint just returned that row
and `/statuses/{sha}` keeps one per POST, so a complete-but-empty history contradicts a
write that demonstrably happened — and `page_statuses` accepts an empty page 1 as
complete, which is what made it reachable. Both reviewers reproduced the clear-then-exempt
outcome. The shipped positive test used exactly that impossible fixture, so it was
pinning the defect; it now seeds the sentinel row, and an impossible-empty negative plus
a witness mutation proof were added.
5. The mid-run "did this row change" comparison now includes the row ID. The two sentinels
are byte-identical by design, so a mid-run replacement of one by another was invisible to
a state/creator/description triple. Measured 2026-08-29 (Gitea 1.27.1, head
|
||
|
|
e30702111f |
fix(849): verify every write, and mark a head nothing could verify
The gate's post-write verification had five routes that all ended the same way — an exemption `success`, or a generic `pending` a later run turns into one, standing over a human `failure`. Two of these were attempted inside #742 and withdrawn, and the withdrawal is what shaped this change. That attempt withheld the exemption by writing a GENERIC `pending`, which is exactly what a later run re-derives into `success` — it moved which run posted the forged green rather than stopping it — and it had no retry path, because this workflow triggers only on `pull_request_target` types, so a transient failure on a PR's last event stalled an exempt PR until a human nudged it. The fix therefore needs two properties at once: sticky, so a later run cannot re-derive it, and reconcilable, so a blip does not cost a head its exemption permanently. Neither the repair sentinel nor a generic `pending` has both, which is why there is now a second sentinel rather than a reuse of the first. What changed: 1. No high-water mark => the exemption is WITHHELD before the POST and the head is marked with the new `UNVERIFIED_DESC` sentinel. Withholding before the write rather than posting and repairing matters because the defect is known in advance: publishing a green to take it back opens a window branch protection, and an already-scheduled auto-merge, can see. 2. Post-write verification runs after EVERY write, not only `success`. A generic `pending` masks a rejection landing in its own write window just as well, and carries no marker, so the next run re-derives it with the human's row now below THAT run's mark. 3. `.description` is type-tested before `startswith`. `(.description // "")` does not replace a NUMBER, so `startswith` hard-errors on one, killing the whole count — the genuine verdict beside the malformed row is lost with it. 4. The retarget count is re-taken AFTER the POST on the exemption path, closing the PERMANENT forged green `ci.verdict-write-retarget-fence` listed as its residual 1. The retarget axis only: a push after the POST moves the head, so the status no longer gates that PR, while a retarget changes the effective diff with the sha unchanged. 5. An unreadable combined-status read retries once and then REPLACES the unknown state instead of declining to write. Declining protects a real verdict and leaves a FORGED one — an off-list `success` is the row #742 exists to revoke, revocation happens by re-deriving it, and the job then went red on a status branch protection does not read. One defect this introduced and fixed on the way: widening the post-write gate to every write made the job match its OWN row, because the machine-sentinel arm selects on a null creator. A run taking the carry-forward path POSTed `$REPAIR_DESC`, then found "a sentinel above the mark", then repaired to the identical description. `--arg own "$desc"` excludes it, by description rather than by id — the id of the row just written is not knowable there. Reconciliation is what bounds the stall: a later run pages `/statuses/{sha}` in full and either finds a `Review-verdict:` row underneath the sentinel — an established fact, so it upgrades to the repair sentinel, clearable only by a human — or finds none and clears it. It is sound because the two endpoints disagree: a masked verdict is invisible on the combined endpoint (latest row per context, which is the sentinel) and still present in the per-POST history. Tests: each fix is paired with a `test_MUTATION_…` proof that restores the exact predecessor text through a new `_run_classify(mutate=…)` knob, whose count assertion is the binding — a clause that has since moved substitutes zero times and fails loudly rather than measuring the unmutated body. Two CHAINED tests feed run N's real output into run N+1, because both sentinels are fixed points and a single hop cannot assert a fixed point: the raced-`pending` repair must survive the run that would otherwise grant the exemption, and the unverified sentinel must not decay while it cannot be reconciled. Docs: new record `ci.verdict-unverified-write-sentinel`; the now-false guarantee prose in `ci.verdict-write-retarget-fence` (its `rule:` frontmatter, the "resolves it" opener, "the fence above closes", the truncating-block claim and residual 1), `ci.exemption-provenance`, `docs/ci-cd.md`, `docs/remote-state-inventory.md` and `CLAUDE.md` corrected by concept rather than by phrase, per the scope boundary recorded on the issue. fixes #849 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF |
||
|
|
5d955000f3 |
fix(845): the verdict writer checks that the gate will honour what it just posted (#889)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 10s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 31s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 15m44s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 10m51s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 8m27s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 1m46s
`review-verdict.yml` inherits an existing `review-verdict/h10=success` only from a status whose `.creator.login` is on its `H10_REVIEWERS` allow-list (#742). `post-review-verdict.sh` wrote those verdicts with whatever account owned the credential in the environment and never asked whose it was. Two coupled values, nothing asserting the coupling, and the failure was the silent kind: the status is written, the tool reports success, and the next `pull_request_target` event re-derives it and posts over it. The PR stalls with no visible cause. The writer now READS ITS OWN STATUS BACK, identifies that write by state and description, and refuses — before the verdict comment, so the surviving half-state is the documented `ask` one — unless the recorded creator is allow-listed. Measured after the write rather than probed before it: that tests what Gitea recorded as the author, which is the value the gate reads, and needs no scope beyond the repo access the POST already required. Membership is required for a `success` ONLY, mirroring the gate's own asymmetry: a `failure` is inherited from any attributable account, so requiring it there would refuse a verdict the gate honours and leave an off-list reviewer no supported way to record a rejection. The allow-list is DERIVED from the gate's own literal by the new `scripts/lib/h10-reviewers.sh` — one declaration, not two plus a parity test. It is a parse rather than a shared declaration both sides source because the gate runs against a checkout of the PR's BASE sha: a PR whose base predates such a file would not have it, and a missing `source` under `set -euo pipefail` kills the job, which posts no `review-verdict/h10` at all and blocks every merge including its own repair (#743). `scripts/post-review-verdict.sh` moves BEHAVIOUR-ONLY -> MUTATION in the guard inventory, which the manifest's own note called "the most valuable upgrade on this list". The declared clause lives in the GATE: rewriting `H10_REVIEWERS` while the posting account stays fixed reddens the accept path only if the writer reads the list live AND the comparison gates the outcome. Two defects were caught by probing the live instance rather than re-reading the code. Reading `.state` instead of `.status` per row would have refused EVERY verdict — a repo-wide deadlock, shipped green, because the test shim replayed the POST payload as the read-back body and so agreed with the parser by construction. Then a `(.status // .state)` fallback added as defensiveness recreated #845 exactly: the writer would accept a shape the gate cannot read and report success. Nine independent cold review rounds, all worktree-isolated, one cross-family (GPT-5.6 via Codex). Round 8 caught the most important one: a `set -u` "correction" made mid-branch had inverted a TRUE statement in live merge-gate code, because the probe used a plain `$UNSET` while the validator uses `${#arr[@]}` — different shapes, different behaviour. Withdrawn wholesale; both libraries are byte-identical to `main` again. Verification: full `scripts/tests` suite green (1278 passed, 2 skipped); the declared mutation executes every run and reddens its named proof with the manifest's `expect` string; every clause disarmed individually and confirmed to redden its own named test; live probes against Gitea 1.27.1 for the row shape, the description round-trip, the paging order and the required-check list. Docs: `ci.exemption-provenance` records the coupling as asserted rather than as a tracked residual, plus `docs/ci-cd.md`, `CLAUDE.md`, `docs/guard-inventory.md`, `docs/remote-state-inventory.md`, `ci.script-tests-job` and the `script-tests` population comment in `pr-checks.yml`. Deferred: the refused-verdict residual (a non-inheritable status left standing with no comment) is the `ask` half-state `release.verdict-writes-status-before-comment` designates as safe; a second corrective write is the sticky-sentinel mechanism #849 is separately designing. fixes #845 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
736649b3b7 |
fix(812): classify the narrative sites by who-benefits; keep the detector's reach (#882)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 7s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 22s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m18s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m33s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Skipped
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 4m52s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
90f96c14a5 |
fix(803,664): fence the HEAD alias on the PR timeline's pull_push count (#873)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 15s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 26s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m16s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m39s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m37s
A force-push H1 -> H2 -> H1 spanning `pr-changed-files.sh`'s paging leaves its final
`.head.sha` comparison equal while the middle pages came from H2, so a mixed file list
could produce a docs-only exemption `success` no single head ever justified. The base
alias had been fenced since #706 by a monotonic `change_target_branch` count; the head
axis had nothing, and three contracts asserted otherwise.
`count_retargets` becomes `count_pr_mutations`: one timeline walk, two tallies, one shared
trust flag, a separate fence arm and diagnostic per axis. The advisory hook re-reads
`.head.sha` at the same hoist and off the same response as the base re-read. All three
overclaiming contracts are corrected, plus four paraphrases the first sweep missed.
Measured, not assumed: Gitea 1.27.1 still serves no `files` on `compare/{base}...{head}`;
every push is a `pull_push` event and its count cannot alias; PR #761 really went
`8798a1d ->
|
||
|
|
761e575836 |
fix(787): derive the dropped-step guard's scope, and reconcile its snapshot against the server (#861)
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 / CI toolchain image resolves (push) Successful in 6s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m45s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m22s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m56s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m33s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
469d19852c |
fix(788): one declarative H10 verdict vocabulary, derived by both sides (#846)
Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 6s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m17s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m50s
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 4m22s
The verdict words lived in two hand-written shell copies — the `case` arms of post-review-verdict.sh (write) and the POS_RE/NEG_RE regexes of check-review-verdict.sh (read) — held together by nothing but a comment that had already gone stale. scripts/lib/review-verdict-vocabulary.sh now declares them once and both sides derive; neither script enumerates a verdict word any more. Only the WORD SET moved. The grammar stays in check-review-verdict.sh, where every #629 false-open actually lived. No parity test: #774 shipped one and withdrew it after six rounds, because a regex over shell source is not a shell parser. The proof is behavioural and graded MUTATION — the harness restores the pre-#788 hardcoded POS_RE each run and requires it to redden. Enforcement is a DATA dependency, not a control-flow gate. Review round 1 found a real fail-open in the first commit: `${#arr[@]}` is nounset-safe only for a declared-empty array, and under `set -u` that error inside a function called as `if ! validate` skips BOTH branches — so on the reader (deliberately no `set -e`) an explicit BLOCKED @ head classified `positive`, exit 0. Validation now sets a sentinel on its last line and the derived views refuse without it. Six cold review rounds; rounds 2-6 found no fail-open across differential fuzzing (4788 / 2612 / 7560 payloads, zero divergences from origin/main's grammar), sentinel forgery, environment poisoning, declare -p evasion on bash 5.3 and 3.2, path/symlink resolution and probe TOCTOU. Every malformation fails closed: reader exit 2, writer exit 1 with nothing posted. Also corrected: CLAUDE.md and release.review-verdict-gate both enumerated the vocabulary without LGTM, a word the code has accepted since #629. fixes #788 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
d304c8573e |
docs(772): state the trap, not the draft history
The advisory narrative check was right about both new passages: 'an earlier draft warned and exited 0' and 'both cold reviews found it independently' only parse to someone who saw the session. What a cold reader needs is that warning-and-exiting-0 is the natural way to write this check and is wrong, and what it costs — which is now what the doc says. refs #772 |
||
|
|
30640bb780 |
fix(772,792): close the fix-round review — the doc still described the semantics the fix reversed
The re-review's one HIGH was mine and was the obvious one to miss: the previous commit changed the preflight so an unverifiable answer FAILS, and left a `docs/ci-cd.md` paragraph two screens away still saying "anything else is reported as could-not-tell". That paragraph is the one an operator reads when the job goes red, and it would have talked them into reinstating the defect. Replaced with the full arm table, including the two rows the first draft got wrong and why. * "gates nothing" was false in the way this repo has recorded before (#598): the merge-consent hook reads the COMBINED status, so a red preflight blocks the merge like any other red job. It does not SKIP the jobs it diagnoses; that is the accurate claim, in ci-cd.md and in the remote-state row. * The production retry defaults were evaluated by nothing — every test overrode both knobs. A test now drops the overrides and measures three attempts and a real pause, so editing the default to 1/0 (which would falsify the "a blip does not redden a PR" argument) goes red. * `journalctl -u gitea | grep ExecuteCleanupRules` is not a reproduction: that identifier reaches the log only through slow-query warnings, so an empty grep on a healthy host reads as "the rule never ran" — the inverse. Replaced with the admin cron API, which answers deterministically. * The recovery recipe's `docker buildx use default` needs the containerd image store to `--push` (both named hosts have it, checked today) and mutated the operator's builder selection without restoring it. * The stub's comment claimed both halves of real curl's transport failure mattered; only the exit status is observable, because `|| resp=""` discards what curl printed. * The empty-half credential refusal echoed the username; it needs no value at all. The 401/403 arm aborts the remaining pins while 404 continues — deliberate, now stated. * `curl -u "$VAR"` puts a credential in argv, and this job runs container-free on a shared host. NOT fixed here: it is the shape all five `scripts/` callers already use, so fixing one site leaves the class and splits the codebase. Filed as #821 and named at the site. refs #772 refs #792 |
||
|
|
0cf355e494 |
fix(772,792): address both cold reviews — an unverified pin is no longer a green job
Two independent reviewers (one cross-family) converged on the same defect, and it was the
important one: the preflight WARNED and exited 0 on every answer that was not 200 or 404,
so a missing `curl`, a moved registry or a DNS change would have left it green forever —
"the check could not run" presenting as "the pin is fine", in a script whose own header
disclaimed exactly that. Unknown answers are now retried (3x, 5s) and then FAIL, with
wording kept distinct from the deleted case because the two send an operator to different
places.
Also from the reviews:
* An absent secret does not arrive as an unset variable. `${{ secrets.X }}:${{ secrets.Y }}`
interpolates to ":", a perfectly non-empty and perfectly useless credential, and the
tests covered only the unset shape. Both halves are now required, and the parametrised
test drives the production shape.
* HTTP 200 is not a manifest. A proxy or a login page answers 200 too, so the body is
fetched and matched for `schemaVersion` (a shell `case`, so no jq dependency and no
pipeline that can inject).
* The curl stub ignored `-u` and answered 200 regardless, so deleting the real `-u` would
have left the suite green while the live registry rejected every request. It now 401s an
unauthenticated read, as the registry does.
* The mutation's declared diagnostic changed with the script: now that unknown fails too,
the exit code no longer separates "deleted" from "could not check", so the proof turns on
the message and `expect` says so.
* docs/ci-cd.md: `scan` is no longer the only `docker-build.yml` job on the small lane, so
the tag-push exclusivity claim and the lane membership were both false. Fixed.
* "Immutable" was overstated: `ci-image.yml` tags `rev-parse --short HEAD`, so a dispatch or
a weekly no-cache run at the same HEAD republishes that tag from a rebuilt image. Stated,
along with what the rebuild recovery does NOT restore (mutable bases and apt, so equivalent
rather than bit-identical).
* The recovery recipe left you in a worktree checked out at the pin commit — where the
verify script does not exist, and where the workflow carries the pre-bump pin. It now
keeps `$repo`, returns, and removes the worktree. It also needed BuildKit's `http = true`
caveat: the container driver does not inherit the daemon's insecure-registries.
* The root cause carries its evidentiary limit and its reproduction commands, and says what
to conclude if a pin vanishes after server-management#842 lands (refuted, not re-applied).
* The `ci.required-job-step-execution-markers` carve-out named one container-free job; there
are two now, and the membership is what rots.
* The decision record's `''` YAML escapes leaked into rendered prose; "status, no comment ->
ask" is qualified (a prior positive verdict for the SAME head still satisfies condition
(c)); "exits 1" is "exits non-zero" (usage exits 2, jq its own status, signals 128+n).
refs #772
refs #792
Decisions-Edit: yes
|
||
|
|
5ba442c11c |
fix(772,792): name the missing toolchain image, and stop a refusal leaving a verdict comment
#772 — the pinned CI toolchain image can be deleted out from under us, and when it was (2026-08-11..13) all five `container:` jobs died at image pull, both required contexts included, with the cause buried in each job's log. Root cause is registry-side and is now established rather than guessed: an owner-level Gitea package cleanup rule (keep_count 15, remove_days 1, remove_pattern `.*`, keep_pattern no 7-hex sha can match) deletes a sha tag once 15 newer versions exist, and `ExecuteCleanupRules` ran nightly through the window. The `ersatztv` package carries the same rule's fingerprint exactly — every sha tag older than the 15-slot window is gone, every keep_pattern tag back to 26.3.1 survives. Version deletes leave no audit row, so the specific run cannot be replayed; that limit is stated where the claim is made. The durable fix belongs to the registry's repo: server-management#842. What lands here is what a consumer of someone else's registry can do: * `toolchain-preflight`, a container-free job (a job consuming the image could not run to report it missing) resolving every pin against the registry and failing with a message that names the tag and the recovery. Not a `needs:` of the jobs it diagnoses — gating five jobs behind a checkout and one curl taxes every green run to speed up a rare red one, and they already fail fast. * Only HTTP 404 means gone. Everything else is could-not-tell, and rejected credentials fail rather than pass as unknown — "the check could not run" must never present as "the pin is fine". * A recovery path that does not need CI: rebuild the SAME tag from the commit it names and push it. The push half was verified against this registry on 2026-08-22 with a throwaway package (created, resolved 200, deleted). #792 — the reported defect was the exit code, and re-measuring says that premise is false: every no-status path already exits 1, and eight refusal modes now assert it against the real predecessor, where they pass. The observed 0 came from the invocation, not the script. What WAS broken is the half-state the issue describes second: the comment was written before the status, so every refusal left `Review-verdict: MERGEABLE @ <head>` on a PR with no gating status behind it. The two writes are now ordered status-then-comment, which makes the only reachable half-state the safe one — a status with no comment leaves the merge hook's condition (c) with nothing to classify, which is an `ask`. The refusals themselves are untouched. Ordering rather than compensating deletion: an orphaned-comment cleanup needs a Gitea call, and these refusals are usually caused by Gitea being unreachable. Proof for the ordering is the split against origin/main's script: the 8 orphan/ordering tests go red there, the 8 exit-code tests stay green. fixes #772 fixes #792 Refs: server-management#842 Decisions-Edit: yes |
||
|
|
6a4265d81d |
feat(784): a doc records the end state — generalize the no-session-narrative rule (#811)
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 40s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m30s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m16s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m52s
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) Failing after 17s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be> |
||
|
|
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
|
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
3322d68948 |
docs(778): state that $sha is stale mid-run too, and track it in #803
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 29s
PR Gates / Docs update reminder (pull_request) Successful in 46s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 47s
PR Gates / decisions lifecycle (pull_request) Successful in 54s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 12s
review-verdict/h10 Awaiting review verdict for 3322d68
Review verdict / Set review-verdict status (pull_request_target) Successful in 10s
PR Gates / Script tests (pytest) (pull_request) Successful in 4m39s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m32s
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) Successful in 5m59s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
Found by applying round 5's own finding symmetrically. `$base_ref` was re-read before the branch-protection lookup because using a value captured at hook start is not checking. `$sha` is captured from the same snapshot and is never re-read, so every later check — CI status, H10 status, verdict comments — evaluates against the commit a mid-run push replaced. Not fixed here: the base case was inside the code this PR introduced, while the sha spans the pre-existing H10 logic, and opening that at round five of review is how a scoped change stops being reviewable. The row now names both staleness windows (within the run, and after the decision) instead of only the second, and #803 carries the fix. 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> |