title: Negative-Control Shapes — Three Ways a Guard Proves It Can Fail date: 2026-09-21 spec: SPEC-191 tags: [testing, guards, negative-control, mutation, false-confidence, ratchet] category: patterns module: vt-base problem_type: quality resolution_type: process severity: high symptoms: - A guard is green, and nobody can name the input that would make it red - A check's own test fed the shape the check was written against, and stayed green through the defect - "exits non-zero" is asserted, and the unfixed code also exits non-zero for an unrelated reason - A marker or claim is satisfied by a comment that mentions it
Negative-Control Shapes — Three Ways a Guard Proves It Can Fail¶
Problem¶
A guard is a claim plus a mechanism for detecting its violation. The mechanism can break independently of the claim, and when it does the guard reports on nothing while looking exactly as it did before. In one week this repository found four such breaks — BUG-087 (a hook registered nowhere, then registered and inert), guard 61 (satisfied by a comment), BUG-086 (three rows green against the very defect they were written for). Every one was found by executing a mutation. Not one was visible by reading.
The rule (configs/user-global/CLAUDE.defaults.md §13) is that an assertion counts as evidence
only once something has been seen to make it fail for its stated reason. This page is the how.
The invariant¶
A negative control is a labelled row that can only pass if a deliberately-defective subject WAS detected AND a deliberately-clean twin was NOT. Either half alone is satisfied by a checker that answers the same thing for everything. And it is never built by mutating the real artefact — that is the BUG-014 mechanism: a crashed restore rewrites the tree.
The three shapes¶
All three emit the same observable — one labelled ok row — so the inventory that enforces them
(guard 121) needs to understand none of them.
| Shape | What it does | Applies to | Cost | In-tree exemplar |
|---|---|---|---|---|
| S1 shared-predicate self-probe | Runs the same predicate object the scan uses against known-good and known-bad lines, before the scan's verdict | parser, regex and classifier guards | ~0 | guard 25's SELF-PROBE block (row NC1): one RECORDS definition, so probe and scan cannot drift; the "caught" rows are what make it load-bearing |
| S2 synthetic member injected into a re-run | Adds one member the real artefact does not have — to a temp copy, or by monkeypatching the source in-process — re-runs the extraction, and requires it to surface in that run AND to be absent from the clean one | "is every hook registered / rendered?" | one copy + re-extract | guard 93 row NC1 (an injected registry row must reach the generated page, and the un-mutated render must lack it — it monkeypatches hooks_data.registry_rows, it does not copy); guard 61 row 2 (a claim naming an unregistered hook must be flagged — control on the un-mutated copy first) |
| S3 differential run of the real mechanism | Runs the real mechanism over a synthetic world twice — once seeded with the defect, once clean — and requires the two outcomes to differ | "does setup.sh --safe exit non-zero on a failed backup?" |
one extra run | guard 72 section B (the real runner over a sandbox TESTS_DIR: a hollowed guard fails, the healthy twin passes) |
BUG-086 is exactly a missing S3 control arm. Three rows asserted "--safe exits non-zero" on a
seeded backup failure; the unfixed installer also exits non-zero in a sandbox, for unrelated
reasons. Only asserting that the un-seeded run in the same sandbox exits zero separates the
two — without it, three green rows could not fail and were about to close a high bug.
The self-probe shape (S1) does not generalise to world-state assertions ("is this hook registered?"). The control-arm discipline under it does, which is why S2 and S3 exist.
Declaring it — SPEC-191 FR-0¶
- The value lives in the
NC[0-9]+namespace. Conforming means editing the row that is the control. Pointing the marker at an ordinary assertion ("A1") is refused — relabelling an existing row would be the cheapest way to look conformant (Decision 15). - The runner must see that row print
ok.run-all.shchecks every guard's log; a declared control that never printedokfails the guard with the canonicalFAIL <name> (exit=1).SKIPdoes not count — a skipped control did not run (Decision 14). - A pure liveness probe declares
NEGATIVE_CONTROL_NONE="<why, at least 20 characters>". That is a property of the guard, never an exemption; a new guard may declare it freely, but an existing control downgraded to NONE fails guard 121's ratchet. - A guard declaring nothing is SILENT. The silent set is shrink-only against the merge base and named in an ADVISORY every run — the gap list is derived, never stored.
- Every marker is classified before ANY guard runs, not before each one. The trust boundary is the RUN: with the read inside the loop, a guard that runs earlier can delete a later guard's marker, and the victim then passes with no control at all while its committed blob — and so the ratchet, the count and the floor — sees nothing. Measured both ways in SPEC-191 pass 4.
⚠ A control row must print ok in EVERY state¶
Make the control a fixture row — S1, S2 or S3 against a sandbox or a copy. Never a check
against the real repository that can SKIP: on every push to main the merge base IS HEAD, a
merge-base ratchet SKIPs there, a SKIP is "not observed", and main goes red on the guard's own
conformance. Guard 121 declares NC2 and its control is a fixture pair for exactly this reason —
and its fixtures use NC1, so a fixture row leaking into its log can never satisfy its own control.
The limit, stated¶
A guard can print its control row unconditionally. The assertion floor catches the crude form (a
guard hollowed to exit 0 drops below its floor), but not a guard that keeps its other rows and
fakes only this one. A sampled mutation probe per run would close it and was rejected — it makes
the suite nondeterministic and a crashed restore wipes the tree (SPEC-191 Decision 12). Review is
what opposes it; /vt-d-4-review asks "has anyone seen the thing that enforces this fail?", and
guard 121's transitions ADVISORY names every newly declared control so a reviewer can check it.
Related¶
structural-test-assertions.md— pair every positive with a negationfixture-precondition-is-a-blind-spot.md— a fixture's preconditions define a region it cannot seesecurity-fixtures-must-prove-reachability.md— prove the fixture reached the branch firstheuristic-calibration-gate.md— a negative control IS a calibration gate for a binary detectorprobe-must-reproduce-against-unfixed-code.md— a probe that passes on the unfixed code proves nothing