fix(742): inherit an h10 verdict only from an allow-listed reviewer #850

Merged
timothy merged 16 commits from fix/742-verdict-allowlist into main 2026-08-27 04:52:03 +02:00
Owner

fixes #742

What shipped

review-verdict.yml decided whether an existing review-verdict/h10 was a verdict worth
inheriting by testing .creator.login != null. That is satisfied by any account's credential.
RENOVATE_TOKEN is a write:repository PAT of the renovate bot; unlike #697's registry
credential it cannot be scoped off status-write (Renovate needs repo write to push branches and
open PRs), and secrets are a per-repo store any workflow can reference — so a status posted with it
read back creator: renovate, non-null, and was inherited.

The test is now membership in H10_REVIEWERS="timothy".

  • A literal in the base-resolved definition, not a repo vars: entry or a secret: those are
    settable without a PR, i.e. without this gate, while .gitea/ is on the PROTECTED path list so the
    edit cannot ride a docs-only exemption.
  • Exact POSIX [ = ] over word-split entries — never [[ == ]] (RHS becomes a pattern), never
    grep -xF (a newline-bearing creator reads as several patterns).
  • A ::warning:: names the creator whenever the job re-derives over a verdict-shaped status from an
    off-list account, so a reviewer left off the list is not discarded silently.

The part worth reviewing: read_existing_verdict answers TWO questions

It is called twice, and the callers want opposite widths. The first draft gated one flag and so
narrowed both — cold review caught it, and the defect was reproduced red before it was fixed.

Site Flag Why
Inheritance short-circuit ex_human (allow-listed) Narrowing is safe — an unrecognised account falls through to re-derivation
Last-moment re-read ex_attributable (broad) + triple changed Asks "did a reviewer post while we were classifying". Narrowing makes the job stop abstaining and post its exemption over the row — and the post-write repair does not cover that, being skipped whenever the high-water mark could not be established
Post-write raced check broad on creator != null See below

The two calls must compute a predicate that "changed" is measurable against. Before the split they
were identical, so anything firing at the second and not the first had arrived in between. Splitting
broke that: a pre-existing row declined for inheritance is still attributable, so the re-read abstained
on the very row the classification had decided to overwrite, left the forgery standing and posted
nothing. Seven tests went red on exactly that. The state/creator/description triple from the first read
is now snapshotted and compared.

Why the raced check stays broad — and not for the reason first written. The first draft said
"narrowing it would leave an exemption success standing over a rejection". Review showed that is not a
checked reason: a real reviewer is on the list by construction. The real reason is the
misconfiguration case — a second human account added and forgotten from the literal — where this is
the last net before a green stands, and the errors are asymmetric (a repair to pending is
recoverable; a wrongly-standing success is not). The cost is stated at the site: any verdict-shaped
row landing inside the write window trips the sticky sentinel and costs that head its exemption.

Verification

Mutation-proven against the shipped file, not a hand-written mutant — each applied to the tree, run,
then reverted:

Mutation Result
membership clause → [ -n "$ex_creator" ] inheritance cases red
raced jq → .creator.login == "timothy" polarity test red
mid-run abstain → ex_human mid-run test red
drop the changed-row comparison inheritance cases red
if false at the abstain both mid-run tests red
H10_REVIEWERS = tim* / empty / bob list-shape guards red (different subsets)

