#!/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)