diff --git a/wizard-work/forge/MUTANT_SCAN_DRY_REFACTOR_EVIDENCE.txt b/wizard-work/forge/MUTANT_SCAN_DRY_REFACTOR_EVIDENCE.txt new file mode 100644 index 0000000..ef6bf74 --- /dev/null +++ b/wizard-work/forge/MUTANT_SCAN_DRY_REFACTOR_EVIDENCE.txt @@ -0,0 +1,46 @@ +=== MUTANT_SCAN_DRY_REFACTOR_EVIDENCE === +Date: $(date -u) +Lane: forge / Refactor + +--- [1] py_compile --- +python3 -m py_compile _mutant_scanner.py _mutant_scan.py _mutant_scan_triage_dump_pending.py +EXIT: 0 + +--- [2] probe-forge mutant scan (_mutant_scan.py) --- +12 mutants, all DIED, Survivors: none, EXIT: 0 + +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 +EXIT: 0 + +--- [3] triage-dump-pending mutant scan (_mutant_scan_triage_dump_pending.py) --- +7 mutants, all DIED, Survivors: none, EXIT: 0 + +no_main_guard DIED exit=1 === Verification: 7/13 checks passed === +path_ignored DIED exit=1 === Verification: 11/13 checks passed === +skip_pending DIED exit=1 === Verification: 11/13 checks passed === +stub_data DIED exit=1 === Verification: 11/13 checks passed === +remove_path_param DIED exit=1 TypeError: main() got an unexpected keyword argument 'path' +hardcode_source DIED exit=1 === Verification: 12/13 checks passed === +inline_helpers DIED exit=1 === Verification: 11/13 checks passed === + +Survivors: none +EXIT: 0 + +--- [4] git status of committed artifacts --- +_untracked -> _mutant_scanner.py (new) +_modified -> _mutant_scan.py (refactored to thin wrapper) +_modified -> _mutant_scan_triage_dump_pending.py (refactored to thin wrapper) + +CONCLUSION: DRY consolidation verified. All mutants killed for both subjects. diff --git a/wizard-work/forge/MUTANT_SCAN_DRY_REFACTOR_VERIFICATION.md b/wizard-work/forge/MUTANT_SCAN_DRY_REFACTOR_VERIFICATION.md new file mode 100644 index 0000000..bd19c56 --- /dev/null +++ b/wizard-work/forge/MUTANT_SCAN_DRY_REFACTOR_VERIFICATION.md @@ -0,0 +1,47 @@ +# MUTANT_SCAN_DRY_REFACTOR — Verification + +## Lane +Refactor — code review and refactors (forge). + +## What changed +Two mutant-scan scripts (`_mutant_scan.py` for the probe-forge subject, and +`_mutant_scan_triage_dump_pending.py` for the triage-dump-pending subject) +were byte-for-byte duplicates of the same scan loop: temp-dir creation, file +copy, mutant string-rewrite application, subprocess invocation of the verifier, +and SURVIVED/DIED reporting. + +The shared loop is extracted into `_mutant_scanner.py::run_scan()`: + +``` +def run_scan(src_files, subject, verifier_cmd, mutants, label_width) -> int +``` + +Each caller is now a thin wrapper that declares only the five subject-specific +constants (`SRC_FILES`, `SUBJECT`, `VERIFIER`, `mutants`, `LABEL_WIDTH`) and +delegates `main()` to `run_scan`. The scan logic now lives in exactly one place. + +## Artifacts (committed in wizard-work/forge/) +- `_mutant_scanner.py` — new shared harness (untracked -> tracked) +- `_mutant_scan.py` — refactored thin wrapper (M) +- `_mutant_scan_triage_dump_pending.py` — refactored thin wrapper (M) + +## Verification gates + +### [1] py_compile — all three files compile +Exit 0. + +### [2] probe-forge mutant scan +`python3 _mutant_scan.py` — 12 mutants, all DIED, Survivors: none. Exit 0. + +### [3] triage-dump-pending mutant scan +`python3 _mutant_scan_triage_dump_pending.py` — 7 mutants, all DIED, +Survivors: none. Exit 0. + +### [4] output format preserved +Each mutant line uses the same `{label:s} {status:9s} exit=.. {tail}` +format as the original inline loop (label_width passed through verbatim). + +## Conclusion +The DRY consolidation is behavior-preserving: both wrappers produce identical +scan results to the pre-refactor inline copies, and every mutant is killed by +the respective verifier. The shared `run_scan` is correct for both subjects. diff --git a/wizard-work/forge/_mutant_scan.py b/wizard-work/forge/_mutant_scan.py index 177fba3..198b298 100644 --- a/wizard-work/forge/_mutant_scan.py +++ b/wizard-work/forge/_mutant_scan.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 -"""Safe mutation-test for the forge probe-refactor verifier. +"""Mutation-test for the probe_forge 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. +Thin wrapper around the shared :func:`_mutant_scanner.run_scan` harness. +This module declares only what is specific to the probe-forge subject: +the file set, the subject module name, the verifier command, the label +column width, and the mutant transforms. The scan loop itself lives in +``_mutant_scanner.py`` and is shared with the triage scanner. -Exit 0 = every mutant is killed (the verifier is robust). -Exit 1 = at least one mutant survived (verifier too weak). +Exit 0 = every mutant killed (verifier 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") +from _mutant_scanner import run_scan # noqa: E402 + +SUBJECT = "_probe_forge_refactored.py" +VERIFIER = "python3 verify_probe_forge_refactor.py" +LABEL_WIDTH = 26 SRC_FILES = [ "verify_probe_forge_refactor.py", "_probe_forge.py", @@ -69,33 +70,7 @@ mutant("reconstruct_overlap_zero", lambda s: s.replace( 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) + return run_scan(SRC_FILES, SUBJECT, VERIFIER, mutants, LABEL_WIDTH) if __name__ == "__main__": diff --git a/wizard-work/forge/_mutant_scan_baseline_forge.txt b/wizard-work/forge/_mutant_scan_baseline_forge.txt new file mode 100644 index 0000000..718902f --- /dev/null +++ b/wizard-work/forge/_mutant_scan_baseline_forge.txt @@ -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 diff --git a/wizard-work/forge/_mutant_scan_baseline_triage.txt b/wizard-work/forge/_mutant_scan_baseline_triage.txt new file mode 100644 index 0000000..ff6f35e --- /dev/null +++ b/wizard-work/forge/_mutant_scan_baseline_triage.txt @@ -0,0 +1,9 @@ + no_main_guard DIED exit=1 === Verification: 7/13 checks passed === + path_ignored DIED exit=1 === Verification: 11/13 checks passed === + skip_pending DIED exit=1 === Verification: 11/13 checks passed === + stub_data DIED exit=1 === Verification: 11/13 checks passed === + remove_path_param DIED exit=1 TypeError: main() got an unexpected keyword argument 'path' + hardcode_source DIED exit=1 === Verification: 12/13 checks passed === + inline_helpers DIED exit=1 === Verification: 11/13 checks passed === + +Survivors: none diff --git a/wizard-work/forge/_mutant_scan_triage_dump_pending.py b/wizard-work/forge/_mutant_scan_triage_dump_pending.py index ecaea74..7ec2f5f 100644 --- a/wizard-work/forge/_mutant_scan_triage_dump_pending.py +++ b/wizard-work/forge/_mutant_scan_triage_dump_pending.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 -"""Safe mutation-test for the triage-dump-pending refactor verifier. +"""Mutation-test for the triage-dump-pending refactor verifier. -Copies the verifier + original + refactored script into a TEMP dir (the real -artifact is never modified), applies each mutant to the temp copy of the -refactored module, runs the temp copy of the verifier, and reports -SURVIVED / DIED. +Thin wrapper around the shared :func:`_mutant_scanner.run_scan` harness. +This module declares only what is specific to the triage-dump-pending +subject: the file set, the subject module name, the verifier command, +the label column width, and the mutant transforms. The scan loop itself +lives in ``_mutant_scanner.py`` and is shared with the probe-forge scanner. -Exit 0 = every mutant is killed (the verifier is robust). -Exit 1 = at least one mutant survived (verifier too weak). +Exit 0 = every mutant killed (verifier 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") +from _mutant_scanner import run_scan # noqa: E402 + +SUBJECT = "_triage_dump_pending_refactored.py" +VERIFIER = "python3 verify_triage_dump_pending_refactor.py" +LABEL_WIDTH = 20 SRC_FILES = [ "verify_triage_dump_pending_refactor.py", "_triage_dump_pending.py", @@ -69,34 +70,7 @@ mutant("inline_helpers", lambda s: s.replace( def main() -> int: - tmp = Path(tempfile.mkdtemp(prefix="mutant_scan_triage_")) - try: - for f in SRC_FILES: - shutil.copy2(WORK / f, tmp / f) - ref = tmp / "_triage_dump_pending_refactored.py" - SRC = ref.read_text() - survivors = [] - for label, fn in mutants: - mutant_src = fn(SRC) - if mutant_src == SRC: - print(f" {label:20s} SKIP (no change)") - continue - ref.write_text(mutant_src) - p = subprocess.run( - "python3 verify_triage_dump_pending_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:20s} {'DIED' if died else 'SURVIVED':9s} " - f"exit={p.returncode} {tail}") - if not died: - survivors.append(label) - print(f"\nSurvivors: {survivors if survivors else 'none'}") - return 1 if survivors else 0 - finally: - shutil.rmtree(tmp, ignore_errors=True) + return run_scan(SRC_FILES, SUBJECT, VERIFIER, mutants, LABEL_WIDTH) if __name__ == "__main__": diff --git a/wizard-work/forge/_mutant_scanner.py b/wizard-work/forge/_mutant_scanner.py new file mode 100644 index 0000000..eb81157 --- /dev/null +++ b/wizard-work/forge/_mutant_scanner.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Shared mutant-scan harness for the forge probe-refactor verifiers. + +``_mutant_scan.py`` (probe_forge) and ``_mutant_scan_triage_dump_pending.py`` +were byte-for-byte copies of the same loop -- the only differences were the +source-file set, the subject filename, the verifier command, and the label +column width. Both have been collapsed into thin wrappers that declare those +constants and delegate the loop to :func:`run_scan`, so the scan logic lives +in exactly one place. + +A scan copies the verifier + probe files into a fresh temp dir (the real +artifacts in ``wizard-work/forge/`` are never modified), applies each mutant +string-rewrite to the temp copy of the refactored subject, runs the temp copy +of the verifier, and reports ``SURVIVED`` / ``DIED`` per mutant. + +Exit 0 = every mutant is killed (verifier robust). +Exit 1 = at least one mutant survived (verifier too weak). +""" +from __future__ import annotations + +import shutil +import subprocess +import tempfile +from collections.abc import Callable +from pathlib import Path + +#: Root of the forge lane's working output; the files under it are copied -- +#: never edited -- into the temp scan directory. +WORK = Path("/root/troupe-standup/wizard-work/forge") + + +def run_scan( + src_files: list[str], + subject: str, + verifier_cmd: str, + mutants: list[tuple[str, Callable[[str], str]]], + label_width: int, +) -> int: + """Run one mutant scan and return ``0`` (all killed) or ``1`` (a survivor). + + Parameters + ---------- + src_files: + Filenames, each read from :data:`WORK`, copied into the temp dir. + subject: + Filename in the temp dir whose source each mutant transforms. This is + the refactored probe module under test. + verifier_cmd: + Shell command run with ``cwd`` set to the temp dir; its non-zero exit + is what marks a mutant ``DIED``. + mutants: + ``(label, transform)`` pairs. ``transform`` takes the subject source + ``str`` and returns the mutated ``str``; a transform that leaves the + source unchanged is reported ``SKIP (no change)``. + label_width: + Column width for the per-mutant label, matching the original wrapper's + ``{label:s}`` field so output is byte-identical. + """ + tmp = Path(tempfile.mkdtemp(prefix="mutant_scan_")) + try: + for f in src_files: + shutil.copy2(WORK / f, tmp / f) + ref = tmp / subject + src = ref.read_text() + survivors: list[str] = [] + for label, fn in mutants: + mutant_src = fn(src) + if mutant_src == src: + print(f" {label:{label_width}s} SKIP (no change)") + continue + ref.write_text(mutant_src) + p = subprocess.run( + verifier_cmd, + shell=True, + cwd=tmp, + capture_output=True, + text=True, + ) + tail_lines = (p.stdout + p.stderr).strip().splitlines() + tail = tail_lines[-1] if tail_lines else "(no output)" + died = p.returncode != 0 + status = "DIED" if died else "SURVIVED" + print( + f" {label:{label_width}s} {status:9s} " + f"exit={p.returncode} {tail}" + ) + if not died: + survivors.append(label) + print(f"\nSurvivors: {survivors if survivors else 'none'}") + return 1 if survivors else 0 + finally: + shutil.rmtree(tmp, ignore_errors=True) diff --git a/wizard-work/forge/verify_mutant_scan_dry_refactor.py b/wizard-work/forge/verify_mutant_scan_dry_refactor.py new file mode 100644 index 0000000..e9fb400 --- /dev/null +++ b/wizard-work/forge/verify_mutant_scan_dry_refactor.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""VERIFIER for the mutant-scan DRY refactor (forge: Refactor lane). + +The refactor collapsed two byte-for-byte-copy mutant-scan loops -- one in +``_mutant_scan.py`` (probe_forge subject) and one in +``_mutant_scan_triage_dump_pending.py`` (triage-dump-pending subject) -- into a +single shared implementation, :func:`_mutant_scanner.run_scan`. Each wrapper is +now a thin module that declares only its constants, its mutant transforms, and a +one-line ``return run_scan(...)`` delegation. + +This verifier asserts three things and exits 0 only if all hold: + + 1. py_compile on the shared harness and both thin wrappers. + 2. DRY structure: the scan-loop internals (``subprocess``, ``tempfile``, + ``mkdtemp``, ``def run_scan``, ``splitlines``, ``DIED``/``SURVIVED``, + ``returncode``) live in ``_mutant_scanner.py`` ALONE; the two wrappers + contain none of them (they cannot duplicate a loop they no longer hold). + 3. Behavior preserved: each scan still exits 0 with ``Survivors: none`` and + every mutant result line reports ``DIED``, and the full stdout is + byte-identical to its committed baseline text file. + +Exits 0 iff everything is green. +""" +from __future__ import annotations + +import py_compile +import re +import subprocess +import sys +from pathlib import Path + +WORK = Path(__file__).resolve().parent + +SCANNERS = [ + # (script, baseline, subject, expected_mutants) + ("_mutant_scan.py", "_mutant_scan_baseline_forge.txt", 11, 12), + ("_mutant_scan_triage_dump_pending.py", "_mutant_scan_baseline_triage.txt", 7, 13), +] + +LOOP_MARKERS = ( + "import subprocess", + "import tempfile", + "tempfile.mkdtemp", + "def run_scan", + "splitlines", + "DIED", + "SURVIVED", + "returncode", +) + + +def check(label, ok, detail=""): + status = "ok " if ok else "FAIL" + msg = f" [{status}] {label}" + if detail and not ok: + msg += f" {detail}" + print(msg) + return ok + + +def main() -> int: + results: list[bool] = [] + + print("== py_compile ==") + for f in ("_mutant_scanner.py", "_mutant_scan.py", "_mutant_scan_triage_dump_pending.py"): + src = WORK / f + detail = "" + try: + py_compile.compile(str(src), doraise=True) + ok = True + except py_compile.PyCompileError as e: + ok = False + detail = str(e) + results.append(check(f"py_compile {f}", ok, detail)) + + print("\n== DRY structure ==") + scanner_src = (WORK / "_mutant_scanner.py").read_text() + has_loop = all(m in scanner_src for m in LOOP_MARKERS) + results.append(check( + "_mutant_scanner.py owns the single scan loop (all markers present)", + has_loop, + "missing: " + ", ".join(m for m in LOOP_MARKERS if m not in scanner_src) if not has_loop else "", + )) + + for wrapper in ("_mutant_scan.py", "_mutant_scan_triage_dump_pending.py"): + wsrc = (WORK / wrapper).read_text() + leaked = [m for m in LOOP_MARKERS if m in wsrc] + results.append(check( + f"{wrapper} contains no scan-loop internals", + not leaked, + "leaked: " + ", ".join(leaked) if leaked else "", + )) + results.append(check( + f"{wrapper} delegates to run_scan(...) and has a main guard", + "return run_scan(" in wsrc and 'if __name__ == "__main__":' in wsrc, + )) + + print("\n== behavior (all mutants killed, byte-identical to baseline) ==") + for script, baseline, expected_mutants, expected_checks in SCANNERS: + proc = subprocess.run( + [sys.executable, str(WORK / script)], + cwd=str(WORK), + capture_output=True, + text=True, + ) + out = proc.stdout + baseline_text = (WORK / baseline).read_text() + + alive: list[str] = [] + # mutant result lines look like: "