Closes two issues that are one mechanism pulling in opposite directions.
#786 asked for inline workflow-job guards to be enumerated using "the same
reviewed-registry-cross-checked-both-ways shape TOOLCHAIN_JOBS uses". #789 says that exact
shape is the defect and must be deleted. Working #786 as written would have built the artifact #789
removes — so they are fixed together, in #789's direction.
The mechanism
Every job now declares metadata about itself, and both populations derive from it:
Marker
Values
Scope
env.CI_EXECUTION_CLASS
toolchain / bare-runner
docker-build.yml
env.CI_JOB_ROLE
guard / advisory / none
all six workflows
Missing or unknown values are hard failures in both checkers.
TOOLCHAIN_JOBS / BARE_RUNNER_JOBS deleted. The literal had a real argument behind it: set
equality between two derived sets is blind to a member leaving both at once, so a job losing its container: block leaves the declared and pinned sets balanced. That needs an anchor that does not
move with the block. The marker is that anchor, better placed — claim and reality in one diff hunk.
Its cost is adjacency, and it is paid rather than argued away. A marker four lines from the container: block can be deleted with it, where a distant literal could not. So the file derives the
requirement a third time — from each job's own step bodies — independent of both marker and
block. That is also the only check that sees the failure #789 actually filed: a .NET step moved
into a bare-runner job, where no set changes at all. It is a necessary condition only
(functional-e2e reaches dotnet via scripts/e2e-local.sh) and says so.
New scripts/tests/test_workflow_job_guards.py asserts set equality both ways between
guard-declaring jobs and a new Workflow-job guards table in docs/guard-inventory.md. The scope
limit that said this class was uncovered is updated, with its two residuals stated (classification
correctness is not proven; an inline assertion carries Proof: NONE honestly).
The four jobs #786 named as lacking a dropped-step guard each get a recorded decision. None is a
required context, so a drop cannot send a required check green; ci-step-ran.sh is deliberately not
widened.
Two corrections to the issues, measured not assumed
#789's sequencing hazard is stale.expected="$(git log -1 --format=%H -- docker/ci)" — the
pathspec is docker/ci only. Verified: docker-build.yml was last touched by main's HEAD and expected still resolves to 32747a0, matching the pin. No publish-then-pin two-step was needed.
guard = the job's output is a verdict. none = its output is an artifact and a red means
the build did not work, not that an invariant was violated. That leaves exactly three none jobs —
the two image publishers and renovate — and docs-reminder as the sole advisory (all three of
its check steps are continue-on-error).
A rejected alternative is recorded in the doc so it is not re-adopted: "a guard enforces an invariant
about the repository, so a job exercising the product is none" reads as more principled and is
wrong — it puts test and migrations outside the table, and those are the two required status
contexts on main, i.e. exactly where a failure to fire is fail-open against branch protection.
Review
Three cold review rounds, alternating model families (Claude / Codex gpt-5.6 / Claude), each from a
cold review-only brief in an isolated worktree. Severity converged BLOCKER/HIGH → MEDIUM → LOW/NIT. Every finding is fixed and demonstrated, not asserted.
Round 1 — the guard/none line excluded both required status contexts (rule replaced with
verdict-vs-artifact; three markers flipped); two positive controls this branch added were vacuous
(2N == 2N, true at N=0) while the pre-existing sibling they were copied from was not; a mechanism
claim in the decision record was refuted by execution; the adjacency residual was understated.
Round 2 (cross-family) — the retracted rule still shipped as active in three further sites
including the decision record and the generated catalog, each naming test as none when it ships
as guard; duplicate table rows collapsed silently into a dict; the toolchain-preflight "Blocks"
cell was false (nothing needs: it).
Round 3 (both families, independently) — the two new MUTATION-named proofs passed with the
check they named fully disarmed: they rebuilt the comparison themselves instead of calling the
shipped code. And test_a_commented_out_marker_does_NOT_count claimed to guard a text-scan
regression that a reviewer demonstrated goes entirely undetected. Both are the branch's own defect
class reproduced by its own fixes. duplicate_row_faults / env_shadow_faults are now named
detectors that the live test and its proof both call, and declared_roles takes an injectable
population so the fixture test drives the real reader — each verified by reproducing the
reviewers' own demonstrations against the fix.
Round 3 also renamed the marker value advisory → report-only: docs/ci-cd.md already calls functional-e2e an advisory job in a different sense, for the one job whose marker is guard.
The rename immediately tripped the row regex (REPORT-ONLY has a hyphen, the Kind group was [A-Z]+) — surfacing as a loud red naming the job, which is the fail-closed property documented one
commit earlier, working.
The recurring defect class throughout was prose drifting from code — over half the findings.
Every factual claim in the changed docs and comments has since been re-verified by execution:
tool list vs docker/ci/Dockerfile, required-context strings vs required-status-contexts.json,
marker-to-container: distances (6, 58, 6, 7, 7 — so "same diff hunk" was wrong and is now stated
as independence rather than proximity), continue-on-error counts, every row's Assertion cell
against the workflow, and every cited script against its own Kind.
Verification
scripts/tests: 1175 passed, 2 skipped on the rebased tree (baseline on main: 1083).
Three real mutations of docker-build.yml witnessed red: container block removed, marker removed, dotnet step planted in a bare-runner job.
The new checker's declared mutation runs as its own harness case
(test_MUTATION_the_declared_clause_reddens_the_named_proof[scripts/tests/test_workflow_job_guards.py]).
ruff check / ruff format --check clean on ruff 0.12.11 (CI's pin); decisions-validate: OK; check-doc-narrative --diff origin/main → 0 warnings.
Docs
docs/ci-cd.md → "Per-job declarations"; docs/guard-inventory.md (new section + scope limit +
counts); docs/README.md task-signal row; new decision record testing.workflow-declares-its-own-job-metadata; catalog regenerated.
Closes two issues that are one mechanism pulling in opposite directions.
**#786** asked for inline workflow-job guards to be enumerated using *"the same
reviewed-registry-cross-checked-both-ways shape `TOOLCHAIN_JOBS` uses"*. **#789** says that exact
shape is the defect and must be deleted. Working #786 as written would have built the artifact #789
removes — so they are fixed together, in #789's direction.
## The mechanism
Every job now declares metadata about itself, and both populations derive from it:
| Marker | Values | Scope |
| --- | --- | --- |
| `env.CI_EXECUTION_CLASS` | `toolchain` / `bare-runner` | `docker-build.yml` |
| `env.CI_JOB_ROLE` | `guard` / `advisory` / `none` | all six workflows |
Missing or unknown values are **hard failures** in both checkers.
## #789 — the literals are gone
`TOOLCHAIN_JOBS` / `BARE_RUNNER_JOBS` deleted. The literal had a real argument behind it: set
equality between two *derived* sets is blind to a member leaving both at once, so a job losing its
`container:` block leaves the declared and pinned sets balanced. That needs an anchor that does not
move with the block. The marker is that anchor, better placed — claim and reality in one diff hunk.
**Its cost is adjacency, and it is paid rather than argued away.** A marker four lines from the
`container:` block can be deleted with it, where a distant literal could not. So the file derives the
requirement a **third** time — from each job's own step bodies — independent of both marker and
block. That is also the only check that sees the failure #789 actually filed: a .NET step **moved**
into a bare-runner job, where no set changes at all. It is a necessary condition only
(`functional-e2e` reaches `dotnet` via `scripts/e2e-local.sh`) and says so.
## #786 — guard JOBS join a population
New `scripts/tests/test_workflow_job_guards.py` asserts set equality both ways between
guard-declaring jobs and a new **Workflow-job guards** table in `docs/guard-inventory.md`. The scope
limit that said this class was uncovered is updated, with its two residuals stated (classification
correctness is not proven; an inline assertion carries `Proof: NONE` honestly).
The four jobs #786 named as lacking a dropped-step guard each get a recorded decision. None is a
required context, so a drop cannot send a required check green; `ci-step-ran.sh` is deliberately not
widened.
## Two corrections to the issues, measured not assumed
- **#789's sequencing hazard is stale.** `expected="$(git log -1 --format=%H -- docker/ci)"` — the
pathspec is `docker/ci` only. Verified: `docker-build.yml` was last touched by `main`'s HEAD and
`expected` still resolves to `32747a0`, matching the pin. No publish-then-pin two-step was needed.
- **#786's "15 jobs across 6 workflows" is 17.**
## Classification
`guard` = the job's output is a **verdict**. `none` = its output is an **artifact** and a red means
the build did not work, not that an invariant was violated. That leaves exactly three `none` jobs —
the two image publishers and `renovate` — and `docs-reminder` as the sole `advisory` (all three of
its check steps are `continue-on-error`).
A rejected alternative is recorded in the doc so it is not re-adopted: "a guard enforces an invariant
about the *repository*, so a job exercising the *product* is `none`" reads as more principled and is
wrong — it puts `test` and `migrations` outside the table, and those are the two **required** status
contexts on `main`, i.e. exactly where a failure to fire is fail-open against branch protection.
## Review
Three cold review rounds, alternating model families (Claude / Codex gpt-5.6 / Claude), each from a
cold review-only brief in an isolated worktree. Severity converged
**BLOCKER/HIGH → MEDIUM → LOW/NIT**. Every finding is fixed and demonstrated, not asserted.
**Round 1** — the `guard`/`none` line excluded both required status contexts (rule replaced with
verdict-vs-artifact; three markers flipped); two positive controls this branch added were vacuous
(`2N == 2N`, true at N=0) while the pre-existing sibling they were copied from was not; a mechanism
claim in the decision record was **refuted by execution**; the adjacency residual was understated.
**Round 2 (cross-family)** — the retracted rule still shipped as *active* in three further sites
including the decision record and the generated catalog, each naming `test` as `none` when it ships
as `guard`; duplicate table rows collapsed silently into a dict; the `toolchain-preflight` "Blocks"
cell was false (nothing `needs:` it).
**Round 3 (both families, independently)** — the two new `MUTATION`-named proofs **passed with the
check they named fully disarmed**: they rebuilt the comparison themselves instead of calling the
shipped code. And `test_a_commented_out_marker_does_NOT_count` claimed to guard a text-scan
regression that a reviewer demonstrated goes entirely undetected. Both are the branch's own defect
class reproduced by its own fixes. `duplicate_row_faults` / `env_shadow_faults` are now named
detectors that the live test and its proof both call, and `declared_roles` takes an injectable
population so the fixture test drives the real reader — each verified by **reproducing the
reviewers' own demonstrations** against the fix.
Round 3 also renamed the marker value `advisory` → `report-only`: `docs/ci-cd.md` already calls
`functional-e2e` an advisory job in a *different* sense, for the one job whose marker is `guard`.
The rename immediately tripped the row regex (`REPORT-ONLY` has a hyphen, the Kind group was
`[A-Z]+`) — surfacing as a loud red naming the job, which is the fail-closed property documented one
commit earlier, working.
The recurring defect class throughout was **prose drifting from code** — over half the findings.
Every factual claim in the changed docs and comments has since been re-verified by execution:
tool list vs `docker/ci/Dockerfile`, required-context strings vs `required-status-contexts.json`,
marker-to-`container:` distances (6, 58, 6, 7, 7 — so "same diff hunk" was wrong and is now stated
as independence rather than proximity), `continue-on-error` counts, every row's Assertion cell
against the workflow, and every cited script against its own `Kind`.
## Verification
- `scripts/tests`: **1175 passed, 2 skipped** on the rebased tree (baseline on `main`: 1083).
- Three real mutations of `docker-build.yml` witnessed red: container block removed, marker removed,
`dotnet` step planted in a bare-runner job.
- The new checker's declared mutation runs as its own harness case
(`test_MUTATION_the_declared_clause_reddens_the_named_proof[scripts/tests/test_workflow_job_guards.py]`).
- `ruff check` / `ruff format --check` clean on ruff 0.12.11 (CI's pin); `decisions-validate: OK`;
`check-doc-narrative --diff origin/main` → 0 warnings.
## Docs
`docs/ci-cd.md` → "Per-job declarations"; `docs/guard-inventory.md` (new section + scope limit +
counts); `docs/README.md` task-signal row; new decision record
`testing.workflow-declares-its-own-job-metadata`; catalog regenerated.
fixes #786
fixes #789
together. #786 asked for inline workflow-job guards to be enumerated using "the same
reviewed-registry-cross-checked-both-ways shape TOOLCHAIN_JOBS uses"; #789 said that
exact shape is the defect and must be deleted. Building #786 as written would have
constructed the artifact #789 removes.
Both now derive from a marker the workflow declares about itself:
env.CI_EXECUTION_CLASS toolchain | bare-runner (docker-build.yml)
env.CI_JOB_ROLE guard | advisory | none (every tracked workflow)
Missing or unknown values are hard failures in both checkers — a marker scheme whose
absent value defaults to a safe class stops applying the moment a job is added and
nobody notices.
it: set equality between two DERIVED sets is blind to a member leaving both at once, so
a job losing its container: block leaves the declared and pinned sets balanced. The
population needs an anchor that does not move with the block. The marker is that anchor
and is better placed — claim and reality in one diff hunk.
Its cost is adjacency, and is paid rather than argued away: a marker four lines from the
container: block can be deleted with it, where a distant literal could not. So the file
derives the requirement a THIRD time, from each job's own step bodies, independent of
both marker and block. That is also the only check that sees the failure #789 filed —
a .NET step MOVED into a bare-runner job, where no set changes at all. It is a
necessary condition only (functional-e2e reaches dotnet via scripts/e2e-local.sh), and
says so; the converse is deliberately not asserted.
test_workflow_job_guards.py asserts set equality both ways against a new
"Workflow-job guards" table in docs/guard-inventory.md. The marker records the job's
PURPOSE, not whether a step can fail: docker-build.yml::build runs a smoke test that
fails the job and is still `none`. Read the other way, every job is a guard and the
table distinguishes nothing.
The four jobs #786 named as having no dropped-step guard each get a recorded decision in
that section. None is a required context, so a drop there cannot send a required check
green; build's residual was already self-documented and is now indexed. ci-step-ran.sh
is deliberately not widened — its scope is derived from the required contexts (#787) and
test_ci_dropped_step_guard.py argues the ban belongs where a drop is consequential.
Two corrections to the issues, both measured rather than assumed:
* #789's "editing docker-build.yml re-points ci-image-pin's expected" is false.
expected="$(git log -1 --format=%H -- docker/ci)" — the pathspec is docker/ci only.
Verified: docker-build.yml was last touched by HEAD and expected still resolves to
32747a0, matching the pin. No publish-then-pin two-step was needed.
* #786's "15 jobs across 6 workflows" is 17.
Proofs, all executed: three real mutations of docker-build.yml witnessed red (container
block removed, marker removed, dotnet step planted in a bare-runner job); the two new
checkers ship parametrised mutants over every job; the declared clause in
mutation_manifest.py is retargeted to the surviving comparison, and the new checker's
entry mutates the guarded ARTIFACT (a renamed table row) per the checker-guard
exception. test_guard_inventory.py's row parsing is scoped to its own section so the
second table cannot be read as phantom guard files.
Suite: 1161 passed, 2 skipped (was 1083) — scripts/tests, ruff 0.12.11 clean.
Refs #786
Refs #789
Self-review findings on the previous commit, all measured before fixing.
THE PREDICATE HAD BOTH FAILURE DIRECTIONS. Measured against constructed inputs:
full-line comment -> ['dotnet'] false POSITIVE
echo string / heredoc -> ['dotnet'] false POSITIVE
dotnet-ef migrations -> [] false NEGATIVE
playwright install -> [] false NEGATIVE
The false positive is the dangerous one: a bare-runner job whose COMMENT merely mentions dotnet
would redden the suite on a correct tree, and a correct guard going red on a correct tree is the
fastest route to that guard being deleted. Full-line comments are now stripped before matching.
`dotnet-ef` and `playwright` join the tool list, and the alternation is sorted longest-first so the
match does not depend on backtracking out of the `dotnet` prefix.
The residue is stated in the code rather than papered over: a TRAILING `# ...` comment, an
`echo "run dotnet build"` and a heredoc body all still match. Parsing those means parsing shell,
which `testing.fixing-a-parser-bug-introduces-the-next-one` prices at three or more rounds. It is
tolerable only because of its DIRECTION — a loud red naming the job and the token, with an obvious
remedy — never a silent pass.
Verified the real workflow's classification is unchanged and exact after the change:
needs-toolchain derives to {test, migrations, functional-e2e, api-docs, format}, equal to the
declared set, zero violations.
THE GUARD/NONE RULE WAS UNDER-SPECIFIED. "Purpose, not whether a step can fail" does not decide
`docker-build.yml::test`, which blocks the merge as a required context. Sharpened everywhere it is
stated: a `guard` job enforces an invariant about the REPOSITORY or its ARTIFACTS; a job that builds
or exercises the PRODUCT is `none` even when it is a required check. No marker value changed — the
rule now explains the classifications that were already there.
Also: a row for test_workflow_job_guards.py in the file-populations table, and the scope-limit
preamble de-narrated (it recounted an earlier draft; the operative warning — an unlisted class reads
as covered — is kept, the autobiography dropped).
Refs #786
Refs #789
Cold adversarial review of b7e5c35fb returned BLOCKED with four substantive findings. All four
are real and all four are fixed here; each fix is demonstrated rather than asserted.
1. THE guard/none LINE EXCLUDED BOTH REQUIRED CONTEXTS. The rule was "a guard enforces an
invariant about the REPOSITORY; a job exercising the PRODUCT is `none`". It reads as
principled and put `test` and `migrations` outside the table — the two required Actions
contexts on `main` per .gitea/required-status-contexts.json, i.e. exactly the jobs where a
failure to fire is fail-OPEN against branch protection, and exactly the gap #786 exists to
close. It also could not survive its own examples: `test` runs ToolCatalogTests.cs and
web/src/api/*.guard.test.ts, the structural guards this document's own scope-limit item 2
names.
Replaced with a line that can be restated: a `guard` job's output is a VERDICT; a `none`
job's output is an ARTIFACT. A `none` job can still go red — that is an error in producing
the thing, not a finding about the repo. `test`, `migrations` and `functional-e2e` flip to
`guard` and gain rows; `none` is now exactly the two image publishers plus renovate. The
rejected rule is recorded so it is not re-adopted.
2. TWO POSITIVE CONTROLS WERE VACUOUS — both of them ones this commit series added, while the
pre-existing sibling it copied was not. `len(_MUTANTS) == 2 * len(population)` is `2N == 2N`,
a tautology at N=0, so an emptied population satisfied it while the parametrised proofs
collected zero cases. Both now assert non-emptiness FIRST. Demonstrated after the fix, by
degrading each population to empty:
role mutation set -> REDDENED: "declared_roles() is EMPTY ..."
declaration mutation set -> REDDENED: "the workflow parsed to ZERO jobs ..."
phantom-row proof -> REDDENED: "the table parsed to nothing ..."
3. A MECHANISM CLAIM IN A DECISION RECORD WAS REFUTED BY EXECUTION. Four sites (the test
docstring, docs/ci-cd.md, the record's `rule:`, and the generated catalog) said
`functional-e2e` reaches dotnet only through scripts/e2e-local.sh and so is invisible to the
step scan. It is not: that job runs `dotnet restore`, `npm ci`, `npm run build` and
`dotnet build` in its own YAML, and the derivation detects it. Measured: the blind-spot set
(declared toolchain minus detected-from-own-steps) is EMPTY today, all five jobs detected.
Reworded to say that, and the converse is still not asserted because a future job could be
written behind a script.
4. THE ADJACENCY RESIDUAL WAS UNDERSTATED. All three checks fail together under one edit that
drops the container block, flips the marker AND moves the invocation into a script — the
review verified it. Now disclosed in the code and in docs/ci-cd.md, with what actually bounds
it: the failure MODE, not its likelihood. The job then dies on a missing binary, loudly,
where #774's original defect sent a REQUIRED check green on the bare runner. A silent pass
traded for a noisy crash, not a hole closed.
Also from the review: the docs-reminder row said the job "never fails" (its Checkout step can);
the phantom-row proof could not fail (it supplied its own phantom, so it now pins the unmutated
set to zero phantoms first); and the scope-limit paragraph was reworded from draft history into
a rejected-alternative note.
Review findings 6 and 7 (regex false positives on comments, false negative on `dotnet-ef`) were
already fixed in 75747fbca, which the reviewer did not see.
Suite: 1161 passed, 2 skipped. ruff 0.12.11 clean; decisions-validate OK.
NOTE for whoever sees a red `test_hook_fire_log.py::test_the_suite_does_not_write_to_the_
PRODUCTION_log` locally: it snapshots mtimes under ~/.cache/ersatztv/hook-fire/, which is shared
across worktrees and sessions, so a second Claude session running concurrently reddens it. Seen
once here with three session logs written during the window; green in isolation, both before and
after this change. That is ersatztv#809 / #822, not this diff.
Refs #786
Refs #789
Self-review of the rows added in the previous commit. They said `test` had "13 of 19 steps
marked" and `migrations` "7 of 9". Measured: 12 of 19 and 6 of 9 carry `ci-step-ran.sh mark`.
The 13th and 7th steps carry `assert`, not `mark` — an aggregate of two different things,
copied without re-deriving it.
Replaced with the invariant rather than a corrected number: every consequential step marks and
the final step asserts, which is what test_ci_dropped_step_guard.py actually enforces. A count
in prose is a second copy of the workflow that rots on the next step added; the invariant does
not.
Refs #786
Cross-family cold re-review (Codex/gpt-5.6) returned BLOCKED with four findings. All verified
independently before fixing.
1. HIGH — THE RETRACTED RULE WAS STILL STATED AS ACTIVE IN THREE PLACES. The previous commit
replaced the guard/none rule in docs/guard-inventory.md and flipped `test`/`migrations` to
`guard`, but left the superseded wording in test_workflow_job_guards.py's module docstring,
docs/ci-cd.md, and the decision record's `rule:` — from which the generated catalog (and
MemPalace) copied it faithfully. Those three explicitly called `docker-build.yml::test` `none`,
which is the opposite of what the workflow now declares. A maintainer following the ACTIVE
decision record could downgrade both required jobs, delete their rows, and every structural
check would stay green, because the checkers read the markers and the markers would agree with
the stale prose.
This is `process.enumerate-clauses-to-close-a-sweep` failing on my own retraction: I fixed the
site I was looking at and left the paraphrases. All three now state the verdict/artifact rule,
and each carries the rejected reading explicitly so it does not come back. Swept by grepping
the RETRACTED words, not the new ones — the only hits left are the deliberate
rejected-alternative notes.
Same commit: docs/ci-cd.md claimed "every job in every workflow declares two things", which its
own table contradicts — `CI_EXECUTION_CLASS` is docker-build.yml only.
2. MEDIUM — A DUPLICATE ROW WAS SILENTLY COLLAPSED. `inventory_jobs()` built a dict, so two rows
for one job overwrote each other, last-one-wins, and set equality still held both ways. The
realistic arrival is a merge conflict leaving an ADVISORY and a GUARD row for the same job: the
registry contradicts itself and its checker reports green. Rows are now kept as a list and
counted against distinct keys by `test_no_job_has_TWO_rows`, with a mutation proof carrying its
own negative control. Witnessed: planting a contradictory duplicate row for
`pr-checks.yml::ci-image-pin` in the real file reddens two named tests; restoring it returns 48
passed.
3. LOW — the toolchain-preflight row said it blocks "every downstream job". Measured: NOTHING
`needs:` it (only `build` has a `needs:`, on test/migrations/scan), and it is not a required
context. Corrected to what it actually blocks — a merge, through the combined commit status the
consent gate reads (#598) — and why it must not consume the image it checks (#772).
4. LOW — the predicate's documented residue was partial. Now enumerated in both directions:
false positives (trailing comment, echo string, heredoc body — stripping is line-based, not
shell-aware) and false negatives (absolute path, script indirection, a `#` heredoc line that a
later command strips and executes).
One of those claims was WRONG as first written and is corrected to what execution shows:
variable indirection is invisible ONLY when the assignment is not in the same run body.
`$SDK_CMD build` with `SDK_CMD` from `env:` is missed; `SDK_CMD=dotnet` written in the same body
IS caught, because the assignment line carries the bare token.
Re-review confirmed the four earlier findings are genuinely fixed: the classification is
consistent across all 17 jobs under the verdict/artifact rule with no job it would reclassify;
both repaired positive controls redden on an emptied population; the corrected functional-e2e
claim is true and the old wording has no matches; the adjacency residual is accurately disclosed
and not overstated; the summary counts assertion holds at (43, 6, 21, 21, 6, 16).
ruff 0.12.11 clean; decisions-validate OK; targeted suites 145 passed.
Refs #786
Refs #789
Second cold re-review (Claude, cold brief, isolated worktree) of the tree at 0944877bf. Its
BLOCKER (the retracted rule surviving in four sites) and its duplicate-row finding were already
fixed in d8a507a3b, which it did not see. Six further findings remain and are fixed here.
JOB-LEVEL env COULD SHADOW A MEMORY-CRITICAL WORKFLOW-LEVEL ONE. Raised as an unmeasured
assumption, and it is the one worth acting on: before this branch NO job in docker-build.yml
declared `env:` at all, and that workflow's workflow-level `env:` carries UseSharedCompilation,
DOTNET_CLI_USE_MSBUILD_SERVER and MSBUILDDISABLENODEREUSE — what keeps a 7.8 GB VBCSCompiler off a
RAM-oversubscribed runner (#406). Actions merges the scopes with the more specific winning, so a
same-named job-level key replaces the workflow value and the loss is INVISIBLE: the job still runs,
just with the shared compiler back on.
Rather than measure the merge once and write the answer in prose, the reliance is removed:
test_no_job_env_key_SHADOWS_a_workflow_level_one asserts no job-level key collides with a
workflow-level one, in any tracked workflow. Measured clean today across all six. Witnessed red by
planting `UseSharedCompilation: true` on `test`:
docker-build.yml::test declares job-level ['UseSharedCompilation'], which also exists at
workflow level. The job value WINS and the workflow value is silently lost.
TWO PROOFS WERE WEAKER THAN THEIR TWINS.
* test_MUTATION_a_guard_job_with_its_row_DELETED_is_reported had no negative control, so a
PRE-EXISTING missing row satisfied every iteration. Its phantom twin got one in c00126b82 and
this one did not — the same asymmetry, one commit apart. It now pins `declared - listed` to
empty first and asserts the deletion is reported as EXACTLY that job.
* test_a_commented_out_marker_does_NOT_count asserted that PyYAML ignores comments — a fact this
suite does not own and that no change here could break. It now writes a workflow to tmp_path,
drives the real `role_faults`, and asserts the commented text IS present in the file, which is
the actual guard: a reader that ever became a text scan would find it.
A PROSE COUNT SHIPPED FIVE LINES ABOVE THE ONE 0944877bf DELETED. "Only three jobs are `none` —"
enumerated the workflow by hand, unasserted, in the same file where the previous commit replaced a
count with an invariant for exactly that reason. Removed: which jobs are `none` is read off the
markers, and the table is the only enumeration a check keeps honest.
Three accuracy fixes, each measured:
* docs-reminder: "all three check steps carry continue-on-error" is off by one — there are TWO
check steps plus a `Set up Python` between them, all three with the flag. The substantive
claim (no finding can fail the job) was right.
* The two REQUIRED-context strings dropped the ` (pull_request)` suffix that
.gitea/required-status-contexts.json carries, so a reader copying one into branch protection
would get a context that never reports. Both now match the file exactly, verified against it.
* The `_FULL_LINE_COMMENT` comment read as though stripping cured a live red. It did not:
measured, ZERO toolchain tokens appear in the raw `run:` bodies of all three bare-runner jobs,
comment lines included. It is preventive, and now says so — along with why it cannot manufacture
a match (`.sub` on a `^…$` match leaves the newline, so no lines are joined).
The re-review independently re-derived the verdict/artifact classification across all 17 jobs and
would not reclassify any, and confirmed the three earlier de-vacuuming fixes hold under execution.
Suite: 1179 passed, 2 skipped. ruff 0.12.11 clean; decisions-validate OK.
Refs #786
Refs #789
`_ROW`'s `[^|]*` Blocks cell means a raw pipe inside it — a code span such as
`grep ... | cut ...` — stops the row matching. An unmatched row is an ABSENT row, which the set
equality reports as a guard job with no entry, so the failure is a loud red rather than a
silently skipped row.
Measured rather than reasoned: injecting a pipe into one Blocks cell reddens
test_the_inventory_covers_exactly_the_guard_JOBS_that_exist naming that job. Recorded because a
regex over a Markdown table invites the opposite assumption, and the next person to widen a cell
should know which way it breaks.
Refs #786
Round three, reviewed twice in parallel (Codex gpt-5.6 and a cold Claude brief). Both converged
independently on the same two MEDIUM findings, which are the branch's own defect class reproduced
by its most recent commits: a test asserting a guarantee it does not provide.
1. THE PROOFS DID NOT EXERCISE THE CODE THEY NAMED. `test_MUTATION_a_DUPLICATE_row_is_reported`
rebuilt the comparison with its own `len(set(...))`, and `test_MUTATION_a_SHADOWING_env_key_is_
reported` built a synthetic dict and intersected it by hand. Both stayed GREEN with the live
check disarmed — a behavioural test wearing a MUTATION name, which is exactly what
`testing.guard-ships-with-mutation-proof` excludes.
`duplicate_row_faults(rows)` and `env_shadow_faults(docs)` are now named detectors; the live
test and its proof both call them. Verified by reproducing the reviewers' own demonstrations
against the fix:
duplicate detector neutered -> REDDENED "the DETECTOR did not report a second row for ..."
shadow detector neutered -> REDDENED "the DETECTOR did not report a shadowing job-level key"
2. `test_a_commented_out_marker_does_NOT_count` CLAIMED TO GUARD A TEXT-SCAN REGRESSION AND DID
NOT. It re-implemented the read inline and never called `declared_roles`. One reviewer built a
comment-blind grep reader reproducing all 17 current values byte-identically and ran every
non-parametrised test in the file: nothing went red.
`declared_roles(paths=None)` now takes an injectable population, so the test drives the REAL
reader over a fixture, asserts the fault function reports it, and pins that a live marker in the
same shape is still read (so the assertion cannot be satisfied by a reader returning None for
everything). Verified: installing the grep reader now reddens it.
RENAMED `advisory` -> `report-only`. docs/ci-cd.md already calls `functional-e2e` an advisory job
in a DIFFERENT sense — not a required context, though it can certainly fail — while this branch
declares that job `guard`. A reader would infer the wrong marker for exactly the job whose marker
is `guard`. The value is now unambiguous, its Kind is `REPORT-ONLY`, and the manifest's declared
`expect` string moved in lockstep with the message it matches.
The rename also caught its own adjacent defect: `REPORT-ONLY` contains a hyphen, which the row
regex's `[A-Z]+` Kind group did not match, so the docs-reminder row stopped parsing. That surfaced
as a loud red naming the job — the fail-closed property documented one commit earlier, working.
Widened to `[A-Z-]+`, matching the `BEHAVIOUR-ONLY` convention in the sibling table.
Nine further accuracy findings, each measured before fixing:
* "would therefore flag every job" — false. git/jq/python3/curl flag 4 of 8. And the REASON was
wrong too: `small` is git-only and provisions Python via actions/setup-python, while `build`
runs on stock ubuntu-latest, so "the bare runner has them" conflated two lanes. They are
excluded because they do not DISCRIMINATE.
* "four lines from the container: block" / "in one diff hunk" — false. Measured 6, 58, 6, 7, 7;
`migrations` has the whole `services:` block in between. The property the guard rests on is
INDEPENDENCE, not proximity, and the docs now say so.
* "the merge semantics never have to be relied upon" — too strong. The check proves no NAME
COLLISION; it does not prove the runner merges. That evidence is in-repo and live and is now
cited: `build`'s Smoke step declares its own step-level `env:` and consumes workflow-level
`${IMAGE}` on the same line, on every push to main.
* "every malformed row fails closed" — over-broad. A malformed DUPLICATE is invisible: its job is
already satisfied by the well-formed row. Stated.
* The `scan` Blocks cell said "a marked job's run: body"; the ban's scope is DELIMITER_BAN_JOBS
(the required contexts PLUS `build`), which is wider, and `scan` carries no `if:` so it runs on
PRs too.
* The functional-e2e row credited `e2e-local.sh` (TOOLING, only boots the instance) for curl
assertions that live in `e2e-functional.sh` (GUARD).
* `report-only` was defined nowhere; now defined as a verdict the job cannot enforce, with the
limit stated (nothing detects a `guard` job whose checks are all continue-on-error).
* "inline rows carry Proof: NONE" was stated universally while two rows cite a pytest; hedged,
and each such row names which PART it covers.
* "Exactly three jobs are `none`" was an unchecked prose enumeration in three sites, in a branch
that deleted one for that reason. Removed; the markers are the source.
* Plus: the ci-cd tool list was short by `dotnet-ef` and `playwright`; docs/README said only a
`guard` job needs a row; "all three checks fail together" read as the opposite of "go blind";
and #774's unrealised consequence is now subjunctive rather than past tense.
Suite: 1178 passed, 2 skipped, 1 known flake (test_hook_fire_log's production-log guard, which
compares mtimes under the SHARED ~/.cache/ersatztv/hook-fire/ — four session logs were written
during the window; 78 passed in isolation. ersatztv#809/#822, evidence posted there).
Refs #786
Refs #789
Self-review sweep over every script the new table cites, cross-checked against that script's own
Kind in the file-guard table above.
Two rows credited a script with coverage it does not provide:
* `functional-e2e` named `scripts/e2e-ui.sh` as an assertion site. It is `TOOLING` — it DRIVES the
Playwright specs, and the UI assertions live in those specs. `scripts/e2e-functional.sh` is the
only `GUARD` of the three; `e2e-local.sh` only boots the instance. Now says so.
* `scan` cited `test_ci_dropped_step_guard.py` and `test_ci_release_path_scan_job.py` and pointed
its Proof at "those files' rows above". The first is graded `PROOF` (for its `ci-step-ran.sh`
role) with Proof `NONE`, so the reference read as coverage that row does not carry. The cell now
names the assertion itself —
`test_the_delimiter_banned_jobs_have_NO_expression_delimiter_in_any_run_body`, which does live in
that PROOF-graded file — says which of the three is the GUARD, and states that the two pytest
files carry `NONE`.
The rule this applies: a cited file's Kind is a property of the file, not of the claim being made
about it, so citing a `TOOLING` or `PROOF` file as an assertion site needs the assertion named or
the reference reads as stronger than it is.
Refs #786
`test_the_marker_is_read_from_the_PARSED_yaml_not_a_text_search` claimed to guard the parser
against regressing to a grep. It asserted only that every tracked job appears as a KEY, so a
reader returning the right keys with wrong VALUES satisfied it — the claim was stronger than the
code. Cold review found it; the same round found the sibling test making the same over-claim, and
that one is now fixed by driving the real reader.
Renamed to what it proves (`test_every_tracked_job_APPEARS_in_the_parse`), with the grep-regression
claim moved to `test_a_commented_out_marker_does_NOT_count`, which actually drives the reader.
Added the property a completeness check structurally cannot express: ATTRIBUTION. A marker belongs
to the job whose `env:` block contains it. A line-oriented reader that attached each
`CI_JOB_ROLE:` to the nearest preceding job header would smear one job's marker onto a neighbour
declaring none, and both jobs still have keys, so nothing phrased over keys can see it. The fixture
is two jobs, only the second marked.
Witnessed, and the control matters: the smearing reader used to demonstrate it is byte-identical
to the real `declared_roles()` on the current tree —
faithful on the real tree? True
REDDENED: the reader attributed a marker to the wrong job — `first` declares none and must
read as None, `second` declares one and must read as its value
— so it satisfies every population and completeness assertion in the file, and only the new
fixture separates them.
Refs #786
The rule was hedged in `docs/guard-inventory.md` when review found two inline rows citing a pytest
despite it. The record's `rule:` frontmatter, the record body, and the checker's module docstring
kept the universal wording — the same fix-one-site-leave-the-paraphrase pattern this branch has
now hit four times, which is why the sweep was by the RETRACTED words rather than the new ones.
All three now say the same thing: an inline row carries `Proof: NONE` unless a pytest genuinely
covers part of it, which `ci-image-pin` and `set-verdict-status` do, and each names which part —
because a reference covering a fraction must not read as covering the whole.
Refs #786
Round four returned MERGEABLE with seven LOW/NIT items. All seven are fixed rather than shipped,
because three of them are the same defect class this branch has been closing all along.
THREE MORE PROOFS RE-IMPLEMENTED THE COMPARISON THEY NAMED. The previous commit extracted
`duplicate_row_faults` and `env_shadow_faults` for exactly this reason and left the set-equality and
Kind comparisons inline, so `test_MUTATION_a_guard_job_with_its_row_DELETED_is_reported`,
`…_a_PHANTOM_row_is_reported` and `…_a_row_whose_KIND_disagrees_is_reported` still rebuilt
`declared - listed` and the Kind check themselves. Review demonstrated it by flattening the live
assertion and watching the file stay green at 50 passed.
`coverage_faults(declared, listed) -> (missing, phantom)` and `kind_faults(roles, listed)` are now
named detectors that the live checks and all three proofs call, and each proof gained a negative
control. Witnessed after the change:
coverage_faults disarmed -> deleted-row proof REDDENED, phantom-row proof REDDENED
kind_faults disarmed -> kind-disagreement proof REDDENED
Worth recording that the system was never fail-open here: the manifest harness already caught the
same disarm ("the named test still PASSED with the clause mutated"). The in-file proofs were weaker
than their names, not absent.
THE ENV-MERGE EVIDENCE WAS STEP-SCOPE USED FOR A JOB-SCOPE CLAIM. `build`'s Smoke step shows the
runner LAYERS step-level `env:` over workflow-level, and what this branch newly relies on is
job-level layering. Measured: on origin/main no job anywhere declared both a job-level `env:` and a
consumed workflow-level var, so the job scope had no in-repo witness. The gap is now named, with
the exposure bounded — the two publish jobs are `if: github.event_name != 'pull_request'`, so PR CI
cannot exercise it and a non-layering runner would fail loudly on the first push to `main` trying to
publish `":<sha>"`, not silently.
Four wording fixes, each measured:
* the record still enumerated "exactly three jobs are `none`" while three other sites say a list
in prose is a second copy that rots — the fourth site now agrees with them.
* "the remaining `docker-build.yml` jobs" over-scoped: six remain after `test`/`migrations`, and
the table has four, because `toolchain-preflight` and `scan` carry the mark/assert protocol
voluntarily without being in the enforced scope. Said so.
* "on every push to `main`" ignored the Smoke step's own `docs_only` gate.
* `docs/ci-cd.md`'s excluded-token list omitted `tar`, which the source comment includes; and the
`scan` Proof cell disclosed the two pytest files' `NONE` but not the cited script row's, two
cells from a sibling that discloses it — so that row now states it claims no proof at all.
Suite: 1178 passed, 2 skipped (the production-log guard deselected; it is the shared-$HOME flake,
green in isolation — #809/#822). ruff 0.12.11 clean over all 47 tracked files; decisions-validate
OK; doc-narrative 0 warnings.
Refs #786
Refs #789
Self-review of the scope-gap paragraph added one commit earlier. It said both publish jobs carry
`if: github.event_name != 'pull_request'`. Measured: only `docker-build.yml::build` does.
`ci-image.yml` declares triggers `push`, `schedule` and `workflow_dispatch` — no `pull_request` at
all — so its job is never CREATED on a PR rather than being created and skipped.
The conclusion the paragraph draws is unchanged (PR CI cannot exercise the job-level env scope),
but the two mechanisms are worth keeping straight: an `if:`-skipped job is still dispatched as a
runner task, which is the behaviour #535 was about, while a job whose workflow has no matching
trigger produces no task at all.
Refs #786
Confirmation review NIT. The paragraph said "That leaves the **four** jobs below" under a heading
reading "The four jobs with no dropped-step guard" — a second hand-maintained copy of the same
count, six lines apart, in the file that argues a hand-maintained summary of a table is a second
copy of it. Nothing checks either. The heading keeps it; the prose does not repeat it.
The review's blocking finding — that the scope-gap paragraph claimed both publish jobs carry
`if: github.event_name != 'pull_request'` — was already corrected in 4a2c76027, one commit after
the tree it reviewed. `ci-image.yml` has no `if:` and no `pull_request` trigger; the docstring says
so.
Refs #786
Rebase-only conflict, caught by re-running the gate on the NEW base rather than trusting the green
from the old one. #871 (`docs(796)`) added `scripts/tests/test_mcp_smoke.py` as a MUTATION-graded
guard row while this branch added `scripts/tests/test_workflow_job_guards.py` as another. Each
bumped the hand-maintained summary sentence by one and each was green against its own base; merged,
the table holds 44/22 and the sentence claimed 43/21.
This is `ci.whole-tree-gate-is-only-as-green-as-its-base` — the shape #780 and #784 hit. The
summary is asserted by `test_the_summary_counts_match_the_table`, so it failed loudly rather than
drifting, which is the whole reason that assertion exists.
Refs #786
Four cold adversarial rounds, alternating model families (Claude, Codex gpt-5.6, both in parallel, then a scoped confirmation pass), each from a review-only brief in an isolated worktree. Rounds 1-3 returned BLOCKED; every finding is fixed and each fix demonstrated by reproducing the reviewer's own test. Round 4 returned MERGEABLE and the confirmation pass verified the detector extraction clean over 4000 randomized equivalence cases per detector with zero mismatches; its one blocking finding had already been fixed a commit before it reported, and its NIT is closed. All five mutation proofs redden when their shipped detector is disarmed. Local suite 1185 passed 2 skipped on the final rebased tree; CI green on d3f470f with Build and test at 514s and Script lint and tests at 444s, so neither is a fast-path phantom.
Review-verdict: MERGEABLE @ d3f470f
Four cold adversarial rounds, alternating model families (Claude, Codex gpt-5.6, both in parallel, then a scoped confirmation pass), each from a review-only brief in an isolated worktree. Rounds 1-3 returned BLOCKED; every finding is fixed and each fix demonstrated by reproducing the reviewer's own test. Round 4 returned MERGEABLE and the confirmation pass verified the detector extraction clean over 4000 randomized equivalence cases per detector with zero mismatches; its one blocking finding had already been fixed a commit before it reported, and its NIT is closed. All five mutation proofs redden when their shipped detector is disarmed. Local suite 1185 passed 2 skipped on the final rebased tree; CI green on d3f470f with Build and test at 514s and Script lint and tests at 444s, so neither is a fast-path phantom.
timothy
merged commit 4b3f6f6c90 into main2026-08-28 22:54:54 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes two issues that are one mechanism pulling in opposite directions.
#786 asked for inline workflow-job guards to be enumerated using "the same
reviewed-registry-cross-checked-both-ways shape
TOOLCHAIN_JOBSuses". #789 says that exactshape is the defect and must be deleted. Working #786 as written would have built the artifact #789
removes — so they are fixed together, in #789's direction.
The mechanism
Every job now declares metadata about itself, and both populations derive from it:
env.CI_EXECUTION_CLASStoolchain/bare-runnerdocker-build.ymlenv.CI_JOB_ROLEguard/advisory/noneMissing or unknown values are hard failures in both checkers.
#789 — the literals are gone
TOOLCHAIN_JOBS/BARE_RUNNER_JOBSdeleted. The literal had a real argument behind it: setequality between two derived sets is blind to a member leaving both at once, so a job losing its
container:block leaves the declared and pinned sets balanced. That needs an anchor that does notmove with the block. The marker is that anchor, better placed — claim and reality in one diff hunk.
Its cost is adjacency, and it is paid rather than argued away. A marker four lines from the
container:block can be deleted with it, where a distant literal could not. So the file derives therequirement a third time — from each job's own step bodies — independent of both marker and
block. That is also the only check that sees the failure #789 actually filed: a .NET step moved
into a bare-runner job, where no set changes at all. It is a necessary condition only
(
functional-e2ereachesdotnetviascripts/e2e-local.sh) and says so.#786 — guard JOBS join a population
New
scripts/tests/test_workflow_job_guards.pyasserts set equality both ways betweenguard-declaring jobs and a new Workflow-job guards table in
docs/guard-inventory.md. The scopelimit that said this class was uncovered is updated, with its two residuals stated (classification
correctness is not proven; an inline assertion carries
Proof: NONEhonestly).The four jobs #786 named as lacking a dropped-step guard each get a recorded decision. None is a
required context, so a drop cannot send a required check green;
ci-step-ran.shis deliberately notwidened.
Two corrections to the issues, measured not assumed
expected="$(git log -1 --format=%H -- docker/ci)"— thepathspec is
docker/cionly. Verified:docker-build.ymlwas last touched bymain's HEAD andexpectedstill resolves to32747a0, matching the pin. No publish-then-pin two-step was needed.Classification
guard= the job's output is a verdict.none= its output is an artifact and a red meansthe build did not work, not that an invariant was violated. That leaves exactly three
nonejobs —the two image publishers and
renovate— anddocs-reminderas the soleadvisory(all three ofits check steps are
continue-on-error).A rejected alternative is recorded in the doc so it is not re-adopted: "a guard enforces an invariant
about the repository, so a job exercising the product is
none" reads as more principled and iswrong — it puts
testandmigrationsoutside the table, and those are the two required statuscontexts on
main, i.e. exactly where a failure to fire is fail-open against branch protection.Review
Three cold review rounds, alternating model families (Claude / Codex gpt-5.6 / Claude), each from a
cold review-only brief in an isolated worktree. Severity converged
BLOCKER/HIGH → MEDIUM → LOW/NIT. Every finding is fixed and demonstrated, not asserted.
Round 1 — the
guard/noneline excluded both required status contexts (rule replaced withverdict-vs-artifact; three markers flipped); two positive controls this branch added were vacuous
(
2N == 2N, true at N=0) while the pre-existing sibling they were copied from was not; a mechanismclaim in the decision record was refuted by execution; the adjacency residual was understated.
Round 2 (cross-family) — the retracted rule still shipped as active in three further sites
including the decision record and the generated catalog, each naming
testasnonewhen it shipsas
guard; duplicate table rows collapsed silently into a dict; thetoolchain-preflight"Blocks"cell was false (nothing
needs:it).Round 3 (both families, independently) — the two new
MUTATION-named proofs passed with thecheck they named fully disarmed: they rebuilt the comparison themselves instead of calling the
shipped code. And
test_a_commented_out_marker_does_NOT_countclaimed to guard a text-scanregression that a reviewer demonstrated goes entirely undetected. Both are the branch's own defect
class reproduced by its own fixes.
duplicate_row_faults/env_shadow_faultsare now nameddetectors that the live test and its proof both call, and
declared_rolestakes an injectablepopulation so the fixture test drives the real reader — each verified by reproducing the
reviewers' own demonstrations against the fix.
Round 3 also renamed the marker value
advisory→report-only:docs/ci-cd.mdalready callsfunctional-e2ean advisory job in a different sense, for the one job whose marker isguard.The rename immediately tripped the row regex (
REPORT-ONLYhas a hyphen, the Kind group was[A-Z]+) — surfacing as a loud red naming the job, which is the fail-closed property documented onecommit earlier, working.
The recurring defect class throughout was prose drifting from code — over half the findings.
Every factual claim in the changed docs and comments has since been re-verified by execution:
tool list vs
docker/ci/Dockerfile, required-context strings vsrequired-status-contexts.json,marker-to-
container:distances (6, 58, 6, 7, 7 — so "same diff hunk" was wrong and is now statedas independence rather than proximity),
continue-on-errorcounts, every row's Assertion cellagainst the workflow, and every cited script against its own
Kind.Verification
scripts/tests: 1175 passed, 2 skipped on the rebased tree (baseline onmain: 1083).docker-build.ymlwitnessed red: container block removed, marker removed,dotnetstep planted in a bare-runner job.(
test_MUTATION_the_declared_clause_reddens_the_named_proof[scripts/tests/test_workflow_job_guards.py]).ruff check/ruff format --checkclean on ruff 0.12.11 (CI's pin);decisions-validate: OK;check-doc-narrative --diff origin/main→ 0 warnings.Docs
docs/ci-cd.md→ "Per-job declarations";docs/guard-inventory.md(new section + scope limit +counts);
docs/README.mdtask-signal row; new decision recordtesting.workflow-declares-its-own-job-metadata; catalog regenerated.fixes #786
fixes #789
Self-review findings on the previous commit, all measured before fixing. THE PREDICATE HAD BOTH FAILURE DIRECTIONS. Measured against constructed inputs: full-line comment -> ['dotnet'] false POSITIVE echo string / heredoc -> ['dotnet'] false POSITIVE dotnet-ef migrations -> [] false NEGATIVE playwright install -> [] false NEGATIVE The false positive is the dangerous one: a bare-runner job whose COMMENT merely mentions dotnet would redden the suite on a correct tree, and a correct guard going red on a correct tree is the fastest route to that guard being deleted. Full-line comments are now stripped before matching. `dotnet-ef` and `playwright` join the tool list, and the alternation is sorted longest-first so the match does not depend on backtracking out of the `dotnet` prefix. The residue is stated in the code rather than papered over: a TRAILING `# ...` comment, an `echo "run dotnet build"` and a heredoc body all still match. Parsing those means parsing shell, which `testing.fixing-a-parser-bug-introduces-the-next-one` prices at three or more rounds. It is tolerable only because of its DIRECTION — a loud red naming the job and the token, with an obvious remedy — never a silent pass. Verified the real workflow's classification is unchanged and exact after the change: needs-toolchain derives to {test, migrations, functional-e2e, api-docs, format}, equal to the declared set, zero violations. THE GUARD/NONE RULE WAS UNDER-SPECIFIED. "Purpose, not whether a step can fail" does not decide `docker-build.yml::test`, which blocks the merge as a required context. Sharpened everywhere it is stated: a `guard` job enforces an invariant about the REPOSITORY or its ARTIFACTS; a job that builds or exercises the PRODUCT is `none` even when it is a required check. No marker value changed — the rule now explains the classifications that were already there. Also: a row for test_workflow_job_guards.py in the file-populations table, and the scope-limit preamble de-narrated (it recounted an earlier draft; the operative warning — an unlisted class reads as covered — is kept, the autobiography dropped). Refs #786 Refs #789Round three, reviewed twice in parallel (Codex gpt-5.6 and a cold Claude brief). Both converged independently on the same two MEDIUM findings, which are the branch's own defect class reproduced by its most recent commits: a test asserting a guarantee it does not provide. 1. THE PROOFS DID NOT EXERCISE THE CODE THEY NAMED. `test_MUTATION_a_DUPLICATE_row_is_reported` rebuilt the comparison with its own `len(set(...))`, and `test_MUTATION_a_SHADOWING_env_key_is_ reported` built a synthetic dict and intersected it by hand. Both stayed GREEN with the live check disarmed — a behavioural test wearing a MUTATION name, which is exactly what `testing.guard-ships-with-mutation-proof` excludes. `duplicate_row_faults(rows)` and `env_shadow_faults(docs)` are now named detectors; the live test and its proof both call them. Verified by reproducing the reviewers' own demonstrations against the fix: duplicate detector neutered -> REDDENED "the DETECTOR did not report a second row for ..." shadow detector neutered -> REDDENED "the DETECTOR did not report a shadowing job-level key" 2. `test_a_commented_out_marker_does_NOT_count` CLAIMED TO GUARD A TEXT-SCAN REGRESSION AND DID NOT. It re-implemented the read inline and never called `declared_roles`. One reviewer built a comment-blind grep reader reproducing all 17 current values byte-identically and ran every non-parametrised test in the file: nothing went red. `declared_roles(paths=None)` now takes an injectable population, so the test drives the REAL reader over a fixture, asserts the fault function reports it, and pins that a live marker in the same shape is still read (so the assertion cannot be satisfied by a reader returning None for everything). Verified: installing the grep reader now reddens it. RENAMED `advisory` -> `report-only`. docs/ci-cd.md already calls `functional-e2e` an advisory job in a DIFFERENT sense — not a required context, though it can certainly fail — while this branch declares that job `guard`. A reader would infer the wrong marker for exactly the job whose marker is `guard`. The value is now unambiguous, its Kind is `REPORT-ONLY`, and the manifest's declared `expect` string moved in lockstep with the message it matches. The rename also caught its own adjacent defect: `REPORT-ONLY` contains a hyphen, which the row regex's `[A-Z]+` Kind group did not match, so the docs-reminder row stopped parsing. That surfaced as a loud red naming the job — the fail-closed property documented one commit earlier, working. Widened to `[A-Z-]+`, matching the `BEHAVIOUR-ONLY` convention in the sibling table. Nine further accuracy findings, each measured before fixing: * "would therefore flag every job" — false. git/jq/python3/curl flag 4 of 8. And the REASON was wrong too: `small` is git-only and provisions Python via actions/setup-python, while `build` runs on stock ubuntu-latest, so "the bare runner has them" conflated two lanes. They are excluded because they do not DISCRIMINATE. * "four lines from the container: block" / "in one diff hunk" — false. Measured 6, 58, 6, 7, 7; `migrations` has the whole `services:` block in between. The property the guard rests on is INDEPENDENCE, not proximity, and the docs now say so. * "the merge semantics never have to be relied upon" — too strong. The check proves no NAME COLLISION; it does not prove the runner merges. That evidence is in-repo and live and is now cited: `build`'s Smoke step declares its own step-level `env:` and consumes workflow-level `${IMAGE}` on the same line, on every push to main. * "every malformed row fails closed" — over-broad. A malformed DUPLICATE is invisible: its job is already satisfied by the well-formed row. Stated. * The `scan` Blocks cell said "a marked job's run: body"; the ban's scope is DELIMITER_BAN_JOBS (the required contexts PLUS `build`), which is wider, and `scan` carries no `if:` so it runs on PRs too. * The functional-e2e row credited `e2e-local.sh` (TOOLING, only boots the instance) for curl assertions that live in `e2e-functional.sh` (GUARD). * `report-only` was defined nowhere; now defined as a verdict the job cannot enforce, with the limit stated (nothing detects a `guard` job whose checks are all continue-on-error). * "inline rows carry Proof: NONE" was stated universally while two rows cite a pytest; hedged, and each such row names which PART it covers. * "Exactly three jobs are `none`" was an unchecked prose enumeration in three sites, in a branch that deleted one for that reason. Removed; the markers are the source. * Plus: the ci-cd tool list was short by `dotnet-ef` and `playwright`; docs/README said only a `guard` job needs a row; "all three checks fail together" read as the opposite of "go blind"; and #774's unrealised consequence is now subjunctive rather than past tense. Suite: 1178 passed, 2 skipped, 1 known flake (test_hook_fire_log's production-log guard, which compares mtimes under the SHARED ~/.cache/ersatztv/hook-fire/ — four session logs were written during the window; 78 passed in isolation. ersatztv#809/#822, evidence posted there). Refs #786 Refs #789`test_the_marker_is_read_from_the_PARSED_yaml_not_a_text_search` claimed to guard the parser against regressing to a grep. It asserted only that every tracked job appears as a KEY, so a reader returning the right keys with wrong VALUES satisfied it — the claim was stronger than the code. Cold review found it; the same round found the sibling test making the same over-claim, and that one is now fixed by driving the real reader. Renamed to what it proves (`test_every_tracked_job_APPEARS_in_the_parse`), with the grep-regression claim moved to `test_a_commented_out_marker_does_NOT_count`, which actually drives the reader. Added the property a completeness check structurally cannot express: ATTRIBUTION. A marker belongs to the job whose `env:` block contains it. A line-oriented reader that attached each `CI_JOB_ROLE:` to the nearest preceding job header would smear one job's marker onto a neighbour declaring none, and both jobs still have keys, so nothing phrased over keys can see it. The fixture is two jobs, only the second marked. Witnessed, and the control matters: the smearing reader used to demonstrate it is byte-identical to the real `declared_roles()` on the current tree — faithful on the real tree? True REDDENED: the reader attributed a marker to the wrong job — `first` declares none and must read as None, `second` declares one and must read as its value — so it satisfies every population and completeness assertion in the file, and only the new fixture separates them. Refs #786Round four returned MERGEABLE with seven LOW/NIT items. All seven are fixed rather than shipped, because three of them are the same defect class this branch has been closing all along. THREE MORE PROOFS RE-IMPLEMENTED THE COMPARISON THEY NAMED. The previous commit extracted `duplicate_row_faults` and `env_shadow_faults` for exactly this reason and left the set-equality and Kind comparisons inline, so `test_MUTATION_a_guard_job_with_its_row_DELETED_is_reported`, `…_a_PHANTOM_row_is_reported` and `…_a_row_whose_KIND_disagrees_is_reported` still rebuilt `declared - listed` and the Kind check themselves. Review demonstrated it by flattening the live assertion and watching the file stay green at 50 passed. `coverage_faults(declared, listed) -> (missing, phantom)` and `kind_faults(roles, listed)` are now named detectors that the live checks and all three proofs call, and each proof gained a negative control. Witnessed after the change: coverage_faults disarmed -> deleted-row proof REDDENED, phantom-row proof REDDENED kind_faults disarmed -> kind-disagreement proof REDDENED Worth recording that the system was never fail-open here: the manifest harness already caught the same disarm ("the named test still PASSED with the clause mutated"). The in-file proofs were weaker than their names, not absent. THE ENV-MERGE EVIDENCE WAS STEP-SCOPE USED FOR A JOB-SCOPE CLAIM. `build`'s Smoke step shows the runner LAYERS step-level `env:` over workflow-level, and what this branch newly relies on is job-level layering. Measured: on origin/main no job anywhere declared both a job-level `env:` and a consumed workflow-level var, so the job scope had no in-repo witness. The gap is now named, with the exposure bounded — the two publish jobs are `if: github.event_name != 'pull_request'`, so PR CI cannot exercise it and a non-layering runner would fail loudly on the first push to `main` trying to publish `":<sha>"`, not silently. Four wording fixes, each measured: * the record still enumerated "exactly three jobs are `none`" while three other sites say a list in prose is a second copy that rots — the fourth site now agrees with them. * "the remaining `docker-build.yml` jobs" over-scoped: six remain after `test`/`migrations`, and the table has four, because `toolchain-preflight` and `scan` carry the mark/assert protocol voluntarily without being in the enforced scope. Said so. * "on every push to `main`" ignored the Smoke step's own `docs_only` gate. * `docs/ci-cd.md`'s excluded-token list omitted `tar`, which the source comment includes; and the `scan` Proof cell disclosed the two pytest files' `NONE` but not the cited script row's, two cells from a sibling that discloses it — so that row now states it claims no proof at all. Suite: 1178 passed, 2 skipped (the production-log guard deselected; it is the shared-$HOME flake, green in isolation — #809/#822). ruff 0.12.11 clean over all 47 tracked files; decisions-validate OK; doc-narrative 0 warnings. Refs #786 Refs #789if:-gated job 64d80e5d28Review-verdict: MERGEABLE @
d3f470fFour cold adversarial rounds, alternating model families (Claude, Codex gpt-5.6, both in parallel, then a scoped confirmation pass), each from a review-only brief in an isolated worktree. Rounds 1-3 returned BLOCKED; every finding is fixed and each fix demonstrated by reproducing the reviewer's own test. Round 4 returned MERGEABLE and the confirmation pass verified the detector extraction clean over 4000 randomized equivalence cases per detector with zero mismatches; its one blocking finding had already been fixed a commit before it reported, and its NIT is closed. All five mutation proofs redden when their shipped detector is disarmed. Local suite 1185 passed 2 skipped on the final rebased tree; CI green on
d3f470fwith Build and test at 514s and Script lint and tests at 444s, so neither is a fast-path phantom.