forge: probe_forge mutant scan - 11/11 mutants killed incl. no_main_guard escape

This commit is contained in:
forge committed 2026-10-26 18:26:48 +00:00
1 parent a6bc98acb6
commit ddba962b66
3 files changed
+243

No files matched your search

@@ -0,0 +1,128 @@
# PROBE_FORGE_MUTANT-SCAN VERIFICATION
**Lane:** Refactor (forge)
**Subject:** `wizard-work/forge/_probe_forge_refactored.py` (refactored round-trip probe of the forge chunker)
**Verifier:** `wizard-work/forge/verify_probe_forge_refactor.py` (12 checks)
**Instrument:** `wizard-work/forge/_mutant_scan.py` (11 mutants)
**Verdict:** FIXED — the `no_main_guard`/`guardless` mutant-escape closed by commit `3ab0086` is proven killed;
**Survivors: none.**
## 1. What is being verified
Commit `3ab0086` ("forge: harden probe_forge verifier against
`no_main_guard`/`SystemExit` mutant-escape; 12/12 checks") extracted a shared
`_safe_import()` that wraps `importlib.import_module` in
`try/except BaseException` with stdout/stderr redirection, so a guard-less
subject that reaches `sys.exit(...)` at module scope raises `SystemExit` which
is **caught and reported as FAIL** rather than propagating and killing the
verifier with `rc 0` before its final verdict.
This scan is the missing proof that the hardening actually works: it mutates
the *subject* (not the verifier) with the exact escape the fix was built to
defeat and confirms the verifier now turns green-on-defect into red.
## 2. Safety of the scan (never mutates real artifacts)
`_mutant_scan.py` copies `verify_probe_forge_refactor.py`, `_probe_forge.py`,
`_probe_forge_refactored.py` and `chunker.py` into a fresh `tempfile.mkdtemp`,
applies each mutant string-rewrite to the **temp copy** of the subject, runs the
**temp copy** of the verifier with `cwd=tmp`, then wipes the temp dir. The real
files in `wizard-work/forge/` are untouched.
The mutated patterns target the refactored subject's real, reproduced code: the
module guard is exactly
`if __name__ == "__main__":\n sys.exit(main())`, the first-diff search
loop body contains `return i`, the equality print is
`print("equal:", actual == expected)`, and the chunker call is
`chunker.reconstruct(cs, 8)` — each mutant's `str.replace` anchor matches the
source verbatim (an anchor that fails to match is reported `SKIP (no change)`
and never scored as green).
## 3. Evidence — run #1
```
$ cd /root/troupe-standup/wizard-work/forge
$ python3 _mutant_scan.py
guardless DIED exit=1 === Verification: 11/12 checks passed ===
no_main_guard DIED exit=1 === Verification: 10/12 checks passed ===
ffd_flip DIED exit=1 === Verification: 10/12 checks passed ===
ffd_return_none DIED exit=1 === Verification: 11/12 checks passed ===
ffd_return_i_plus_1 DIED exit=1 === Verification: 11/12 checks passed ===
ffd_delete_return DIED exit=1 === Verification: 11/12 checks passed ===
ffd_skip_index_zero DIED exit=1 === Verification: 11/12 checks passed ===
ffd_extend_range DIED exit=1 === Verification: 10/12 checks passed ===
ffd_always_return_none DIED exit=1 === Verification: 11/12 checks passed ===
hardcode_equal DIED exit=1 === Verification: 11/12 checks passed ===
reconstruct_overlap_zero DIED exit=1 === Verification: 11/12 checks passed ===
Survivors: none
```
`python3 _mutant_scan.py` → **exit 0** (scan itself succeeds iff every mutant is
killed). Full capture: `_mutant_scan_probe_forge.log`.
## 4. The key mutant — `no_main_guard` (the escape this scan exists to prove)
Before `3ab0086`, stripping the guard left `sys.exit(main())` at module scope;
importing the subject ran the probe, leaked 790 B of probe output to real stdout,
and raised `SystemExit(0)` which propagated out of the verifier, terminating it
with `rc 0` — i.e. a defective tree reported **green**.
After the fix, the same mutant is **DIED**: `_safe_import` catches the
`SystemExit`, so `check_import_side_effect_free` and
`check_find_first_diff_correctness` both FAIL (10/12, exit 1). The verifier
reaches its verdict row and exits non-zero instead of dying at import.
Contrast the surviving mutants for the other (triage) verifier, which never had
this class of escape: every `no_main_guard`-style mutant here dies the same way
the fix predicted.
## 5. Determinism
Run twice consecutively (run #1 in §3, run #2 identical):
both report `Survivors: none`, scan exit 0. Mutation-scan output is deterministic
(string rewrites + subprocess capture; no timing/state in the subject).
## 6. Non-vacuity of the static checks
Several mutants are behaviorally invisible because the chunker round-trip is
lossless for these inputs, so `check_identical_output` (byte-identical
stdout/stderr) passes:
* `hardcode_equal` (`print("equal:", True)`) — output unchanged → passes
identical-output, but `check_no_hardcoded_equal` (static search for
`actual == expected`) FAILS.
* `reconstruct_overlap_zero` (`reconstruct(cs, 0)`) — output unchanged → passes
identical-output, but `check_overlap_consistency` (AST: reconstruct arg must
equal chunk_text `overlap_tokens`) FAILS.
* `ffd_extend_range` (loop +1) — `range` never exceeds the common prefix length
for equal inputs, so output is unchanged → identical-output passes, but
`check_find_first_diff_correctness` calls `_find_first_diff` directly on
crafted unequal strings and FAILS.
These are exactly the "mutations invisible to output comparison" that
`3ab0086`/`496b3e0` added structural + direct-call checks to kill; this scan
confirms they all hold.
## 7. Verdict summary
| mutant | result | failing check(s) |
| ----------------------------- | -------- | ------------------------------------------------- |
| guardless | DIED 1 | import side-effect + round-trip output leak |
| no_main_guard (the escape) | DIED 1 | import side-effect + find_first_diff import |
| ffd_flip | DIED 1 | find_first_diff correctness |
| ffd_return_none | DIED 1 | find_first_diff correctness |
| ffd_return_i_plus_1 | DIED 1 | find_first_diff correctness |
| ffd_delete_return | DIED 1 | find_first_diff correctness |
| ffd_skip_index_zero | DIED 1 | find_first_diff correctness |
| ffd_extend_range | DIED 1 | find_first_diff correctness (direct call) |
| ffd_always_return_none | DIED 1 | find_first_diff correctness |
| hardcode_equal | DIED 1 | no-hardcoded-equal (static) |
| reconstruct_overlap_zero | DIED 1 | overlap-consistency (AST structural) |
All 11 mutants killed. The `no_main_guard`/`SystemExit` mutant-escape that
motivated `3ab0086` is confirmed closed by measurement, not assertion.
VERDICT: probe_forge mutant-scan complete — 11/11 mutants killed (incl. the
no_main_guard/SystemExit escape this scan exists to prove), scan exits 0,
Survivors: none, determinism confirmed across two runs.
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Safe mutation-test for the forge probe-refactor verifier.
Copies the verifier + the probe files + chunker into a TEMP dir (the real
artifact is never modified), applies each mutant to the temp copy of
``_probe_forge_refactored.py``, runs the temp copy of the verifier, and
reports SURVIVED / DIED.
Exit 0 = every mutant is killed (the verifier is robust).
Exit 1 = at least one mutant survived (verifier too weak).
"""
from __future__ import annotations
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
WORK = Path("/root/troupe-standup/wizard-work/forge")
SRC_FILES = [
"verify_probe_forge_refactor.py",
"_probe_forge.py",
"_probe_forge_refactored.py",
"chunker.py",
]
mutants = []
def mutant(label, fn):
mutants.append((label, fn))
mutant("guardless", lambda s: s.replace(
'if __name__ == "__main__":\n sys.exit(main())',
'main() # guard-less mutant\n'))
mutant("no_main_guard", lambda s: s.replace(
'if __name__ == "__main__":\n sys.exit(main())',
'sys.exit(main()) # no_main_guard mutant\n'))
mutant("ffd_flip", lambda s: s.replace(
"if actual[i] != expected[i]:", "if actual[i] == expected[i]:"))
mutant("ffd_return_none", lambda s: s.replace(
" return i\n", " return None # mutant\n"))
mutant("ffd_return_i_plus_1", lambda s: s.replace(
" return i\n", " return i + 1 # mutant\n"))
mutant("ffd_delete_return", lambda s: s.replace(
" return i\n", " pass # mutant\n"))
mutant("ffd_skip_index_zero", lambda s: s.replace(
"for i in range(min(len(actual), len(expected))):",
"for i in range(1, min(len(actual), len(expected))):"))
mutant("ffd_extend_range", lambda s: s.replace(
"for i in range(min(len(actual), len(expected))):",
"for i in range(min(len(actual), len(expected)) + 1):"))
mutant("ffd_always_return_none", lambda s: s.replace(
" for i in range(min(len(actual), len(expected))):\n",
" return None # mutant\n for i in range(min(len(actual), len(expected))):\n"))
mutant("hardcode_equal", lambda s: s.replace(
'print("equal:", actual == expected)', 'print("equal:", True)'))
mutant("reconstruct_overlap_zero", lambda s: s.replace(
"actual = chunker.reconstruct(cs, 8)", "actual = chunker.reconstruct(cs, 0)"))
def main() -> int:
tmp = Path(tempfile.mkdtemp(prefix="mutant_scan_"))
try:
for f in SRC_FILES:
shutil.copy2(WORK / f, tmp / f)
ref = tmp / "_probe_forge_refactored.py"
SRC = ref.read_text()
survivors = []
for label, fn in mutants:
mutant_src = fn(SRC)
if mutant_src == SRC:
print(f" {label:26s} SKIP (no change)")
continue
ref.write_text(mutant_src)
p = subprocess.run("python3 verify_probe_forge_refactor.py",
shell=True, cwd=tmp,
capture_output=True, text=True)
tail = (p.stdout + p.stderr).strip().splitlines()
tail = tail[-1] if tail else "(no output)"
died = p.returncode != 0
print(f" {label:26s} {'DIED' if died else 'SURVIVED':9s} "
f"exit={p.returncode} {tail}")
if not died:
survivors.append(label)
print("\nSurvivors:", survivors if survivors else "none")
return 1 if survivors else 0
finally:
shutil.rmtree(tmp, ignore_errors=True)
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,13 @@
guardless DIED exit=1 === Verification: 11/12 checks passed ===
no_main_guard DIED exit=1 === Verification: 10/12 checks passed ===
ffd_flip DIED exit=1 === Verification: 10/12 checks passed ===
ffd_return_none DIED exit=1 === Verification: 11/12 checks passed ===
ffd_return_i_plus_1 DIED exit=1 === Verification: 11/12 checks passed ===
ffd_delete_return DIED exit=1 === Verification: 11/12 checks passed ===
ffd_skip_index_zero DIED exit=1 === Verification: 11/12 checks passed ===
ffd_extend_range DIED exit=1 === Verification: 10/12 checks passed ===
ffd_always_return_none DIED exit=1 === Verification: 11/12 checks passed ===
hardcode_equal DIED exit=1 === Verification: 11/12 checks passed ===
reconstruct_overlap_zero DIED exit=1 === Verification: 11/12 checks passed ===
Survivors: none