Files
ersatztv/scripts/tests/mutation_manifest.py
T
timothyandClaude Fable 5.1 a9341d8415 fix(881): a GREEN claim is only readable if its proof REACHES the clause
Round three found the one half of the new mechanism with no relevance gate.
`verify_claim`'s GREEN path read exactly two things — the run exited 0, and
something PASSED — and both are satisfied by a proof that never touches the
mutated file at all. Reproduced before fixing: retargeting the shipped GREEN
entry's proof from `test_check_doc_narrative.py` to `test_bom_guard_detection.py`
changed nothing, and the entry still reported verified. The RED direction never
had this hole, because a proof that ignores the mutation stays green and is
refused as "the clause is not load-bearing".

So a GREEN entry now declares a `reach_replacement` and its `reach_expect`: a
SECOND mutation of the SAME clause, required to REDDEN the same proof, executed
through `verify_mutation` so its red is read through the diagnostic gate rather
than on exit status. The shipped entry declares `path = p` — dropping the `b/`
stripping every scanned diff header goes through — and the run then scans
NOTHING, which is what the declared diagnostic reads. The same retarget now
fails, naming the reach verdict.

The gate runs LAST of the three: run first it would refuse before the status and
vacuity gates were read and neither could be witnessed failing alone (#685), and
the sandbox is reset between a claim's two proof runs for the reason it is reset
between mutations. It has its own disarm proof, and the two synthetic claim
sandboxes are now real git repositories so `reset_sandbox` has a baseline;
`_lib_with` shares the baseline registry, since a copied module's own starts
empty.

Also from that round:

- The record no longer counts the mutation-outcome claims in the pinned
  proposal-3 scan. A third of the same shape sits in the same result set
  (`test_a_verdict_BEYOND_A_SHORT_PAGE_is_still_found`), and which side of the
  line a sentence falls on is a judgement, so an exact count is a figure the
  next reader re-derives differently — the failure this record is about.
- The calibration paragraph no longer restates the post-review-verdict outcome
  as a dated witnessing. It points at the `CLAIMS` entry that executes it, which
  is the form the rewritten shell comment beside it demands.
- The comment in `check-doc-narrative.py` claimed a universal ("reddens no
  test") while one file is executed. It now names that file, so the quote binds
  an outcome no wider than what is checked.
- Proposal 4 from the issue is dispositioned explicitly: rejected as a rule
  here, on the issue's own argument that an exhortation does not fire at the
  moment of least slack.
- `docs/README.md`'s task-signal parenthetical now names the `CLAIMS`
  population; the file was owned by another slot when this branch started.

Cost re-measured 2026-09-05, three baseline/branch pairs: the `CLAIMS` half adds
31.7-43.6%, up from the 12.7-16.6% measured before the gate existed. The old
figure is retired rather than scaled — growing the population invalidates the
measurement that described it.

Refs #881

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
2026-09-05 05:02:46 +02:00

683 lines
46 KiB
Python

"""The DECLARED clause mutations: one per `MUTATION`-graded row of `docs/guard-inventory.md`
in `MUTATIONS`, plus one per declared PROSE claim about a mutation's outcome in `CLAIMS`
(ersatztv#881). The two populations are derived differently and are kept apart for that reason;
the `CLAIMS` section below states its own.
Data only. The machinery that applies these is `mutation_harness_lib.py`; the checks that keep this
file honest are `test_mutation_harness.py`.
Every entry is declared by hand and none is inferred, which is the whole design constraint from
ersatztv#790: "a harness that guesses which clause of a 90-line hook is *the* guard would manufacture
exactly the confident-but-empty coverage this is meant to prevent". Where a proof test already names
its own clause in source — the BOM guard's `= "efbbbf" ]; then`, `UNSET_CLAUSE`, `prove-fix.sh`'s
`if [ "$RC" -eq 0 ]; then` — the entry reuses THAT string rather than inventing a second one, so a
retarget in either place is caught by the other.
WHY AN ENTRY'S `target` MAY DIFFER FROM ITS `guard`. Some guards here ARE tests
(`scripts/tests/test_*.py`). Disarming such a guard makes it ABSENT rather than red, so
`testing.guard-ships-with-mutation-proof`'s checker-guard exception applies: the mutation goes into
the guarded ARTIFACT — a deleted row, a planted phantom row — and the check must report it. Mutating
a checker's own POPULATION instead is a trap that looks identical and is not: a shrunken population
makes every real row report as PHANTOM, so the proof reddens on a false positive while saying
nothing about the missing-row detection the row claims. `why` states per entry which shape applies
and why; no count is kept here, because a count of the entries below is a second copy of them.
"""
from __future__ import annotations
from scripts.tests.mutation_harness_lib import Claim, Mutation
CLAUSE = Mutation.CLAUSE
DETECTOR = Mutation.DETECTOR
RED = Claim.RED
GREEN = Claim.GREEN
MUTATIONS: tuple[Mutation, ...] = (
Mutation(
guard=".claude/hooks/posttooluse-worktree-marker.sh",
target=".claude/hooks/posttooluse-worktree-marker.sh",
clause='printf \'%s\\n\' "$me" > "$abs/.claude-worktree-owner" 2>/dev/null || true',
replacement="true",
proof="test_worktree_ownership_guard.py::test_MUTATION_a_marker_hook_that_stops_WRITING_makes_the_guard_go_quiet",
granularity=CLAUSE,
expect="the UNMUTATED pair did not deny",
why="The marker write is the hook's entire job; without it the guard has nothing to read and "
"fails open. The clause string is the one the proof test itself passes to its `_mutate` helper.",
),
Mutation(
guard=".claude/hooks/pretooluse-bom-guard.sh",
target=".claude/hooks/pretooluse-bom-guard.sh",
clause='= "efbbbf" ]; then',
replacement='= "deadbeef" ]; then',
proof="test_bom_guard_detection.py::test_DISARMING_the_BOM_comparison_stops_detection",
granularity=CLAUSE,
expect="the BOM comparison has moved",
why="The BOM comparison is the guard's only detection logic. Same clause the proof test names.",
),
Mutation(
guard=".claude/hooks/pretooluse-worktree-guard.sh",
target=".claude/hooks/pretooluse-worktree-guard.sh",
clause='marker="$root/.claude-worktree-owner"',
replacement='marker="$root/.claude-worktree-owner-NOTHING-WRITES-THIS"',
proof="test_worktree_ownership_guard.py::test_MUTATION_disarming_the_guards_MARKER_READ_stops_the_deny",
granularity=CLAUSE,
expect="the UNMUTATED guard did not deny",
why="The marker read is what the ownership decision hangs on. Same clause the proof test names.",
),
Mutation(
guard=".husky/pre-push",
target=".husky/pre-push",
clause="unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE",
replacement=": # clause removed by the mutation harness",
proof="test_prepush_unsets_git_env.py::test_MUTATION_DELETING_the_unset_lets_drift_through_silently",
granularity=CLAUSE,
expect="the UNMUTATED pre-push did not catch the drift",
why="Without the unset, every git call the pre-push chain makes is aimed at the repository git "
"exported the environment for, not the one being pushed. `UNSET_CLAUSE` in the proof test.",
),
Mutation(
guard="scripts/build_decisions_catalog.py",
target="scripts/build_decisions_catalog.py",
clause="want.strip() != have.strip()",
replacement="False",
proof="test_build_catalog_check_path.py::test_MUTATION_disarming_the_stale_comparison_stops_detection",
granularity=CLAUSE,
expect="the stale-detection clause has moved or been reworded",
why="`main()`'s only stale-detection logic, per the proof test's own docstring, which uses this "
"exact clause and this exact replacement.",
),
Mutation(
guard="scripts/ci-detect-docs-only.sh",
target="scripts/ci-detect-docs-only.sh",
clause='if [ "$is_shallow" = "true" ]; then',
replacement="if true; then",
proof="test_docs_only_detector_clone_depth.py::test_the_push_arm_leaves_a_COMPLETE_clone_complete",
granularity=CLAUSE,
expect="GRAFTED the complete clone shallow",
why="The shallow test is the whole of ersatztv#836's fix: disarmed, `--depth=2` goes back to "
"every checkout including `build`'s complete one, which grafts it and makes the `git describe` "
"in the next step find no reachable tag. This row was UNDECLARED until #836 on the stated "
"grounds that the script 'feeds the skip gate, so its effect is visible only in a workflow "
"run' — untrue of this clause, whose effect is the shallow flag on a real clone and is "
"observable in-process, which is what the proof test asserts.",
),
Mutation(
guard="scripts/ci-step-ran.sh",
target="scripts/ci-step-ran.sh",
clause='if ! grep -qxF "$key" "$marker" 2>/dev/null; then',
replacement="if false; then",
proof="test_ci_dropped_step_guard.py::test_dropping_ANY_single_step_FAILS_the_guard",
granularity=CLAUSE,
expect="never having executed",
why="The per-key membership test is what turns a dropped step into a red job; disarmed, every "
"expected key reads as present and the guard passes a run in which nothing executed.",
),
Mutation(
guard="scripts/tests/test_ci_status_context_uniqueness.py",
target=".gitea/workflows/docker-build.yml",
clause=" name: API docs in sync (OpenAPI + endpoint index)",
replacement=" name: Build & test (.NET)",
proof="test_ci_status_context_uniqueness.py::test_no_two_jobs_synthesize_the_SAME_status_context",
granularity=CLAUSE,
expect="these status contexts can be produced by more than one job",
why="THE CHECKER-GUARD SHAPE: this guard IS a test, so disarming its own assertion makes it "
"absent rather than red. Per `testing.guard-ships-with-mutation-proof` the mutation goes into "
"the guarded ARTIFACT instead — here a workflow job renamed to collide with the REQUIRED "
"`Build & test (.NET)` job, which is exactly the evasion the guard exists to catch: two jobs "
"synthesizing one status context that branch protection cannot tell apart, so the required "
"check could be satisfied by the producer whose steps carry no execution markers.",
),
Mutation(
guard="scripts/check-required-contexts.sh",
target="scripts/check-required-contexts.sh",
clause="($rule.status_check_contexts | sort | unique) == $snap",
replacement="true",
proof="test_check_required_contexts.py::test_MUTATION_disarming_the_set_comparison_stops_every_drift_report",
expect="the set-comparison clause has moved or been reworded",
granularity=CLAUSE,
why="The set comparison IS the finding: it is the only thing that turns a live required-check "
"list differing from the committed snapshot into `drift`. Witnessed: unmutated reports "
"`drift` on an added context, the mutant reports `match` — a permanent no-op that would "
"confirm the snapshot fresh forever. The proof asserts the mutant's EXACT verdict "
"`(0, 'match')` rather than merely 'not drift', and ships a positive control for its own "
"tmp layout: a copy of the script without the classifier it loads beside "
"itself exits 2 with empty stdout, and 'not drift' is then satisfied by a copy "
"that never ran. The clause string is the one the proof test asserts on before mutating.",
),
Mutation(
guard="scripts/decisions_validate.py",
target="scripts/decisions_validate.py",
clause="wing_faults=record_wing_faults() + yaml_faults,",
replacement="wing_faults=yaml_faults,",
proof="test_decisions_validate.py::test_main_actually_CALLS_the_wing_scan",
granularity=CLAUSE,
expect="a wing fault must fail the validator",
why="The wiring the proof test exists for: deleting this call left the whole suite green while "
"a real block-scalar record vanished under `decisions-validate: OK` (#609).",
),
Mutation(
guard="scripts/prove-fix.sh",
target="scripts/prove-fix.sh",
clause='if [ "$RC" -eq 0 ]; then',
replacement="if false; then",
proof="test_prove_fix.py::test_MUTATION_disarming_the_UNPROVEN_clause_reddens_the_refusal_test",
granularity=CLAUSE,
expect="the clause under mutation is gone",
why="The UNPROVEN branch: a named test that passes WITHOUT the fix must be refused. Same clause "
"the proof test names.",
),
Mutation(
guard="scripts/tests/test_ci_image_paths_pin_agreement.py",
target="scripts/tests/test_ci_image_paths_pin_agreement.py",
clause="for path in sorted(published - required):",
replacement="for path in sorted(set()):",
proof="test_ci_image_paths_pin_agreement.py::test_a_diverging_list_is_DETECTED",
granularity=CLAUSE,
expect="the agreement check accepted a workflow pair whose publish paths and pin pathspec",
why="The two set-difference clauses fail differently and neither implies the other, so each "
"is separately load-bearing: this one catches a path that PUBLISHES an image the pin never "
"tracks (silent and green), while `required - published` catches a pathspec entry that "
"never publishes (a red on a blocking job until an image for it is published). Disarming "
"this clause alone leaves "
"the `publish-path-added` mutant undetected while the other four cases stay green, which "
"is why the grade is CLAUSE rather than DETECTOR.",
),
Mutation(
guard="scripts/tests/test_ci_image_pin_population.py",
target="scripts/tests/test_ci_image_pin_population.py",
clause="for name in sorted(expected - set(pinned)):",
replacement="for name in sorted(set()):",
proof="test_ci_image_pin_population.py::test_a_single_job_losing_its_pin_is_DETECTED",
granularity=CLAUSE,
expect="the population check accepted a workflow in which a container job no longer runs",
why="The against-the-DECLARED-CLASS direction, and the one the other two clauses cannot "
"cover: a job that loses its `container:` block leaves `declared` and `pinned` equal, so "
"only this comparison notices it has moved to the bare runner. This is the clause #790 "
"asked for instead of neutering `pin_population_faults` wholesale. `expected` is "
"`toolchain_declared(doc)` since ersatztv#789 replaced the `TOOLCHAIN_JOBS` literal this "
"clause used to name; the comparison and its role are unchanged.",
),
Mutation(
guard="scripts/tests/test_guard_populations_derive_from_git.py",
target="scripts/tests/test_guard_inventory.py",
clause="if ref in tracked:",
replacement="if (REPO_ROOT / ref).exists():",
proof="test_guard_populations_derive_from_git.py::test_no_derivation_admits_an_untracked_file",
granularity=CLAUSE,
expect="after git stopped tracking them",
why="THE GUARD IS A TEST, so the mutation goes into the guarded ARTIFACT — one of the "
"derivations it watches — rather than into the checker, per the checker-guard exception. "
"The clause is the exact defect this guard was written after: `derived_guard_files` read "
"its CALLERS from the index and then admitted the paths they name on `Path.exists()`, so a "
"tracked workflow naming a script that exists on one machine only entered the population "
"there, red on that checkout and green in CI (#778's third shape, "
"inside #806 itself). Note what this mutation does NOT do: on a clean tree the mutated set "
"is identical, so `test_guard_inventory.py`'s own assertions stay green — only narrowing "
"the index, which is what the proof does, separates them. That is why the proof has to "
"remove EVERY member rather than sample one.",
),
Mutation(
guard="scripts/tests/test_optional_request_members.py",
target="scripts/tests/test_optional_request_members.py",
clause='"ArtworkContentTypeModel": (',
replacement='"ArtworkContentTypeModelRENAMED": (',
proof="test_optional_request_members.py::test_every_droppable_request_schema_has_a_stated_disposition",
granularity=CLAUSE,
expect="no disposition written down",
why="THE GUARD IS A TEST, so the mutation goes into the guarded ARTIFACT — here the "
"DISPOSITIONS registry the checker maintains, the same shape as the deleted "
"guard-inventory row below. Renaming the key rather than deleting the entry keeps the "
"module importable, so the red is a real set-equality failure and not an ImportError "
"reddening for the wrong reason. The rename fires BOTH directions — MISSING for the real "
"schema and PHANTOM for the renamed key — which is the correct behaviour and worth stating, "
"since `expect` names only the MISSING half. "
"`ArtworkContentTypeModel` is the right key to name: it is the exact schema #807's "
"hand-written table omitted, because `...Model` reads as a response model while it is in "
"fact reachable from the full-replace PUT /channels/{id}.",
),
Mutation(
guard="scripts/tests/test_workflow_job_guards.py",
target="docs/guard-inventory.md",
clause="| `pr-checks.yml::ci-image-pin` |",
replacement="| `pr-checks.yml::ci-image-pin-RENAMED` |",
proof="test_workflow_job_guards.py::test_the_inventory_covers_exactly_the_guard_JOBS_that_exist",
granularity=CLAUSE,
expect="guard/report-only but have NO row in",
why="THE GUARD IS A TEST, so the mutation goes into the guarded ARTIFACT — the workflow-job "
"table — rather than into the checker, per the checker-guard exception. RENAMED rather than "
"deleted, so the row count is unchanged and the red cannot come from an empty table: the "
"anti-vacuity test still sees rows, and what fails is the set equality itself, in BOTH "
"directions at once (MISSING for the real job, PHANTOM for the renamed key). "
"`ci-image-pin` is the right row to name — it is the guard job #774 found stating an "
"invariant it did not check, and the case #786 was filed to bring into a population.",
),
Mutation(
guard="scripts/tests/test_guard_inventory.py",
target="docs/guard-inventory.md",
clause="| `.claude/hooks/decisions-guard.sh` | a commit | GUARD | NONE | — |\n",
replacement="",
proof="test_guard_inventory.py::test_the_inventory_covers_exactly_the_guards_that_exist",
granularity=CLAUSE,
expect="these guard files exist but have no row in guard-inventory.md",
why="THE GUARD IS A TEST, so the mutation goes into the guarded ARTIFACT rather than into the "
"checker — disarming a checker makes it absent, not red, and mutating its population instead "
"would only demonstrate a false POSITIVE (a shrunken population reports every real row as "
"phantom) while proving nothing about the missing-row detection the row claims. A deleted "
"row is the defect this guard exists to catch, and it is one of the mutations #774 witnessed "
"by hand.",
),
Mutation(
guard="scripts/tests/test_hook_fire_log.py",
target="scripts/tests/test_hook_fire_log.py",
clause=" if mentions != [CANONICAL_SINK_ASSIGNMENT, CANONICAL_SINK_SOURCE]:",
replacement=" if False:",
proof="test_hook_fire_log.py::test_a_LATER_reassignment_the_regex_cannot_see_is_DETECTED",
granularity=CLAUSE,
expect="reassignment of the SOURCED sink passed clean",
why="REGRADED FROM `DETECTOR` BY ersatztv#891, because the guard acquired a clause that IS "
"load-bearing on its own. `instrumentation_faults` used to accumulate only from arms a "
"stripped hook trips several of at once, so disarming any one left the rest answering and no "
"clause-level mutation could redden the proof; the entry carried the finest surviving "
"mutation (`if not _SOURCES_SINK.search(text):` -> `if False:`) as its honesty check. #891 "
"replaced the lexical rule over the sink assignment with BYTE-IDENTITY of the preamble's two "
"lines, and that comparison decides alone: disarming it reddens this proof on every hook, "
"while nothing else catches an indented or `export`ed reassignment. The old canary had to go "
"with it rather than be carried on: byte-identity necessarily matches the sourcing line, so "
"`_SOURCES_SINK` can no longer decide anything (measured: 0 cases in 13 hooks x 8 "
"perturbations) and a mutation required to KEEP surviving would have been guaranteed to. "
"THE GENERAL SHAPE, because it is not specific to this entry: a `survived_clause` asserts "
"that a finer mutation still survives, so its precondition is that the guard is deliberately "
"coarse there. WIDENING the main clause can remove that precondition, and the canary then "
"does not fail — it becomes a tautology, which reads exactly like a passing proof. When a "
"clause is widened, re-derive every proof calibrated against the narrow one; a widened clause "
"can retire its own evidence.",
),
Mutation(
guard="scripts/tests/test_remote_state_inventory.py",
target="docs/remote-state-inventory.md",
clause="| `scripts/post-review-verdict.sh` — commit-status write |",
replacement="| `scripts/DELETED-BY-THE-MUTATION-HARNESS.sh` — not a real path |",
proof="test_remote_state_inventory.py::test_every_in_scope_file_has_a_row_and_every_row_names_a_real_file",
granularity=CLAUSE,
expect="in scope but absent from docs/remote-state-inventory.md",
why="THE GUARD IS A TEST, so the mutation goes into the guarded ARTIFACT: a real executable's "
"row is renamed away, which is the MISSING-row defect the row's Blocks column claims — an "
"in-scope file with no classification. Two shapes were tried and rejected. Emptying the "
"guard's `git ls-files` derivation reddens the proof with an IndexError over an empty "
"population: a crash, not a detection. Planting a PHANTOM row reddens "
"`test_MUTATION_PROOF_a_dropped_row_and_a_phantom_row_are_both_detected` by contaminating "
"the fixture that test builds for itself, and proves the opposite direction from the one the "
"row claims. Renaming the row exercises both directions of the production set comparison at "
"once and is matched on the missing half.",
),
Mutation(
guard="scripts/tests/test_mutation_harness.py",
target="scripts/tests/mutation_harness_lib.py",
clause=" if mutation.expect not in diagnostic:",
replacement=" if False:",
proof="test_mutation_harness.py::test_MUTATION_disarming_the_DIAGNOSTIC_gate_accepts_a_red_for_the_wrong_reason",
granularity=CLAUSE,
expect="the UNMUTATED verdict already accepted it, so the mutant proves nothing",
why="The target is not the guard for a structural reason: the harness keeps its machinery in "
"`mutation_harness_lib.py` so a clause of it can be disarmed in an isolated copy at all. The "
"clause is the DIAGNOSTIC gate — the check that a failing proof failed with the diagnostic "
"its row declares. Disarmed, a red for any unrelated reason is certified as a guard doing "
"its job, which is the shape that made two rows in this very file measure nothing. THE "
"INVENTORY HOLDS ONE REF PER ROW, so this entry names the gate verified rows hang on most "
"directly, and the gates that read a proof RUN carry disarm proofs of their own: "
"`test_MUTATION_disarming_the_EXIT_STATUS_gate_accepts_a_run_that_NEVER_RAN_A_TEST` for the "
"exit-status gate and, on the GREEN direction `CLAIMS` uses, the status, vacuity and "
"relevance gates. "
"So does the PRE-FLIGHT refusal that no later gate can stand in for — a replacement "
"identical to its clause mutates nothing, so the proof runs against the original tree and "
"an already-red one reddens exactly like a detection: "
"`test_MUTATION_disarming_the_IDENTICAL_REPLACEMENT_gate_CERTIFIES_A_ROW_THAT_MUTATED_NOTHING`. "
"Named rather than counted, because a count moves silently when one is added — and naming "
"is not a claim the list is CLOSED: the two remaining pre-flight refusals, an absent target "
"and a clause that does not occur exactly once, decide before any proof runs and this entry "
"says nothing about what they are worth.",
),
Mutation(
guard="scripts/ci-toolchain-image-resolves.sh",
target="scripts/ci-toolchain-image-resolves.sh",
clause=" 404)",
replacement=" 4040)",
proof="test_ci_toolchain_image_resolves.py::test_MUTATION_a_deleted_tag_is_reported_as_a_failure",
granularity=CLAUSE,
expect="a deleted tag was not reported as GONE",
why="404 is the ONE answer that establishes the pinned toolchain image is gone; every other "
"code means the check could not run. Both fail the job, so the EXIT CODE does not separate "
"them and the mutation is caught by the DIAGNOSTIC instead: retargeting the arm sends the "
"real outage down the could-not-verify path, which sends an operator to the registry's "
"health rather than to the rebuild that fixes it.",
),
Mutation(
guard="scripts/tests/test_review_verdict_vocabulary.py",
target="scripts/check-review-verdict.sh",
clause="""POS_RE='^review-verdict:[[:space:]]*('"$POS_ALTERNATION"')([[:space:]@]|$)'""",
replacement="""POS_RE='^review-verdict:[[:space:]]*(mergeable|approved|lgtm)([[:space:]@]|$)'""",
proof="test_review_verdict_vocabulary.py::test_a_word_added_to_the_shared_source_reaches_BOTH_sides",
granularity=CLAUSE,
expect="restating the word list rather than deriving it",
why="THE GUARD IS A TEST, so the mutation goes into the guarded ARTIFACT — the read side "
"whose derivation it watches — rather than into the checker, per the checker-guard "
"exception. The replacement is not an invented mutant: it is the LITERAL pre-#788 line, the "
"second hand-written copy this change removed, so the proof is taken against the real "
"predecessor. Mutating the shared vocabulary instead would be the trap that looks "
"identical: emptying or corrupting the word list reddens the proof through the fail-closed "
"validator, which says nothing about whether the read side still DERIVES from it.",
),
Mutation(
guard="scripts/tests/test_workflow_persist_credentials.py",
target=".gitea/workflows/ci-image.yml",
clause=""" persist-credentials: false
# only docker/ci/Dockerfile is needed; no git describe/log here""",
replacement=""" # only docker/ci/Dockerfile is needed; no git describe/log here""",
proof="test_workflow_persist_credentials.py::test_every_actions_checkout_DROPS_the_persisted_credential",
granularity=CLAUSE,
expect="persists a write-capable Authorization header into .git/config",
why="THE GUARD IS A TEST, so the mutation goes into the guarded ARTIFACT rather than into "
"the checker, per the checker-guard exception. The defect is the one the guard exists for: "
"an `actions/checkout` step that simply omits the key, which is what a newly added job gets "
"by default — not an explicit `true`, which nobody writes. `ci-image.yml` carries exactly "
"one checkout, so the clause is unambiguous there; the trailing comment line is part of it "
"only to make the match unique within the file.",
),
Mutation(
guard="scripts/tests/test_mcp_smoke.py",
target="scripts/mcp_smoke.py",
clause=" id_init = secrets.randbelow(2**31 - 1000) + 1000",
replacement=" id_init = 1",
proof="test_mcp_smoke.py::test_MUTATION_a_PRE_ANSWERED_id_is_refused_because_the_request_ids_are_UNGUESSABLE",
granularity=CLAUSE,
expect="a pre-answered id was ACCEPTED",
why="The target is not the guard for the same structural reason as the harness entry above: "
"`scripts/mcp_smoke.py` is a checker reached only transitively, through "
"`scripts/check-local-lsp.sh`, so it holds no inventory row of its own and the clause has to "
"be disarmed in it directly (`testing.verification-code-needs-its-own-proof`). The clause is "
"the unguessable request id, which AT THE `initialize` STAGE is the only thing refusing a "
"server that answers before it is asked: the pending-registration cannot help there, because "
"that id is already in flight when the pre-answer arrives, which is why #793 replaced "
"the lock rather than tightening it. Disarmed, the stub's pre-answer is "
"accepted at `initialize` and the run dies one stage later at `tools/list`, so the proof "
"asserts the STAGE (rc 9 and the initialize diagnostic) rather than mere failure: the mutant "
"still fails, and a test checking only that would stay green over a real vulnerability. THE "
"`id_tools` TWIN IS DELIBERATELY NOT THIS CLAUSE, measured rather than assumed: replacing "
"`id_tools` alone leaves every case green, because the reader keeps only the reply matching "
"the id in flight, so a `tools/list` frame emitted while `initialize` is pending is dropped "
"whatever its id. Disarming that retention clause alone is green too. Only BOTH together "
"produce the false green (rc 0), so the tools stage is held by two mechanisms that mask each "
"other and neither is singly detectable — #685's shape. `id_init` is the one place a "
"pre-answer IS singly exploitable, which is why it is the declared clause; the tools stage is "
"covered behaviourally in the same file.",
),
Mutation(
guard="scripts/tests/test_complete_annotation_dispositions.py",
target="web/src/api/completeAnnotations.guard.test.ts",
clause="MultiCollectionItemRequest: {\n disposition: 'ANNOTATED',",
replacement="MultiCollectionItemRequest: {\n disposition: 'CREATE',",
proof="test_complete_annotation_dispositions.py::test_the_two_dispositions_AGREE",
granularity=CLAUSE,
expect="this file says COVERED, the SPA guard says CREATE",
why="This guard IS a test, so disarming it makes it absent rather than red — the checker-guard "
"exception applies and the mutation goes into the guarded ARTIFACT, the SPA guard's disposition "
"table. The VALUE is the clause: flipping this row from ANNOTATED "
"to CREATE retires the requirement that MultiCollectionItemRequest be annotated, so deleting the "
"Complete<...> from MultiCollectionsScreen.toItemRequest then leaves every suite green with "
"#807's silent weight reset live again. The row is named in full rather than by the bare "
"disposition line, which occurs three times and would identify none of them.",
),
Mutation(
guard="scripts/post-review-verdict.sh",
target=".gitea/workflows/review-verdict.yml",
clause='H10_REVIEWERS="timothy"',
replacement='H10_REVIEWERS="somebody-who-is-not-a-reviewer"',
proof="test_post_review_verdict.py::test_a_verdict_posted_by_an_ALLOWLISTED_account_is_accepted",
granularity=CLAUSE,
expect="the verdict writer REFUSED an allow-listed account",
why="THE COUPLING, NOT THE COMPARISON. The guard is the writer; the clause lives in the GATE, "
"because what ersatztv#845 is about is the two drifting apart. Mutating the gate's own "
"allow-list while the posting account stays fixed reddens the accept path only if BOTH hold: "
"the writer reads the list LIVE from the workflow (a hand-copied list would not move), and "
"the membership comparison actually gates the outcome (a no-op comparison would not care "
"that it moved). Disarming the comparison inside the script instead would prove the second "
"and say nothing about the first, which is the half that was missing.",
),
Mutation(
guard="scripts/tests/test_image_build_delegates_the_spa_suite.py",
target="docker/Dockerfile",
clause="RUN npm run lint && npm run typecheck && npm run build",
replacement="RUN npm run lint && npm run typecheck && npm test -- --run && npm run build",
proof="test_image_build_delegates_the_spa_suite.py::test_every_SPA_CARRYING_STAGE_runs_exactly_its_pinned_commands",
granularity=CLAUSE,
expect="docker/Dockerfile::web-build does not run its pinned commands",
why="The target is not the guard, per the checker-guard exception: the guard IS a test, so "
"disarming it makes it absent rather than red, and the mutation goes into the guarded "
"ARTIFACT. The clause is the web-build stage's whole command line, and the replacement is "
"the defect itself rather than a caricature of it — ersatztv#883 put a vitest run back into "
"a gitless stage, and every image build failed from that commit until #887. WHAT THIS PROVES "
"IS NARROWER THAN IT LOOKS, and saying so is the point: the guard no longer decides whether "
"a command RUNS the suite (that predicate was wrong nine times), "
"it compares the stage's commands against a pin. So this mutation proves the pin is "
"compared and reported — not that any particular spelling is recognised, because none needs "
"to be. The mutant is deliberately the UNFILTERED spelling: the filtered one is what broke, "
"but a pin rejects both identically and choosing the narrower case would suggest otherwise.",
),
)
# ------------------------------------------------------------------------------------------------
# THE OTHER GUARDS — stated per guard, and compared for SET EQUALITY against the inventory
# ------------------------------------------------------------------------------------------------
#
# #790's third Done-when box asks that guards whose mutation cannot be declared be STATED. A reason
# keyed on the row's GRADE would be cheaper and is tautological: a new guard graded NONE inherits one
# automatically and nobody ever looks at that particular guard. A count of them is no better — it
# moves only on net change, so adding one undeclared guard while promoting another leaves it at 22.
#
# So this is keyed on the guard, and `test_every_GUARD_row_is_either_DECLARED_or_STATED_here` asserts
# set equality against the inventory's GUARD rows in both directions. That makes it the same kind of
# hand-maintained-but-machine-checked table as `docs/guard-inventory.md` itself: a new guard cannot
# arrive without someone writing a line here about why it carries no mutation, and a line cannot
# outlive the row it is about.
#
# WHAT `NONE` ACTUALLY MEANS, because the wording matters here: the row nominates no proof ref. It
# does NOT mean the guard is untested. `scripts/ci-prove-ban-detects.sh` is graded NONE and is driven
# end to end by `test_ci_release_path_scan_job.py`. Nominating a proof is a judgement about which
# test is THE proof, which is #775's scope; this file can only verify one afterwards.
UNDECLARED: dict[str, str] = {
# NO GROUPING. Sorting these into "driven through their deciding path" and
# "not driven at all" was wrong twice — in both
# directions, over entries whose own text said the opposite. A category above a list is a second
# classification of the same facts, and it drifts the moment one entry's situation changes. Each
# entry states its own case instead.
#
# THE TWO THINGS THAT GO MISSING ARE DIFFERENT, and which one it is decides where the work goes.
# A guard may be DRIVEN — `test_hook_fire_log.py` executes most hooks through their real deciding
# branch, its matrix asserting instrumentation TRANSPARENCY (the wrapped and unwrapped runs
# agree), never that the decision is right or that a particular clause produced it — and still
# have no NOMINATED proof and no witnessed clause disarm. Nominating one is a judgement about
# which test is THE proof, which is #775's scope; this file can only verify one afterwards. A
# guard nothing executes at all needs the test first.
#
# `NONE` in the inventory means the row nominates no proof ref. It does NOT mean untested.
".claude/hooks/decisions-guard.sh": "Driven to a block and to a pass by test_hook_fire_log.py's "
"constructed cases, which assert transparency rather than the decision. No nominated proof, and "
"no clause disarmed.",
".claude/hooks/prepush-clean-worktree-check.sh": "Driven with a file both modified in the tree "
"and present in the pushed set, by test_hook_fire_log.py, for transparency. No nominated proof, "
"and no clause disarmed.",
".claude/hooks/prepush-donewhen.sh": "Driven against a stub Gitea by test_hook_fire_log.py, so "
"its real blocking path is reached — for transparency. No nominated proof, and no clause "
"disarmed.",
".claude/hooks/prepush-rebase-check.sh": "BEHAVIOUR-ONLY. A named test drives it and "
"test_hook_fire_log.py reaches its behind-origin block, but which clause carries that decision "
"has not been established by disarming one.",
".claude/hooks/pretooluse-agent-ram.sh": "Driven at 5% and 15% free memory through a stubbed "
"`memory_pressure`, by test_hook_fire_log.py, for transparency. No nominated proof, and no "
"clause disarmed.",
".claude/hooks/pretooluse-agent-model.sh": "Driven with and without a `model` in the payload by "
"test_hook_fire_log.py's matrix, for transparency. No nominated proof, and no clause disarmed.",
".claude/hooks/pretooluse-bash-guard.sh": "Driven with an ETV_UPDATE_GOLDENS command and a "
"harmless one by test_hook_fire_log.py's matrix, for transparency. No nominated proof, and no "
"clause disarmed.",
".claude/hooks/pretooluse-nav-guard.sh": "Driven with an `/iptv/` URL by test_hook_fire_log.py's "
"matrix, for transparency. No nominated proof, and no clause disarmed.",
".claude/hooks/design-sync-reminder.sh": "Driven by test_hook_fire_log.py, which gives it its "
"start/finish arguments and works around its self-throttle — for transparency, and never to the "
"one-shot branch that fires on the first Stop after a UI change and then allows. A proof has to "
"model that state transition rather than a single invocation.",
".claude/hooks/pretooluse-merge-consent.sh": "BEHAVIOUR-ONLY. Several suites execute it, but consent "
"is derived from several independent conditions, so which one a given red belongs to has to be "
"established before a clause can be named.",
".husky/commit-msg": "Outside the hook-fire population (that globs `.claude/hooks/*.sh`) and "
"executed by no test: the repositories the suite builds are fresh `git init`s that never install "
"husky, so the hook is absent rather than bypassed. A proof has to install or invoke it.",
".husky/pre-commit": "Runs lint-staged, the decisions guard, the root-PNG check and the format "
"gate. Only the decisions guard has an inventory row of its own — root-PNG and format are INLINE "
"here, so this one row is the whole classification of both, and neither has a proof. Executed by "
"no test, and nothing observes the dispatch itself.",
"scripts/check-kickoff-guard.sh": "Nothing drives it. A proof needs a tree carrying a revived "
"#237 reference, which is cheap and simply not written.",
"scripts/check-review-verdict.sh": "BEHAVIOUR-ONLY. Its named test feeds the real script an "
"input only one clause rejects, which proves it reacts, not that the clause is load-bearing.",
"scripts/ci-detect-already-validated.sh": "Blocks nothing directly — it feeds the skip gate. The "
"consequence a mutation would have to be observed through is a job that skips, which is visible "
"only in a workflow run.",
"scripts/ci-prove-ban-detects.sh": "Driven end to end by test_ci_release_path_scan_job.py, and "
"it runs a mutation of its own at CI time. Grading it here needs a decision about what a second "
"mutation would add; the row nominates no ref today.",
"scripts/e2e-functional.sh": "Needs a running instance. Its clauses are HTTP contract "
"assertions, so a proof means booting the app — `scripts/e2e-local.sh`'s job, not this "
"harness's.",
"scripts/jq-preflight.sh": "BEHAVIOUR-ONLY. Its named test drives the real script below the "
"version floor; no clause has been disarmed to show the floor comparison is what refuses.",
"scripts/pr-changed-files.sh": "BEHAVIOUR-ONLY. Its named test feeds a short page to the real "
"enumeration; the pagination clause has not been disarmed.",
"scripts/tests/test_ci_release_path_scan_job.py": "A GUARD that is a test, so the mutation would "
"have to go into the guarded artifact — the release-path scan job in the workflow. Which "
"weakening of that job is THE defect it exists to catch has not been settled.",
}
# ------------------------------------------------------------------------------------------------
# THE PROSE CLAIMS — same rule, wider population (ersatztv#881)
# ------------------------------------------------------------------------------------------------
#
# `MUTATIONS` above covers the `MUTATION`-graded rows of `docs/guard-inventory.md` and nothing else,
# which is where ersatztv#812's four defects got in: a claim about a mutation's outcome written in a
# TEST DOCSTRING, a DECISION RECORD or a CODE COMMENT was outside the executed-claim rule by
# construction, so four consecutive review rounds each found one that was simply false. The rule was
# already the right rule; its population was its first site.
#
# So a claim entry is keyed on the PROSE, not on a guard. `site` names the tracked file that makes
# the claim and `quote` is the sentence, verbatim: the harness requires the quote to occur exactly
# once there, so the entry and the sentence cannot drift apart silently. That binding is the half
# #812's defects were missing — every one of them was an assertion with nothing tying it to its
# referent.
#
# THE QUOTE SPANS THE MUTATION AND THE OUTCOME. The mutation half of a sentence is the part that
# is uncontroversial; the checkable assertion is the outcome — `reddens <this test>`, `leaves
# the suite green`. A quote that stops before it binds the wrong half: the words that make the
# sentence a claim could then be reworded, or the test renamed in the prose, while the entry's
# binding still resolves.
#
# THE REFERENT IS DECLARED, NEVER INFERRED — the same doctrine the clause mutations above are built
# on, applied one level up. A checker that went looking for sentences that SOUND like mutation claims
# would be a string predicate over prose, which `docs/defect-shapes-773.md` §4 and
# `docs.no-session-narrative` both refuse to make load-bearing. What is checked here is a claim that
# named its own referent; what is NOT checked is a claim that named none, and that limit is stated in
# `testing.mutation-claims-are-executed` rather than papered over.
#
# `outcome` is RED or GREEN. GREEN is new and is the point: a sentence asserting that a mutation is
# NOT noticed is one no `MUTATION` row can express, so before this the rule "declare it or do not
# write it" had no way to be satisfied for that shape at all. A
# GREEN entry carries no `expect` — there is no red to read a diagnostic out of — and is worth
# exactly what the retired `DETECTOR` survivor was worth: it is required to KEEP surviving, and the
# day the mutation starts being noticed the entry reddens and the sentence has to be rewritten.
#
# A GREEN ENTRY ALSO DECLARES ITS `reach_replacement`, AND IS WORTHLESS WITHOUT IT. The two
# directions are not symmetric: a RED claim checks itself, because a proof that never executes the
# mutated file stays green and the harness refuses it in the same words it refuses a clause that has
# stopped being load-bearing. A GREEN claim has no such property — an unrelated proof produces
# exactly the green the sentence asserts, so a green certifies nothing until the proof is shown to
# reach the clause. `reach_replacement` is a SECOND mutation of the SAME clause, declared to redden
# the same proof with `reach_expect`, and it is executed through the same `verify_mutation` the rows
# above use, so its red is read exactly as theirs is rather than on exit status.
#
# No count is kept here either, for the reason stated at the top of this file.
CLAIMS: tuple[Claim, ...] = (
Claim(
site="scripts/post-review-verdict.sh",
# The quote spans the OUTCOME half and the named test, not only the mutation half: the
# checkable assertion here is `<mutation> reddens <this test>`, and a quote stopping at
# the comma would leave the words that make it a claim free to be reworded — or the test
# renamed in the prose — while the entry's binding still resolves.
quote=(
"rather than asserted: disarming the type test feeds a `null` `.statuses` to "
"`map(select(...))`,\n"
"# jq refuses to iterate it, and the script dies with the PARSE message instead of "
"the shape one — so\n"
"# `test_a_readback_whose_statuses_array_is_NULL_is_refused` reddens."
),
target="scripts/post-review-verdict.sh",
clause='if (.statuses | type) == "array" then',
replacement="if true then",
proof="test_post_review_verdict.py::test_a_readback_whose_statuses_array_is_NULL_is_refused",
outcome=RED,
expect="Cannot iterate over null (null)",
why="The sentence explains why three read-back outcomes are kept apart with separate "
"messages, and it asserts that the array-TYPE test is what keeps them apart. Disarmed, a "
"`null` `.statuses` reaches `map(select(...))`, jq refuses to iterate it and the `|| die` "
"fires with the PARSE message, so the named test fails on the SHAPE message it asserts. The "
"declared diagnostic is jq's own, not that assertion's expected text: the expected text also "
"appears in the comparison pytest prints, so matching it would certify any red inside the "
"same assertion, including one produced by an unrelated change to the script.",
),
Claim(
site="scripts/check-doc-narrative.py",
quote="`test_check_doc_narrative.py` stays green with this arm removed",
target="scripts/check-doc-narrative.py",
clause=' path = None if p == "/dev/null" else (p[2:] if p.startswith("b/") else p)',
replacement=' path = p[2:] if p.startswith("b/") else p',
proof="test_check_doc_narrative.py",
outcome=GREEN,
expect="",
# THE RELEVANCE GATE, and without it this entry certifies nothing: a proof that never
# executes this line produces exactly the green the sentence predicts. The reach mutation
# drops the `b/` stripping the same expression performs, so every `+++ b/<path>` header
# resolves to a path `is_scanned_path` rejects and the run scans NOTHING — which is what the
# declared diagnostic reads, from the ACTUAL half of the comparison rather than the expected
# one. Passing it means the proof reaches this clause and what the clause computes changes
# what the proof observes, which is the precondition the green half needs and cannot supply.
reach_replacement=" path = p",
reach_expect="scanned 0 file(s); 0 advisory warning(s)",
why="The `/dev/null` arm is declared DEFENSIVE in its own comment — a deleted file "
"contributes no `+` lines, so nothing is yielded for it either way. The comment says so and "
"then names an outcome, which is a measurement, and until this entry nothing re-took it. The "
"mutation removes exactly the arm the comment is about, leaving the `b/` stripping it is "
"wrapped around, so a red here would mean the arm has become load-bearing and the comment is "
"now wrong. The quote names the proof FILE rather than every test in the repository, because "
"the file is what the harness runs: an outcome half broader than the executed scope is the "
"same drift in miniature.",
),
Claim(
site="docs/decisions/records/docs/record-wing-parse-guard.md",
quote="deleting the single line that WIRES it into `main()` reddens a dedicated test",
target="scripts/decisions_validate.py",
clause=" wing_faults=record_wing_faults() + yaml_faults,",
replacement=" wing_faults=yaml_faults,",
proof="test_decisions_validate.py::test_main_actually_CALLS_the_wing_scan",
outcome=RED,
expect="a wing fault must fail the validator",
why="A RED claim, and one made in a DECISION RECORD — the site class two of #812's four "
"defects lived in, and the one furthest from anything that executes. The record says the "
"wiring line's removal previously left the whole suite green while a real record vanished, "
"so the dedicated test is the only thing standing between that defect and a silent return. "
"The mutation drops `record_wing_faults()` from the call and keeps `yaml_faults`, which is "
"the deletion the sentence describes rather than a caricature of it.",
),
)