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