scripts/tests: 968 passed, 2 skipped.
test_hook_fire_log.py::test_the_suite_does_not_write_to_the_PRODUCTION_log went red once during a
full run and passes in isolation on both this branch and a pristine origin/main worktree —
cross-session interference on the shared production log (#809/#822), not this change.

Live verification is post-merge, and that is stated rather than assumed. pull_request_target
resolves this definition from main (ci.gate-trigger-base-resolved), so the new behaviour cannot run
on its own PR — the same self-test gap ci.exemption-provenance records for route 1. A live
cross-account probe would additionally require minting a second Gitea account's credential; the
pre-merge evidence is the executed-behaviour tests above, which run the workflow's real classify shell.

What this does NOT close

An allow-listed collaborator's own write-scoped token can still post a verdict nobody formed;
branch protection binds the context name, not its issuer. And post-review-verdict.sh does not check
that its own account is one the gate accepts — a changed writer credential would deadlock PRs rather
than fail loudly. Filed as #845, deliberately not bundled: that script is on the merge-consent path.

Docs

docs/ci-cd.md (Review-verdict gate), ci.exemption-provenance, ci.actions-credential-scoping,
release.verdict-status-check, and ci.gate-trigger-base-resolved — the last found by sweeping for the
old wording rather than only the files this change touched; it stated in both its rule and its body
that a RENOVATE_TOKEN status "carries a real creator and IS inherited". Catalog regenerated.


Review history — 11 rounds, and what they changed

Recorded because several rounds changed the DESIGN, not just the diff.

Round Family Outcome
1 Claude ×2 The allow-list also narrowed the mid-run abstain, a mirror-image site. Reproduced red, then split ex_attributable (broad) from ex_human (allow-listed)
2 Claude ×2 My own split broke the invariant that made the second read work — both calls used to compute an identical predicate. Added the snapshot triple
3 Claude ×2 Guards were right, the suite couldn't tell: deleting the annotation, any one triple clause, or making the membership loop last-entry-only all shipped green
4 Claude The annotation's guard was the diff's only green mutation and its "pin" could not fail → withdrew the annotation
5 Codex The failure short-circuit's attributability was unpinned; three pre-existing post-write BLOCKERs → #849
6 Codex "Pre-existing" was only textually true — this change alters their reachability. Fixed two
7 Codex Those fixes were worse than the gap (generic pending is re-derived anyway; no retry trigger exists) → withdrew them
8–10 Codex Retracted the safety claims the deferred routes disprove; three sweeps, each finding more paraphrases → recorded a scope boundary in #849
11 Claude MERGEABLE. Nine mutations, all reddened; dropping one triple clause reddens exactly one case

Two mechanisms were withdrawn rather than patched a third time, per this repo's own rule. Both withdrawals are recorded in ci.exemption-provenance so the next reader doesn't re-attempt them the same way.

Follow-ups filed: #845 (post-review-verdict.sh doesn't check its own account is allow-listed) and #849 (post-write verification: three routes leaving an exemption success over a human failure, plus the retarget fence's post-POST gap).

fixes #742 ## What shipped `review-verdict.yml` decided whether an existing `review-verdict/h10` was a verdict worth **inheriting** by testing `.creator.login != null`. That is satisfied by any account's credential. `RENOVATE_TOKEN` is a `write:repository` PAT of the `renovate` bot; unlike #697's registry credential it **cannot be scoped off status-write** (Renovate needs repo write to push branches and open PRs), and secrets are a per-repo *store* any workflow can reference — so a status posted with it read back `creator: renovate`, non-null, and was inherited. The test is now membership in `H10_REVIEWERS="timothy"`. - **A literal in the base-resolved definition**, not a repo `vars:` entry or a secret: those are settable without a PR, i.e. without this gate, while `.gitea/` is on the PROTECTED path list so the edit cannot ride a docs-only exemption. - **Exact POSIX `[ = ]`** over word-split entries — never `[[ == ]]` (RHS becomes a pattern), never `grep -xF` (a newline-bearing creator reads as several patterns). - A `::warning::` names the creator whenever the job re-derives over a verdict-shaped status from an off-list account, so a reviewer left off the list is not discarded silently. ## The part worth reviewing: `read_existing_verdict` answers TWO questions It is called twice, and the callers want **opposite widths**. The first draft gated one flag and so narrowed both — cold review caught it, and the defect was reproduced red before it was fixed. | Site | Flag | Why | |---|---|---| | Inheritance short-circuit | `ex_human` (allow-listed) | Narrowing is safe — an unrecognised account falls through to re-derivation | | Last-moment re-read | `ex_attributable` (broad) + triple changed | Asks "did a reviewer post while we were classifying". Narrowing makes the job stop abstaining and post its exemption over the row — and the post-write repair does **not** cover that, being skipped whenever the high-water mark could not be established | | Post-write raced check | broad on `creator != null` | See below | **The two calls must compute a predicate that "changed" is measurable against.** Before the split they were identical, so anything firing at the second and not the first had *arrived* in between. Splitting broke that: a pre-existing row declined for inheritance is still attributable, so the re-read abstained on the very row the classification had decided to overwrite, left the forgery standing and posted nothing. Seven tests went red on exactly that. The state/creator/description triple from the first read is now snapshotted and compared. **Why the raced check stays broad — and not for the reason first written.** The first draft said "narrowing it would leave an exemption success standing over a rejection". Review showed that is not a checked reason: a real reviewer is on the list by construction. The real reason is the **misconfiguration** case — a second human account added and forgotten from the literal — where this is the last net before a green stands, and the errors are asymmetric (a repair to `pending` is recoverable; a wrongly-standing `success` is not). The cost is stated at the site: any verdict-shaped row landing inside the write window trips the sticky sentinel and costs that head its exemption. ## Verification Mutation-proven against the shipped file, not a hand-written mutant — each applied to the tree, run, then reverted: | Mutation | Result | |---|---| | membership clause → `[ -n "$ex_creator" ]` | inheritance cases red | | raced `jq` → `.creator.login == "timothy"` | polarity test red | | mid-run abstain → `ex_human` | mid-run test red | | drop the changed-row comparison | inheritance cases red | | `if false` at the abstain | both mid-run tests red | | `H10_REVIEWERS` = `tim*` / empty / `bob` | list-shape guards red (different subsets) | `scripts/tests`: **968 passed, 2 skipped**. `test_hook_fire_log.py::test_the_suite_does_not_write_to_the_PRODUCTION_log` went red once during a full run and passes in isolation on **both** this branch and a pristine `origin/main` worktree — cross-session interference on the shared production log (#809/#822), not this change. **Live verification is post-merge, and that is stated rather than assumed.** `pull_request_target` resolves this definition from `main` (`ci.gate-trigger-base-resolved`), so the new behaviour cannot run on its own PR — the same self-test gap `ci.exemption-provenance` records for route 1. A live cross-account probe would additionally require minting a second Gitea account's credential; the pre-merge evidence is the executed-behaviour tests above, which run the workflow's real classify shell. ## What this does NOT close An **allow-listed** collaborator's own write-scoped token can still post a verdict nobody formed; branch protection binds the context *name*, not its issuer. And `post-review-verdict.sh` does not check that its own account is one the gate accepts — a changed writer credential would deadlock PRs rather than fail loudly. Filed as **#845**, deliberately not bundled: that script is on the merge-consent path. ## Docs `docs/ci-cd.md` (Review-verdict gate), `ci.exemption-provenance`, `ci.actions-credential-scoping`, `release.verdict-status-check`, and `ci.gate-trigger-base-resolved` — the last found by sweeping for the **old wording** rather than only the files this change touched; it stated in both its rule and its body that a `RENOVATE_TOKEN` status "carries a real creator and IS inherited". Catalog regenerated. --- ## Review history — 11 rounds, and what they changed Recorded because several rounds changed the DESIGN, not just the diff. | Round | Family | Outcome | |---|---|---| | 1 | Claude ×2 | The allow-list also narrowed the **mid-run abstain**, a mirror-image site. Reproduced red, then split `ex_attributable` (broad) from `ex_human` (allow-listed) | | 2 | Claude ×2 | My own split broke the invariant that made the second read work — both calls used to compute an identical predicate. Added the snapshot triple | | 3 | Claude ×2 | Guards were right, the suite couldn't tell: deleting the annotation, any one triple clause, or making the membership loop last-entry-only all shipped green | | 4 | Claude | The annotation's guard was the diff's **only** green mutation and its "pin" could not fail → **withdrew the annotation** | | 5 | Codex | The `failure` short-circuit's attributability was unpinned; three pre-existing post-write BLOCKERs → **#849** | | 6 | Codex | "Pre-existing" was only *textually* true — this change alters their **reachability**. Fixed two | | 7 | Codex | Those fixes were worse than the gap (generic `pending` is re-derived anyway; no retry trigger exists) → **withdrew them** | | 8–10 | Codex | Retracted the safety claims the deferred routes disprove; three sweeps, each finding more **paraphrases** → recorded a scope boundary in #849 | | 11 | Claude | **MERGEABLE.** Nine mutations, all reddened; dropping one triple clause reddens exactly one case | Two mechanisms were **withdrawn** rather than patched a third time, per this repo's own rule. Both withdrawals are recorded in `ci.exemption-provenance` so the next reader doesn't re-attempt them the same way. **Follow-ups filed:** #845 (`post-review-verdict.sh` doesn't check its own account is allow-listed) and #849 (post-write verification: three routes leaving an exemption `success` over a human `failure`, plus the retarget fence's post-POST gap).
timothy added 15 commits 2026-08-27 04:15:49 +02:00
`review-verdict.yml` left an existing `review-verdict/h10` alone when it was "positively
identifiable as a human verdict" — a non-null `.creator.login` plus a `Review-verdict:`
description. Non-null only proves SOME user credential posted it, and this repo stores
credentials for accounts that are not reviewers. `RENOVATE_TOKEN` is a `write:repository`
PAT of the `renovate` bot; unlike #697's registry credential it cannot be scoped off
status-write (Renovate needs repo write to push branches and open PRs), and secrets are a
per-repo store any workflow can reference — so a status posted with it read back
`creator: renovate`, non-null, and was inherited as a verdict.

The test is now membership in `H10_REVIEWERS="timothy"`, a literal in the base-resolved
definition. Not a repo `vars:` entry or a secret: those are settable without a PR, i.e.
without this gate, while `.gitea/` is on the PROTECTED path list so the edit cannot ride a
docs-only exemption. Membership is an exact `[ = ]` comparison over word-split entries,
never `grep -xF` — an API-supplied creator containing a newline would be read as several
patterns, any one of which could match an allow-listed line.

The allow-list is applied to INHERITANCE ONLY. The post-write raced-verdict check reads the
same `.creator` field for the opposite purpose — "did anything human land while we were
writing" — so narrowing it would not make it stricter, it would make it count fewer rows,
and a raced verdict from an off-list account would leave the exemption `success` standing
over a rejection. That is the one path in this job whose failure direction is toward
SUCCESS, so it stays broad on `creator != null` and the repair is allowed to be over-eager.

Both directions are mutation-proven against the shipped file, not a hand-written mutant:
reverting the membership clause to `[ -n "$ex_creator" ]` reddens the seven new inheritance
cases; narrowing the raced jq to `.creator.login == "timothy"` reddens
`test_the_RACED_verdict_check_still_counts_a_NON_allowlisted_creator` alone; `H10_REVIEWERS`
set to `tim*`, empty, or `bob` reddens the two list-shape tests. `scripts/tests`: 965 passed,
2 skipped.

What this does NOT close, unchanged: an allow-listed collaborator's own write-scoped token
can still post a verdict nobody formed. Branch protection binds the context NAME, not its
issuer, and Gitea offers no way to restrict who may post a context.

fixes #742

Decisions-Edit: yes
Cold review round 1 (two independent agents) found the first draft narrowed BOTH
readers of the verdict row, not just inheritance.

* `read_existing_verdict` is called twice. The second call is the last-moment re-read,
  which asks "did a reviewer post a verdict while we were classifying" — the mirror-image
  question. Gating the single flag made the job stop abstaining and post its exemption
  straight over a mid-run row, and the post-write repair does NOT cover that: it is skipped
  whenever the high-water mark could not be established. Reproduced red first, then fixed:
  `test_the_MID_RUN_abstain_still_fires_for_a_NON_allowlisted_creator`.
* Splitting the flags broke the invariant that made the re-read work at all — the two calls
  used to compute an IDENTICAL predicate, so anything firing at the second and not the first
  had arrived in between. A pre-existing row declined for inheritance is still attributable,
  so the re-read abstained on the very row the classification had decided to overwrite and
  posted nothing. Seven tests went red on exactly that. The state/creator/description triple
  from the first read is now snapshotted and compared, making "changed" explicit.
* The stated reason for keeping the post-write raced check broad was asserted, not checked:
  a genuine reviewer is on the list by construction, so membership there would still count
  every real verdict. Replaced with the real reason (the misconfiguration case — a second
  human account forgotten from the literal) and the real cost (a verdict-shaped row landing
  in the write window trips the sticky sentinel and costs that head its exemption).
* A `::warning::` now names the creator whenever the job re-derives over a verdict-shaped
  status from an off-list account, so a reviewer left off the list is not discarded silently.
* The list guard uses `findall` + a single-assignment assertion: `search` validates the
  FIRST assignment while the shell runs the LAST executed one.
* Mutation counts dropped from the decision record in favour of test names — nothing
  re-executes a number written there.

Mutation-proven: mid-run site back to `ex_human` reddens the mid-run test; dropping the
changed-row comparison reddens the inheritance cases; `if false` at the abstain reddens both
mid-run tests.

fixes #742

Decisions-Edit: yes
Sweeping for the OLD wording rather than only the sites the change touched turned up
`ci.gate-trigger-base-resolved`, which stated in both its rule and its body that a
RENOVATE_TOKEN-posted status 'carries a real creator and IS inherited'. True until this
change, and a record that reads as a checked fact is worse than none.

Also files #845 for the coupling the shell/polarity review raised and this change does not
close: post-review-verdict.sh posts with whatever account owns the credential and never
checks it is one the gate accepts, so a changed writer credential deadlocks PRs rather than
failing loudly. Recorded as a residual with the live verification behind it, not asserted away.

Decisions-Edit: yes
Round-2 cold review (two independent agents) returned BLOCKED. Both found the same class
of hole from different angles.

FAIL-OPEN, and a regression against main. The short-circuit branched on success OR failure
under one membership test, so an off-list `failure` was re-derived. Trace it: a real
reviewer left off the literal posts BLOCKED; ex_human=no so the short-circuit declines; the
PR is docs-only so the classifier exempts it; the re-read triple never changed so the
mid-run abstain declines; the row predates the high-water mark so the post-write repair
cannot see it. The job posts success over an explicit rejection — reached by the very
misconfiguration the allow-list's own diagnostic is about. `failure` now needs only
attributability, which costs nothing the allow-list bought: inheriting a rejection cannot
green anything, and the worst a forged one achieves is a stall that any human clears in one
command (post-review-verdict.sh POSTs unconditionally).

THE CHANGED-ROW COMPARISON WAS NOT PINNED. A presence-only version (`[ -z "$pre_state" ]`)
passed all 158 tests, because every mid-run test started from an empty head and exercised
only the ARRIVAL case. The REPLACEMENT case is the one that co-occurs with #742's own
scenario. Stub knob STUB_PRE_ROW plus a test; the presence-only mutant now reddens.

Also from the round: the ::warning:: fired with a self-contradictory message for an
allow-listed creator in a non-terminal state, and interpolated an API-supplied value into a
workflow command unstripped; a quoted test count that no longer matched the tree; the
function's own contract line omitted the flag this change adds; a comment naming ex_human
where it is now ex_attributable; and an unprobed claim about how Gitea renders ::warning::,
now withdrawn rather than restated.

MEASURED rather than asserted: two POSTs of context probe742/combined-newest to one sha
(pending, then success) came back as ONE row on /commits/{sha}/status — the newest — and as
BOTH on /statuses/{sha}. That claim became load-bearing with the changed-row comparison;
main depended on presence alone. The probe left the sha's combined state `success`.

Recorded, not silently gained: an attributable row in a state other than success/failure
used to stall a PR forever on main (neither short-circuit fired, the old unconditional
abstain did). The changed-row comparison removes that stall.

scripts/tests: 969 passed, 2 skipped.

fixes #742

Decisions-Edit: yes
Round-3 cold state-machine review returned MERGEABLE (no fail-open, no permanent stall, all
five clause mutations pinned) with three LOWs, all measured rather than argued.

* The re-derivation `echo` asserted 'is NOT an attributable reviewer verdict' on a path also
  reached by an ALLOW-LISTED creator whose row is in a state other than success/failure —
  printing "creator='timothy', allow-list='timothy'". The ::warning:: below it was already
  guarded against exactly that contradiction and the echo was not, which is the same defect
  one line up. Now state-neutral.
* The failure short-circuit's message said an explicit rejection is left alone 'whoever
  recorded it', but the guard requires attributability: a failure with a non-verdict
  description, a null creator, or a DIFFERENT recorded base is still re-derived. Message
  qualified rather than the guard widened — a bare [ state = failure ] would be safe by the
  clause's own argument, but it would quietly opt `failure` out of the base binding, and
  none of the excluded shapes is producible by post-review-verdict.sh. Written down so the
  next reader does not 'simplify' it.
* ci.exemption-provenance cited a test that the success/failure split had renamed — a
  mutation proof nobody could run. Fixed, and the class is now guarded: a new test derives
  the cited names from the record's own text and asserts each is defined in scripts/tests.
  Mutation-proven against the pre-fix wording.

scripts/tests: 970 passed, 2 skipped.

fixes #742

Decisions-Edit: yes
Round-3 cold tests/prose review returned BLOCKED. Its findings were all of one shape: the
clauses are correct, but the suite could not tell.

Measured green with the mechanism DELETED, now each reddens on exactly one test:
* deleting the ::warning:: entirely — 166 green. It is not decoration: docs/ci-cd.md heads a
  paragraph with 'If a reviewer's verdicts stop being honoured, this is why', and
  ci.exemption-provenance uses its existence to soften the #845 residual. Asserted present
  in the off-list case, and ABSENT for an allow-listed reviewer, which also pins the
  ex_human != yes conjunct.
* dropping any ONE clause of the changed-row triple, or reducing the triple to a single
  comparison. Both mid-run tests moved all three fields at once, so 'compare the whole row'
  was argued, not measured. STUB_MIDRUN_ROW plus one parametrised case per field.
* a membership loop that can only ever match its LAST entry. Every behavioural test ran the
  singleton list, under which that is indistinguishable from a correct loop — and the
  annotation this change adds tells an operator to ADD a second entry, so the two-entry path
  is the first one exercised in anger. A reviewers= override substitutes a two-entry literal
  into the real step body; first-entry, last-entry control and absent-account control.

Prose, all measured by the reviewer rather than argued:
* 'was inherited as a human verdict' described an event nobody observed — renovate has never
  posted a commit status in this repo (every row on all six Renovate-authored PR heads carries
  creator: null). The asymmetry underneath IS measured; the ROUTE is an inference. Conditional
  voice in all three places, with the distinction stated.
* the ::warning:: does not fire for a row recording a DIFFERENT base — the base binding
  re-derives it before the annotation is reached. 'whenever' was false.
* a credential drift re-derives to an exemption success on an exempt PR, not only to pending —
  the quieter and worse half.
* the live account-set measurement now carries its two curl paths.
* CLAUDE.md states the H10 convention and now carries the constraint: the credential you post
  with must be an allow-listed account, or the verdict is written, reported as success, and
  silently re-derived (#845).
* residual session chronology trimmed from the record.

STUB_PRE_ROW now emits the measured shape (total_count, a decoy context) rather than a
minimal one.

scripts/tests: 976 passed, 2 skipped.

fixes #742

Decisions-Edit: yes
Round-4 cold review found the annotation's `ex_human != yes` conjunct was the diff's only
GREEN mutation, and that the assertion captioned as pinning it could not fail: both
parametrizations of that test exit at a short-circuit two guards earlier and never reach the
block at all. A caption claiming a proof is worse than no assertion.

WITHDRAWN rather than patched a third time. That mechanism produced three defects in three
rounds — a message reading "from 'timothy', which is NOT in H10_REVIEWERS ('timothy')", an
unescaped API value interpolated into a workflow command, and now a vacuous pin — and it
duplicated all three values from the plain log line one line above it. Its only delta was a
severity whose rendering could not be probed. Deleting it removes the green mutation, ~28
comment lines, the vacuous assertion and a docs paragraph that had promoted it to a contract.

The log line keeps the whole diagnostic and gains the remediation sentence, and its three
parts are now asserted on a path the job always reaches. Both halves mutation-proven:
dropping the remediation reddens, and so does dropping the allow-list VALUE — an earlier
assertion tested for the WORD `H10_REVIEWERS`, which the remediation sentence satisfies on
its own, so that deletion had stayed green.

Also: the comment justifying POSIX `[ = ]` claimed `[[ == ]]` makes its RHS a pattern. Probed
on bash 5.3 — with a QUOTED RHS it does not; only an unquoted one globs. The invariant is
the quoting, and it is now stated as that, with the untestability claim (which rested on the
false premise) dropped.

Round-by-round narrative trimmed out of the code comments per docs.no-session-narrative; the
rules those blocks carry stay, the chronology moves to the PR.

fixes #742

Decisions-Edit: yes
Cold CROSS-FAMILY review (Codex/GPT-5.6), the first this branch has had — the earlier four
rounds were all one model family.

* The `failure` short-circuit's attributability requirement was NOT pinned. Every existing
  `failure` case supplied a well-formed, base-matching, non-null-creator row, so widening the
  clause to a bare [ "$ex_state" = failure ] passed the whole suite — the exact widening the
  comment at the site says is deliberately not done, because it would opt `failure` out of the
  base binding. Three cases now cover a null creator, a non-verdict description and a verdict
  recorded against ANOTHER base; all three redden on that mutation.
* The docs described the allow-list more broadly than the code implements it: an existing
  `failure` is left alone on the weaker attributability test and needs no membership. Scoped
  to an inherited `success` in docs/ci-cd.md, ci.exemption-provenance (rule + body) and
  CLAUDE.md, with the exception stated rather than left implicit.

Its three BLOCKERs are REAL but PRE-EXISTING, and filed as #849 rather than fixed here:
an unestablished high-water mark skips the post-write race check; an unreadable post-write
history leaves the green; and the check runs only for state=success, so a generic `pending`
write can bury a rejection a later run then greens. Verified by diff against origin/main —
those three guards are byte-identical and this change touches none of them. #742 is a
provenance change; those are the post-write verification's own residuals, and burying them in
a passing review would be the worst outcome. Recorded in the record, not just the issue.

scripts/tests: 1012 passed, 2 skipped.

fixes #742

Decisions-Edit: yes
Cross-family RE-review of the previous fix round. Its sharpest point: my claim that the
three post-write residuals were 'pre-existing, #742 touches none of them' was only TEXTUALLY
true. The guards are byte-identical to main, but this change alters their REACHABILITY —
before it, an attributable `success` from ANY account short-circuited and the job wrote
NOTHING, so the unverified-write paths were unreachable for that whole class of head.
Re-deriving an off-list `success` is the POINT of #742, and it turns 'no write' into 'a
write'. Two of the three therefore belong here, not in the follow-up:

* An UNESTABLISHED high-water mark now WITHHOLDS the exemption. The post-write race check is
  the only thing between 'a reviewer posted failure during our write window' and a green over
  a rejection, and it is skipped outright when the mark is missing — so the run asks for a
  verdict instead of posting a green it cannot verify.
* An UNREADABLE post-write history now REPAIRS to the sticky sentinel instead of logging 'The
  exemption … stands' and doing so. Same uncertainty the page-2 probe already resolves to
  raced=1; which of the two reads failed is not a difference in what is KNOWN. The repair
  `case` is hoisted out of the readable branch so both converge on it.

Both cost the run its exemption on a transient API failure — the price this file already pays
for the same reason in #751's page-2 rule, recoverable in one command, where a green over a
rejection is not. Mutation-proven: restoring either old behaviour reddens its test.

The withholding guard sits AFTER the mark is established, not with the classification.
$max_id_before does not exist there and referencing it early is an unbound variable under
set -u — the step dies and every PR is stranded with no status. My first placement did exactly
that: 39 tests red. Found by running it, not by reading it.

#849 keeps the third route (post-write verification runs only for state=success, so a generic
pending write can bury a rejection) and the fence's post-POST gap, with its reachability from
this change's class stated rather than dismissed.

scripts/tests: 1014 passed, 2 skipped.

fixes #742

Decisions-Edit: yes
Seventh review round. The previous commit's two post-write fixes are WITHDRAWN — re-review
showed the fix was worse than the gap on both counts, and my own rule applies: two defects
from one mechanism means withdraw, not patch a third time.

* The withheld exemption wrote a GENERIC pending, and a generic pending is exactly what a
  later run re-derives into success. It did not close the burial route, it moved which run
  greens it. Closing it needs a distinct STICKY 'unverified write' sentinel — a new state in
  a machine that already has three.
* That pending has NO RETRY PATH. Verified: review-verdict.yml triggers only on
  pull_request_target types, with no schedule and no workflow_dispatch. A transient API
  failure on a PR's LAST event therefore stalls an exempt PR until a human nudges it. Trading
  a race that needs BOTH a read failure AND a reviewer posting inside the write window, for a
  stall that needs only the read failure, is not obviously the safe direction — and I called
  it a one-run cost, which it is not.

The workflow and its tests are restored byte-for-byte to the previous commit's state; the
whole of post-write verification goes back to #849, which now also carries two further routes
the same round found (a malformed status row makes the verifier's jq error out under set -e,
skipping the repair; and an unreadable combined-status read leaves a pre-existing off-list
success standing, with the job's own red not being a required check).

Also corrects ci.verdict-write-retarget-fence, whose 'What is NOT closed' said the post-POST
gap yields only a TRANSIENT forged green because the retarget fires an `edited` event. That
assumes the
successor finishes last. It need not: the successor can start, see the base-matching failure,
short-circuit and write nothing, CONSUMING the event — after which the stale run posts its
success last and nothing remains to correct it. Permanent, not transient, and now stated so.

What #742 itself does is unbroken after seven rounds. What it changes is the REACHABILITY of
these residuals, and that is recorded in ci.exemption-provenance rather than left implicit.

scripts/tests: 1012 passed, 2 skipped.

fixes #742

Decisions-Edit: yes
Cross-family review of the withdrawal. It confirmed the revert is exact (both blobs match
the pre-attempt commit, no withdrawn stub modes or tests left behind) and found no BLOCKER
or MEDIUM — but four documentation defects, two of them contradictions this branch itself
introduced by correcting a record's body and leaving its summary alone.

Retracted in the workflow, where the claims are load-bearing for the next maintainer:

* "the POST below is still correct, only the verification is weakened" — without a mark the
  race check is skipped entirely, so a verdict in the write window is neither detected nor
  repaired. Now says so, and names the withdrawn attempt so it is not re-attempted the same
  way.
* "pending cannot turn a rejection green, so the only write that can cause the damage is the
  exemption success" — FALSE. A generic pending masks a raced rejection just as well, and
  because it is not the sticky sentinel the next run re-derives it into success with the
  human's row now below that run's mark. The damage arrives one event later, not never.
* "the LAST net before a green stands" — it is the last net on THAT path only; a rejection
  already on the head is below the mark, and the pending route bypasses the block entirely.

Record summaries brought into line with their own bodies:

* ci.verdict-write-retarget-fence and ci.exemption-provenance both still promised the fence
  RESOLVES the retarget residual. It narrows it: a retarget between the final pre-write count
  and the POST leaves a PERMANENT forged green, because the successor can consume the edited
  event and exit before the stale run posts last. Also "overtaken after writing" -> "after the
  final pre-write count", which is where the window actually is.
* ci.exemption-provenance said a lost verdict is re-posted as pending; on an exempt PR it is
  re-derived to an exemption success, the quieter and worse half.
* ci.actions-credential-scoping still described the pre-742 rule (any real creator inherited).
* The rule frontmatter said [[ == ]] makes its RHS a pattern. Only an UNQUOTED RHS does; the
  workflow comment already had this right after being probed on bash 5.3.

No behaviour change. scripts/tests: 1012 passed, 2 skipped.

fixes #742

Decisions-Edit: yes
Sweeping for the RETRACTED wording rather than only the files I had edited — the rule this
repo records as "stale comments: sweep by subject, not verb" — caught two clauses that the
previous commit's own correction contradicts.

ci.exemption-provenance's rule: now says the retarget residual is "NARROWED, not resolved",
and then ended the same sentence with "leaving only the sub-round-trip window that no API
without compare-and-set can close". Both cannot be true. There are TWO windows: the
sub-round-trip one, and the post-final-count/pre-POST one, which is not sub-round-trip and
yields a PERMANENT green because the successor can consume the edited event and exit before
the stale run posts last.

The same record's #706 paragraph carried the same "the sub-round-trip window it describes
survives" framing, with no pointer to the correction. It now points at it.

A retraction that only edits the sentence you remembered writing is the failure mode here:
the previous commit corrected three claims and left two paraphrases of one of them in place,
in the same file.

No behaviour change, and this time MEASURED rather than asserted: the classify step's
executable content (comments and blank lines stripped) is 323 lines at HEAD~1 and 323
identical lines at HEAD.

scripts/tests: 1012 passed, 2 skipped.

fixes #742

Decisions-Edit: yes
Third sweep, and the third time the same mistake: I grepped for the exact wording I had
written and missed the same claim in PARAPHRASE at sites I never opened. Cross-family review
enumerated them. This closes both families by concept.

Family 1 — "the retarget fence resolved the race". Corrected at every site, not just the two
I remembered: the fence record's own rule and its "handoff, not a stall" induction (which is
a property of a run that ABSTAINS and says nothing about one that already passed its final
count and then posts), ci.exemption-provenance's "Resolved 2026-08-03" heading, and
docs/ci-cd.md's "That residual is now fenced". It NARROWS: a retarget between the final
pre-write count and the POST leaves a PERMANENT forged green, because the successor consumes
the edited event and exits before the stale run posts last.

Family 2 — "a pending write is harmless". Retracted in the workflow's untrusted-count
rationale ("for no safety gain"), in the same phrase inside the fence record's rule, and in
docs/ci-cd.md twice, including the absolute "a human BLOCKED can never be silently turned
green". A generic pending blocks the merge IMMEDIATELY, which is what makes letting it
through the right trade — but it is not inert: it masks a rejection landing in its own write
window, post-write verification does not run for it, and a later run re-derives it into an
exemption success with that rejection below the new mark. Letting it through is still right;
the trade is "immediate block, later re-derivation risk", not "free".

Every retraction names #849, which carries the behaviour work.

No behaviour change: the classify step's executable content is identical across all three
docs commits (323 lines, comments and blanks stripped, compared via the YAML-extracted step
rather than line ranges).

scripts/tests: 1012 passed, 2 skipped.

fixes #742

Decisions-Edit: yes
Fourth prose round. Three of the four sites it named are real and fixed; one is a false
positive, recorded so it is not "fixed" later.

Fixed:
* the classify comment heading "THE PROBLEM THIS SOLVES" -> NARROWS, with #849 named;
* ci.verdict-write-retarget-fence's induction, which ended "the induction terminates when
  retargeting stops and the last run writes the final answer". That holds only over runs that
  ABSTAIN. A run that already passed its final count is outside it — it writes whenever it
  gets there — so the run that writes LAST is not necessarily the one that classified last;
* CLAUDE.md's "so a rejection is never re-derived into a green", which is MY sentence from
  this branch and is overbroad: it is true of a rejection VISIBLE AT THE FIRST READ, not of
  one landing inside a run's own write window;
* two test docstrings carrying the retracted "withholding pending buys no safety", and a
  scope note on test_a_REAL_human_verdict_is_still_NEVER_overwritten, whose NAME asserts a
  guarantee wider than what it measures.

NOT fixed, deliberately: process.check-and-use-pins-a-version's title says "the gap is stated
and fenced". That is the generic check/action version-binding rule and has nothing to do with
the retarget fence. Matching on the word rather than the concept is what put it on the list.

AND THE BOUNDARY, recorded in #849 rather than left to the next round to rediscover. Each
round corrected the sites it could see and the next found more, because these claims exist in
paraphrase across corpus prose written before #742. #742 has now corrected every site that is
its own text or directly about the allow-list, plus the fence/pending claims in the four docs
and the classify comments. Remaining corpus prose describing the #706 fence's or the
post-write verification's guarantees belongs to #849 — those sentences are wrong only because
of the residual analysis in that issue, so they should be corrected when the behaviour lands,
rather than leaving prose that promises a property no code yet has. #849 has a Done-when box
for it.

No behaviour change. scripts/tests: 1012 passed, 2 skipped.

fixes #742

Decisions-Edit: yes
test(742): the Route 3 section header names the allow-list, not attributability
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 13s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 20s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 13s
PR Gates / Docs update reminder (pull_request) Successful in 38s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Failing after 22s
PR Gates / decisions lifecycle (pull_request) Successful in 46s
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 8s
review-verdict/h10 Awaiting review verdict for 3817414
probe742/inherit Review-verdict: MERGEABLE @ 3817414 (base: main)
Review verdict / Set review-verdict status (pull_request_target) Successful in 20s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m55s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Canceled after 4s
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
3817414366
Last finding from a clean cold round (VERDICT: MERGEABLE, nine mutations applied and every
one reddened). The banner said an inherited success is re-derived unless it is an
'attributable human verdict' — since this change, attributable alone is the weaker test that
governs an inherited FAILURE; a success additionally needs allow-list membership. In scope by
this branch's own boundary rule: it is prose about a property this change alters.

scripts/tests: 1012 passed, 2 skipped.
timothy added 1 commit 2026-08-27 04:25:21 +02:00
style(742): ruff format the new tests with the CI-pinned version
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 13s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 26s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 15s
PR Gates / Docs update reminder (pull_request) Successful in 29s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 35s
PR Gates / decisions lifecycle (pull_request) Successful in 36s
Review verdict / Set review-verdict status (pull_request_target) Successful in 37s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 6m17s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m47s
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 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
review-verdict/h10 Review-verdict: MERGEABLE @ 586577c (base: main)
586577c6f6
CI's 'Script lint and tests' went red on `ruff format --check`. `ruff check` was clean and
the suite was green throughout — this was purely formatting of lines this branch added, and
it is a gap in MY local gate: I ran pytest and the decisions validator every round and never
ran the formatter.

Formatted with ruff 0.12.11, the version pr-checks.yml pins, rather than the local install —
the pin exists precisely so the verdict is not a function of whoever ran it, and a different
version could have reformatted lines this branch never touched. The diff is confined to
blocks added here (a re.subn call, two assertion strings, two parametrize entries).

Verified over the whole CI population, not just the file I edited:
git ls-files '*.py' | xargs ruff check   -> All checks passed
git ls-files '*.py' | xargs ruff format --check -> 43 files already formatted

scripts/tests: 1012 passed, 2 skipped.
Author
Owner

Review-verdict: MERGEABLE @ 586577c

11 cold review rounds, 6 cross-family; final verdict clean with 9 mutations all reddening. Residuals deferred to #845 and #849.

Review-verdict: MERGEABLE @ 586577c 11 cold review rounds, 6 cross-family; final verdict clean with 9 mutations all reddening. Residuals deferred to #845 and #849.
timothy merged commit d68ce42fcb into main 2026-08-27 04:52:03 +02:00
timothy deleted branch fix/742-verdict-allowlist 2026-08-27 04:52:04 +02:00
Sign in to join this conversation.