Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 35s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 57s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 37s
PR Gates / Docs update reminder (pull_request) Successful in 1m0s
PR Gates / decisions lifecycle (pull_request) Successful in 20s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 17s
review-verdict/h10 Review-verdict: MERGEABLE @ a7d91bf (base: main)
Review verdict / Set review-verdict status (pull_request_target) Successful in 45s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m17s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 19m27s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 6m4s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
`docs.no-session-narrative` reaches every durable artifact, but its detector scanned only `docs/**/*.md` and root markdown, and nothing had ever swept the rest. The issue named four sites from one grep and called them a floor. Deriving the population instead — a whitespace-joined sweep over every tracked file outside the detector, for the detector's own phrasings plus the attribution and review-round class #812 found — gave 453 sites in 108 files at `fb5592971`, and a second pass for phrasings the first list missed (hyphenated `round-N`, "an earlier version", "the reviewer proved") added residuals in the same files. Every site was classified with #812's three dispositions (CUT / SEVER / KEEP with its sub-kind) under the who-benefits test; the per-site manifests are on the PR. The rejected designs, tested-and-rejected fixtures, measurements and traps stay; the attribution of who found them and the round in which they were found go. The detector's population grows to `.claude/`, `.gitea/`, `.husky/` and `scripts/` regardless of extension, minus the detector and its own test (whose fixtures ARE the phrasings) and minus `scripts/tests/fixtures/` (test data, including decision-record copies — the same reasoning as the records' own exemption, and what keeps the record's depth measurement true), and `--all` lists tracked REGULAR files only — a symlink's content is its target and a gitlink has none. The #812 argument for leaving `docs/superpowers/**` in the population runs the other way here: `--diff` sees only ADDED lines, and 287 of the 453 sites were under 30 days old — this corpus is where narrative is being added, so the advisory nudge has reach. Density agrees: 56 line-mode hits over the 113 regular files the predicate admits, against 9 over 66 docs files before #812. `web/` and C# stay out on the same measurement (3 of 74 PATTERNS-matching sites, ~4,600 files). The predicate did not grow: PATTERNS matched 74 of 453 sites, and widening the word list to the attribution class is the treadmill the withdrawn parity test ran on. The population oracle is restated over segments with the new arms, the synthetic cross product gains the process heads and non-markdown extensions, a fixture witnesses that a tracked symlink is neither scanned nor counted, a `.py.bak` axis separates a by-name exemption from a `startswith` over the same tuple, and eight mutants (drop the process arm, drop the by-name exemption, exempt by `startswith`, drop or add a prefix, drop the fixtures exemption, list only markdown, drop the symlink filter, test the mode per row instead of per path) each redden it. A pre-existing silent drop in `--diff` goes with it: git tab-terminates a `+++` filename that contains a space, and the kept tab made `is_scanned_path` refuse the file with no notice — fixed, with a positive control and its own mutant. Code is unchanged by construction, measured per file type against `origin/main`: Python modules are AST-equal with docstrings stripped, except `#` lines inside the embedded fixture programs (string literals) of three test modules; workflows differ only in `#` lines inside `run:` block scalars; shell, C#, TypeScript and jq are equal with comment lines stripped. The stated exceptions: the detector and its test, 26 vitest titles that carried review-round or severity labels or a reviewer attribution (call sites whose title changed — every changed title line walked back to its `it(` / `it.each(...)(` anchor, so a `' + '` concatenation counts once), two registry note strings and the mutation manifest's prose fields. scripts/tests: 1565 passed. Web: lint, typecheck, 1319 tests green. Closes #876. Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEcBoFw7ctrf3Nb7R7x7wk
256 lines
16 KiB
Bash
256 lines
16 KiB
Bash
# shellcheck shell=bash
|
|
# h10-reviewers.sh — WHOSE verdicts the H10 gate will inherit, DERIVED from the gate itself
|
|
# (ersatztv#845).
|
|
#
|
|
# SOURCED, NEVER EXECUTED. It defines pure functions over one file and takes no action of its own,
|
|
# so it is deliberately not executable and carries no shebang.
|
|
#
|
|
# WHY THIS FILE EXISTS. `.gitea/workflows/review-verdict.yml` inherits an existing
|
|
# `review-verdict/h10=success` only when the status's `.creator.login` is a member of its
|
|
# `H10_REVIEWERS` allow-list (ersatztv#742). `scripts/post-review-verdict.sh` is the tool that WRITES
|
|
# those verdicts, with whatever account owns `ETV_GITEA_TOKEN` / `ETV_GITEA_BASICAUTH`. The two
|
|
# values are coupled and nothing asserted the coupling: the day the writer credential stops being an
|
|
# allow-listed account — a service token, a second machine, a renamed account, a bot minted for
|
|
# automation — every verdict is still WRITTEN, the tool still reports success, and the gate silently
|
|
# re-derives it on the next PR event. A repo-wide merge stall whose only diagnostic was a
|
|
# `::warning::` in a workflow run nobody reads.
|
|
#
|
|
# THERE IS NO SECOND COPY OF THE LIST HERE, on purpose. The workflow literal is the ONE declaration
|
|
# and this file DERIVES from it, the way `review-verdict-vocabulary.sh` made one word list serve both
|
|
# the read and the write side (ersatztv#788). A hand-copied allow-list held together by a parity test
|
|
# is the shape that record spent six rounds failing to converge on.
|
|
#
|
|
# WHY A PARSE RATHER THAN A SHARED DECLARATION BOTH SIDES SOURCE. That would be the better shape and
|
|
# it is not available. The gate runs under `pull_request_target` against a checkout of the PR BASE
|
|
# sha, so a PR whose base predates the introduction of a new `scripts/lib/` file would not have it —
|
|
# the same condition the `jq preflight` step already handles by skipping. Under `set -euo pipefail` a
|
|
# missing source is a dead job, and a dead job posts no `review-verdict/h10` at all, which blocks
|
|
# EVERY merge to `main` including the PR that would repair it (`block_admin_merge_override: true`,
|
|
# ersatztv#743). Writing a fallback list for that case reintroduces the second copy. So the
|
|
# derivation runs on the side that can fail safely — this one — and the gate body is untouched.
|
|
#
|
|
# WHAT THE PARSE IS ALLOWED TO BE. Exactly one anchored assignment line, or a refusal. That is not a
|
|
# shell parser and does not try to be: `testing.guard-derives-population-from-source` records why a
|
|
# regex over shell source does not converge when the target is an open-ended construct. The target
|
|
# here is closed — `test_pr_changed_files.py::test_the_H10_REVIEWERS_list_is_glob_free_and_non_empty`
|
|
# reads the same literal, over the SAME POPULATION (the whole file), with the same anchors, and
|
|
# already fails the suite if the workflow stops declaring it as exactly one double-quoted
|
|
# assignment. So a shape change reddens CI on the PR that makes it rather than going stale here.
|
|
#
|
|
# That guard used to scope itself to the classify step's `run:` body, which is NOT the same tree this
|
|
# file walks: a second anchored assignment in any other `run:` block left it green while making this
|
|
# refuse every verdict. Measured before the fix: the guard green, and about half the writer suite red.
|
|
# BE PRECISE ABOUT WHAT WAS MISSING, because the obvious phrasing is wrong twice over. Those failures
|
|
# DO carry this library's refusal, which names the count and the variable. And alignment does not
|
|
# REPLACE them — it ADDS a named guard failure alongside them. What was missing is a failure that
|
|
# POINTS AT THE WORKFLOW: the reds surface under unrelated test names
|
|
# (`test_positive_verdict_posts_success_on_the_resolved_head`, …), so the cause had to be inferred
|
|
# from a stack of them.
|
|
#
|
|
# NO FIGURE IS GIVEN FOR HOW MANY NAME IT, deliberately, and the trap is worth stating so nobody
|
|
# adds one back. A `grep -c` over pytest output counts LINES, not failures, and `--tb=line` prints
|
|
# each matching failure twice — so that count overstates. Truncation moves it the other way: raising
|
|
# the traceback detail makes many more of the failures show the refusal text. Any figure here is
|
|
# therefore a property of the pytest flags used to produce it, not of the code, and reproducing one
|
|
# requires parsing per-failure sections rather than grepping lines. "About half the suite reddens"
|
|
# survives because it is coarse enough to be robust to all of that.
|
|
#
|
|
# BE EXACT ABOUT THE CONSEQUENCE, because the obvious phrasing overstates it. Such an edit landing on
|
|
# `main` WOULD be a repo-wide merge outage — no verdict could be posted, and `review-verdict/h10` is
|
|
# a required check on a PR-only `main`. But it is not a silent route in: the writer suite is in the
|
|
# `script-tests` job, which goes red on that edit. That job is ADVISORY (measured 2026-08-30, the
|
|
# required contexts are `Build & test`, `EF migration integrity` and `review-verdict/h10`), so it
|
|
# does not block server-side; what it does is make the PR visibly red. The reachable path to the
|
|
# outage is therefore the snapshot boundary above — a verdict posted from a checkout that predates
|
|
# the edit — not the guard being green. Aligning the populations buys a legible diagnostic on the PR
|
|
# that makes the edit, which is worth having and is not the same as being the only thing standing
|
|
# between the repo and an outage.
|
|
#
|
|
# SNAPSHOT BOUNDARY, and its DIRECTION, because the direction is the part that matters. This reads
|
|
# the workflow in the LOCAL checkout, while the gate that actually runs is the one at `origin/main`
|
|
# (base-resolved). On a branch that edits `H10_REVIEWERS` the two differ until it merges, and the
|
|
# likely edit is ADDING a reviewer — for which this check ACCEPTS an account the live gate will not
|
|
# yet inherit from. That is a false accept: the #845 stall, reached through the very check meant to
|
|
# catch it, on the one branch where someone is thinking about the allow-list.
|
|
#
|
|
# It is accepted rather than closed, with the reasons stated. A live remote read of the base
|
|
# workflow would close it and puts a network dependency on the merge-consent write path, where a
|
|
# read failure has to resolve either to a refusal (a new way to block every verdict) or to a pass
|
|
# (the hole, reopened). The window is one PR long, the operator making the edit is the one person
|
|
# who knows the list is in flux, and the residual is a STALL, never a merge of unreviewed code. The
|
|
# direction is what is written down; a future reader deciding whether to close it should start here.
|
|
#
|
|
# NO ENVIRONMENT VARIABLE SELECTS THE SOURCE FILE. The path is derived from this file's own
|
|
# location, and a caller may pass one explicitly; there is deliberately no `ETV_*_WORKFLOW`
|
|
# override, because an env var that chooses which tree answers a security question is an unsound
|
|
# input (ersatztv#858 is the same shape one file over).
|
|
|
|
# --- The default source: the shipped gate definition. --------------------------------------------
|
|
# Resolved relative to THIS FILE, never to the caller's cwd — `post-review-verdict.sh` is run from
|
|
# whatever directory an operator happens to be in.
|
|
ETV_H10_REVIEWERS_WORKFLOW=''
|
|
if _etv_h10_root=$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." 2>/dev/null && pwd); then
|
|
ETV_H10_REVIEWERS_WORKFLOW="$_etv_h10_root/.gitea/workflows/review-verdict.yml"
|
|
fi
|
|
unset _etv_h10_root
|
|
|
|
# --- The validation sentinel. --------------------------------------------------------------------
|
|
# Reset to 0 HERE, at load time, so that "derived successfully" is a DATA dependency the query
|
|
# function requires rather than a control-flow step a caller is trusted to have taken.
|
|
#
|
|
# ATTRIBUTED CAREFULLY, because the obvious second reason is not this line's. That reason is that
|
|
# the reset defeats an inherited `ETV_H10_REVIEWERS_OK=1` from the environment, backed by a
|
|
# measurement showing a hostile environment does not get a verdict past the writer. The measurement
|
|
# is real; the conclusion does not follow. Removing this reset produces a byte-identical refusal,
|
|
# because `etv_h10_reviewers_load` runs on that path and overwrites both variables anyway. What the
|
|
# reset actually covers is a caller that never reaches `load` at all — the data-dependency reason
|
|
# above, and what `etv_h10_reviewers_contains` returning 2 rests on. So the sentinel earns its place
|
|
# on that ground alone.
|
|
#
|
|
# THE SKIP-PAST-THE-GATE ROUTE IS REAL BUT NOT REACHABLE HERE, which is why the sentinel is kept
|
|
# without repeating that rationale. `review-verdict-vocabulary.sh` documents the mechanism; the
|
|
# scoping below is stated here and measured here, on bash 3.2.57 and 5.3.15. Under `set -u` and
|
|
# WITHOUT `set -e`, a `${#arr[@]}` on an unset name or a scalar aborts the FUNCTION only, so an
|
|
# `if ! validate` gate takes neither branch and execution continues past it. That is its consumer's situation (`check-review-verdict.sh` runs `set -uo pipefail`).
|
|
# It is NOT this file's: `post-review-verdict.sh` runs `set -euo pipefail`, where the shell exits on
|
|
# any of those shapes. So the sentinel here earns its place on the data-dependency ground alone.
|
|
ETV_H10_REVIEWERS_OK=0
|
|
ETV_H10_REVIEWERS=''
|
|
|
|
# --- The split idiom, stated once. ---------------------------------------------------------------
|
|
# Both functions below turn the space-separated list into the positional parameters with PATHNAME
|
|
# EXPANSION OFF, then restore `-f` to whatever the caller had. It is written out at each site rather
|
|
# than factored into a helper because a helper cannot set its caller's positional parameters, and
|
|
# returning the words through a string would reintroduce a split — of a value that may contain a
|
|
# newline — at the other end.
|
|
#
|
|
# Getting this wrong is the documented fail-open, not a style point: `for x in $list` and
|
|
# `$(printf %s "$list")` both glob, so a `*` entry expands to the FILENAMES around it, and any of
|
|
# those matching the plain-login class below would validate cleanly and enter the allow-list as a
|
|
# real reviewer. That is the defect `review-verdict-vocabulary.sh` records against its own word list,
|
|
# reached one file over through the allow-list.
|
|
#
|
|
# WHICH SITE CARRIES THAT RISK: the one in `etv_h10_reviewers_load`, which splits a value NOTHING has
|
|
# validated yet. By the time `etv_h10_reviewers_contains` runs, the plain-login class has already
|
|
# excluded every metacharacter, so its `set -f` is defence in depth against a future edit that
|
|
# reorders or weakens the validation — not the primary guard. Said plainly because "this is the
|
|
# fail-open" attached to both sites would misdirect the next reader about which line to keep.
|
|
#
|
|
# `IFS` IS PINNED TOO. Globbing is only half of what an unquoted expansion depends on; the other
|
|
# half is the ambient `IFS`, and a caller that had set `IFS=-` turned `renovate-bot` into two
|
|
# entries, making `renovate` read as a member. Not reachable from `post-review-verdict.sh` today
|
|
# (bash resets `IFS` at startup, so the environment route is closed, and the script never assigns
|
|
# it), but this file invites reuse and the cost is one word per function.
|
|
|
|
# --- The derivation. -----------------------------------------------------------------------------
|
|
# Sets `ETV_H10_REVIEWERS` and the sentinel. Returns 0 when the allow-list is usable, 1 otherwise
|
|
# (message on stderr). It never exits: the consumer decides its own exit semantics.
|
|
#
|
|
# $1 — the workflow file to read; defaults to the shipped one resolved above.
|
|
etv_h10_reviewers_load() {
|
|
local src="${1:-${ETV_H10_REVIEWERS_WORKFLOW:-}}"
|
|
local count value entry n=0 glob_was_off=0
|
|
# Pin the split: the caller's IFS is not this function's business to inherit.
|
|
local IFS=$' \t\n'
|
|
|
|
ETV_H10_REVIEWERS_OK=0
|
|
ETV_H10_REVIEWERS=''
|
|
|
|
if [ -z "$src" ]; then
|
|
printf 'h10 reviewers: could not resolve the gate definition from the location of this library, and no path was passed\n' >&2
|
|
return 1
|
|
fi
|
|
if [ ! -r "$src" ]; then
|
|
printf 'h10 reviewers: %s is missing or unreadable, so who the gate accepts cannot be derived\n' "$src" >&2
|
|
return 1
|
|
fi
|
|
|
|
# COUNT the anchored assignments, never `grep -q`. Two separate reasons, both recorded because each
|
|
# has cost this repo a round: `grep -q` closes the pipe early and inverts under `pipefail` on large
|
|
# input (`ci.grep-q-pipefail-inversion`), and — the reason that matters here — a boolean cannot
|
|
# tell ONE assignment from TWO. The shell runs the LAST assignment executed, so a second or
|
|
# conditional one would leave this deriving a value the gate does not use. `grep -c` reads a FILE
|
|
# argument, so there is no pipe to invert in the first place.
|
|
count=$(grep -cE '^[[:space:]]*H10_REVIEWERS="[^"]*"[[:space:]]*$' "$src") || count=0
|
|
if [ "$count" != 1 ]; then
|
|
printf 'h10 reviewers: %s carries %s anchored H10_REVIEWERS assignments, not exactly 1 — refusing to guess which one the gate runs with\n' "$src" "$count" >&2
|
|
return 1
|
|
fi
|
|
|
|
value=$(sed -n 's/^[[:space:]]*H10_REVIEWERS="\([^"]*\)"[[:space:]]*$/\1/p' "$src") || value=''
|
|
|
|
# Validate every entry against the same character class the workflow's own lint enforces. It is not
|
|
# cosmetic on either side: the gate membership loop word-splits the list UNQUOTED, so `*`, `?` or
|
|
# `[` there would be matched against the runner cwd and could silently change who the gate accepts.
|
|
case "$-" in *f*) glob_was_off=1 ;; esac
|
|
set -f
|
|
# shellcheck disable=SC2086 # deliberate word-split of the space-separated list, globbing off
|
|
set -- $value
|
|
[ "$glob_was_off" = 1 ] || set +f
|
|
for entry in "$@"; do
|
|
case "$entry" in
|
|
*[!A-Za-z0-9._-]*)
|
|
printf 'h10 reviewers: %s is not a plain login; * ? and [ are glob metacharacters in the membership loop of the gate\n' "$entry" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
n=$((n + 1))
|
|
done
|
|
|
|
# Emptiness is checked on the SPLIT COUNT, not on the raw string. A value of " " is non-empty as
|
|
# a string and splits to nothing, and an emptied list is not a harmless default: the gate loop
|
|
# `for rv in ""` iterates zero times, so every verdict from every account would stop being
|
|
# inherited and `pending` would be re-posted over real human verdicts forever.
|
|
if [ "$n" -eq 0 ]; then
|
|
printf 'h10 reviewers: %s declares an EMPTY H10_REVIEWERS — no verdict from any account could be inherited\n' "$src" >&2
|
|
return 1
|
|
fi
|
|
|
|
ETV_H10_REVIEWERS="$value"
|
|
# The ONLY assignment to 1 in this file, and it is the last statement of the successful path. Any
|
|
# earlier `return 1`, and any abort part-way through, leaves the sentinel at 0.
|
|
ETV_H10_REVIEWERS_OK=1
|
|
return 0
|
|
}
|
|
|
|
# --- Derived view: is this login one whose verdict the gate would inherit? -----------------------
|
|
# THREE outcomes, deliberately, because two would conflate them: 0 = a member, 1 = NOT a member,
|
|
# 2 = CANNOT ANSWER (the derivation never ran, or failed). A caller that treats "cannot answer" as
|
|
# "not a member" turns a broken checkout into a refusal, which is survivable; one that treats it as
|
|
# "member" turns it into a silent pass, which is the failure this file exists to prevent. Both are
|
|
# wrong to guess at, so the third code makes the caller say which it meant.
|
|
etv_h10_reviewers_contains() {
|
|
local login="${1:-}" entry glob_was_off=0
|
|
# Pin the split: the caller's IFS is not this function's business to inherit.
|
|
local IFS=$' \t\n'
|
|
|
|
if [ "${ETV_H10_REVIEWERS_OK:-0}" != 1 ]; then
|
|
printf 'h10 reviewers: the allow-list was never derived (call etv_h10_reviewers_load first)\n' >&2
|
|
return 2
|
|
fi
|
|
|
|
case "$-" in *f*) glob_was_off=1 ;; esac
|
|
set -f
|
|
# shellcheck disable=SC2086 # deliberate word-split of the space-separated list, globbing off
|
|
set -- $ETV_H10_REVIEWERS
|
|
[ "$glob_was_off" = 1 ] || set +f
|
|
|
|
for entry in "$@"; do
|
|
# POSIX `[ = ]`, exactly as the gate compares. Never `[[ $entry == $login ]]` — an unquoted
|
|
# right-hand side under `[[ ]]` is a PATTERN, not a literal — and never `grep -xF`, because an
|
|
# API-supplied creator containing a newline would read as several patterns.
|
|
if [ "$entry" = "$login" ]; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# --- Reached-the-end marker. ---------------------------------------------------------------------
|
|
# A top-level `exit` in a SOURCED file terminates the sourcing script where it stands, so no check
|
|
# placed after the `source` can ever run. The consumer therefore sources this file in a SUBSHELL
|
|
# first and requires the marker below on stdout; a file that exits early never prints it. Defining a
|
|
# function is not enough (a truncated file can define every function and still stop short), so this
|
|
# is the LAST line and it must stay last.
|
|
etv_h10_reviewers_loaded() { printf 'etv-h10-reviewers-loaded\n'; }
|