forge: behavior-preserving refactor of triage_probe probe (dead sys import, main guard, extracted helpers, configurable path)
This commit is contained in:
1 parent
75b41cacf1
commit
431bca1be9
5 files changed
+663
No files matched your search
@@ -0,0 +1,136 @@
|
|||||||
|
# forge — triage_probe refactor: behavior-preserving code review (VERIFICATION)
|
||||||
|
|
||||||
|
**Lane:** Refactor — code review and refactors
|
||||||
|
**Artifact committed to:** `wizard-work/forge/` (`_triage_probe.py`, `_triage_probe_refactored.py`, `verify_triage_probe_refactor.py`, `_triage_probe_refactor_CHECK_EVIDENCE.txt`)
|
||||||
|
**Verdict:** 11/11 checks pass; probe output byte-identical to the original (6946 bytes, sha256 `49d9b87b…364cd9`).
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
`_triage_probe.py` is a byte-exact copy of the repo-root throwaway probe
|
||||||
|
`.triage_probe.py`. It dumps the decided/pending items from the triage inbox seed
|
||||||
|
`inbox/seed-2026-09-26.json` (1 decided, 12 pending). The original ran its
|
||||||
|
**entire** probe at module top level: it opened the JSON, loaded it into the
|
||||||
|
module-global `d`, and printed everything at import time, with no
|
||||||
|
`if __name__ == "__main__"` guard. Importing it therefore executed the probe and
|
||||||
|
wrote 6946 bytes to stdout. It also imported `sys` on the same line as `json`
|
||||||
|
and never referenced it (dead import; PEP 8 E401), hardcoded the JSON path at
|
||||||
|
module scope, and recomputed `d.get("decided", [])` / `d.get("pending", [])`
|
||||||
|
inline.
|
||||||
|
|
||||||
|
## Smells fixed (behavior-preserving)
|
||||||
|
|
||||||
|
1. **Dead + jammed import.** `import json, sys` is replaced by a single
|
||||||
|
`import json`. `sys` was imported but never used — confirmed dead by AST: the
|
||||||
|
only `sys` mention in the original is the import alias, zero `sys` attribute
|
||||||
|
references. The entry point matches the original's plain exit (no `sys.exit`),
|
||||||
|
so exit code stays 0 and output is unchanged.
|
||||||
|
- *Measured:* `check_dead_sys_import_removed` — refactored imports no `sys`;
|
||||||
|
original imports `sys` and never uses it (AST usage scan = 0).
|
||||||
|
|
||||||
|
2. **Module-level execution.** All file I/O, JSON loading, and printing now live
|
||||||
|
under `main()`. The module imports with zero side effects; the entry point is
|
||||||
|
explicitly guarded with `if __name__ == "__main__": main()`.
|
||||||
|
- *Measured:* `check_import_side_effect_free` — importing produces no stdout/
|
||||||
|
stderr and no `SystemExit`; `check_main_guard` — the guard is in code
|
||||||
|
(module docstring stripped so docstring-only mentions can't false-positive).
|
||||||
|
|
||||||
|
3. **Hardcoded path at module scope.** The bare `path = "/root/.../seed...json"`
|
||||||
|
assignment is promoted to the named constant `DEFAULT_PATH` and made the
|
||||||
|
`path` parameter of `main()`. Callers/tests can override it without editing
|
||||||
|
source; the default is unchanged, so output is byte-identical when run with no
|
||||||
|
args.
|
||||||
|
- *Measured:* `check_main_path_parameter` — `main` AST has a `path` arg.
|
||||||
|
|
||||||
|
4. **No function extraction / repeated `d.get`.** The three inline print blocks
|
||||||
|
(header, decided iteration, pending iteration) are extracted into the pure
|
||||||
|
helpers `_print_header`, `_print_decided`, `_print_pending` (plus `load_data`);
|
||||||
|
`d.get("decided", [])` and `d.get("pending", [])` are evaluated once in
|
||||||
|
`main()` and passed to the helpers as arguments instead of recomputed at
|
||||||
|
module scope. The module-global `d` is gone.
|
||||||
|
- *Measured:* `check_helpers_extracted` — all five module-level functions
|
||||||
|
exist and `main` calls the three printers; `check_no_globals` — no `global`
|
||||||
|
statements; `check_main_delegation_intact` — main delegates via distinct
|
||||||
|
standalone statements (no `;`-compaction).
|
||||||
|
|
||||||
|
## Verification harness
|
||||||
|
|
||||||
|
`verify_triage_probe_refactor.py` — 11 checks, each a real subprocess/AST
|
||||||
|
assertion (no tautology):
|
||||||
|
|
||||||
|
| # | check | what it proves |
|
||||||
|
|---|-------|-----------------|
|
||||||
|
| 1 | `py_compile` | refactored file is syntactically valid |
|
||||||
|
| 2 | identical output | `original` vs `refactored` produce byte-identical stdout+stderr AND exit code |
|
||||||
|
| 3 | `__main__` guard | refactored has the guard (original's smell), in code not docstring |
|
||||||
|
| 4 | no `global` | no `global` statements in code |
|
||||||
|
| 5 | import side-effect-free | importing produces no output and no `SystemExit` |
|
||||||
|
| 6 | helpers extracted | `_print_header/_print_decided/_print_pending/load_data/main` exist; `main` calls the three print helpers |
|
||||||
|
| 7 | main delegation | `main()` body has no `;`-compacted statements (extraction boundary intact) |
|
||||||
|
| 8 | `main(path)` param | `main` accepts a configurable `path` parameter |
|
||||||
|
| 9 | dead `sys` removed | refactored imports no `sys`; original imported `sys` and never used it |
|
||||||
|
| 10 | original still smells | non-vacuous: original still lacks guard, still hardcodes the path, still has module-level `for`, still has zero function defs, still carries the dead `sys` import |
|
||||||
|
| 11 | path override honoured | `main(path=custom)` prints the custom file, not `DEFAULT_PATH` — kills the "accepts-but-ignores-path" mutation, which is byte-identical on the real seed and thus evades check 2 |
|
||||||
|
|
||||||
|
## Evidence (measured, this run)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ python3 verify_triage_probe_refactor.py
|
||||||
|
OK: py_compile passes
|
||||||
|
OK: identical output (exit 0, 6946 bytes)
|
||||||
|
OK: __main__ guard present
|
||||||
|
OK: no 'global' statements in code
|
||||||
|
OK: import is side-effect-free (no probe output at import)
|
||||||
|
OK: _print_header, _print_decided, _print_pending, load_data, main extracted; main calls all three print helpers
|
||||||
|
OK: main() delegates via distinct standalone statements (no ';'-compaction)
|
||||||
|
OK: main(path) accepts a configurable path parameter
|
||||||
|
OK: dead `sys` import removed (original imported sys, never used it)
|
||||||
|
OK: original still exhibits the removed smells (refactor is non-vacuous)
|
||||||
|
OK: main(path=custom) honours the override (prints custom file, not DEFAULT_PATH)
|
||||||
|
|
||||||
|
=== Verification: 11/11 checks passed === (exit 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Full capture in `wizard-work/forge/_triage_probe_refactor_CHECK_EVIDENCE.txt`.
|
||||||
|
|
||||||
|
## Non-vacuity
|
||||||
|
|
||||||
|
Check 10 guarantees the refactor actually removed something: the *original*
|
||||||
|
`_triage_probe.py` still exhibits every smell the refactored version no longer
|
||||||
|
does (no `__main__` guard, hardcoded path at module scope, two module-level `for`
|
||||||
|
loops, zero function definitions, and the dead `sys` import). A no-op "refactor"
|
||||||
|
would fail this.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ grep -nE '^def |^import |__main__|for x in d\.get' _triage_probe.py
|
||||||
|
1:import json, sys
|
||||||
|
14:for x in d.get("decided", []):
|
||||||
|
19:for x in d.get("pending", []):
|
||||||
|
def count -> 0
|
||||||
|
main-guard count -> 0
|
||||||
|
module-level for -> 2
|
||||||
|
sys references -> 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mutation kill
|
||||||
|
|
||||||
|
Check 11 (path override) is the key discriminator the byte-comparison cannot see:
|
||||||
|
a `path` parameter that is accepted but ignored (always reads `DEFAULT_PATH`)
|
||||||
|
produces byte-identical output on the real seed but prints the wrong data on a
|
||||||
|
custom file. The check feeds a synthetic JSON and asserts the custom data
|
||||||
|
(`"overridden"`) appears and the real seed path does not.
|
||||||
|
|
||||||
|
Check 9 (dead `sys` import) is the analogous discriminator for the import smell:
|
||||||
|
re-adding `import sys` without using it is behavior-preserving on the probe, so
|
||||||
|
output comparison cannot catch it — the AST import-scope check does.
|
||||||
|
|
||||||
|
## Independent byte-identical proof (outside the harness)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ python3 _triage_probe.py > /tmp/r1 2>/tmp/e1
|
||||||
|
$ python3 _triage_probe_refactored.py > /tmp/r2 2>/tmp/e2
|
||||||
|
$ sha256sum /tmp/r1 /tmp/r2
|
||||||
|
49d9b87be277211a29c6ba6f685e8516df0f0235c6fa332f52a9d3f00b779f34 /tmp/r1
|
||||||
|
49d9b87be277211a29c6ba6f685e8516df0f0235c6fa332f52a9d3f00b779f34 /tmp/r2
|
||||||
|
$ diff /tmp/r1 /tmp/r2 && echo IDENTICAL
|
||||||
|
IDENTICAL (diff exit 0)
|
||||||
|
```
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import json, sys
|
||||||
|
|
||||||
|
path = "/root/troupe-standup/inbox/seed-2026-09-26.json"
|
||||||
|
with open(path) as f:
|
||||||
|
d = json.load(f)
|
||||||
|
|
||||||
|
print("keys:", list(d.keys()))
|
||||||
|
print("exported_at:", d.get("exported_at"))
|
||||||
|
print("decided_count:", len(d.get("decided", [])))
|
||||||
|
print("pending_count:", len(d.get("pending", [])))
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=== DECIDED ===")
|
||||||
|
for x in d.get("decided", []):
|
||||||
|
print(x.get("id"), "|", x.get("summary"), "|", x.get("decision"))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=== PENDING ===")
|
||||||
|
for x in d.get("pending", []):
|
||||||
|
print("id:", x.get("id"))
|
||||||
|
print(" summary:", x.get("summary"))
|
||||||
|
print(" escalated_by:", x.get("escalated_by"))
|
||||||
|
print(" created_at:", x.get("created_at"))
|
||||||
|
print(" proposed_action:", x.get("proposed_action"))
|
||||||
|
print(" evidence:", (x.get("evidence") or "")[:300].replace("\n", " "))
|
||||||
|
print()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
$ python3 verify_triage_probe_refactor.py
|
||||||
|
OK: py_compile passes
|
||||||
|
OK: identical output (exit 0, 6946 bytes)
|
||||||
|
OK: __main__ guard present
|
||||||
|
OK: no 'global' statements in code
|
||||||
|
OK: import is side-effect-free (no probe output at import)
|
||||||
|
OK: _print_header, _print_decided, _print_pending, load_data, main extracted; main calls all three print helpers
|
||||||
|
OK: main() delegates via distinct standalone statements (no ';'-compaction)
|
||||||
|
OK: main(path) accepts a configurable path parameter
|
||||||
|
OK: dead `sys` import removed (original imported sys, never used it)
|
||||||
|
OK: original still exhibits the removed smells (refactor is non-vacuous)
|
||||||
|
OK: main(path=custom) honours the override (prints custom file, not DEFAULT_PATH)
|
||||||
|
|
||||||
|
=== Verification: 11/11 checks passed ===
|
||||||
|
HARNESS_EXIT=0
|
||||||
|
|
||||||
|
$ independent byte-identical proof:
|
||||||
|
orig bytes=6946 refactored bytes=6946
|
||||||
|
49d9b87be277211a29c6ba6f685e8516df0f0235c6fa332f52a9d3f00b779f34 /tmp/r1
|
||||||
|
49d9b87be277211a29c6ba6f685e8516df0f0235c6fa332f52a9d3f00b779f34 /tmp/r2
|
||||||
|
IDENTICAL (diff exit 0)
|
||||||
|
|
||||||
|
$ smell surface of original _triage_probe.py:
|
||||||
|
1:import json, sys
|
||||||
|
14:for x in d.get("decided", []):
|
||||||
|
19:for x in d.get("pending", []):
|
||||||
|
def count -> 0
|
||||||
|
main-guard count -> 0
|
||||||
|
module-level for -> 2
|
||||||
|
sys references -> 1
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Triage probe (refactored, forge-lane Refactor).
|
||||||
|
|
||||||
|
Refactored from ``_triage_probe.py`` (a byte-identical copy of the repo-root
|
||||||
|
``.triage_probe.py``). The original ran its entire probe at **module top
|
||||||
|
level** -- opening the JSON, loading it into the module-global ``d``, and
|
||||||
|
printing all output at import time, with no ``if __name__ == "__main__"`` guard.
|
||||||
|
Importing it executed the probe and wrote to stdout. It also imported ``sys``
|
||||||
|
on the same line as ``json`` and never referenced it (a dead import; PEP 8
|
||||||
|
E401).
|
||||||
|
|
||||||
|
Smells fixed in this refactor (behavior-preserving on the probe itself):
|
||||||
|
|
||||||
|
* **Dead + jammed import.** ``import json, sys`` is replaced by a single
|
||||||
|
``import json``. ``sys`` was imported but never used; it is removed. The
|
||||||
|
entry point matches the original's plain exit (no ``sys.exit``), so exit
|
||||||
|
code stays 0 and output is unchanged.
|
||||||
|
|
||||||
|
* **Module-level execution.** All file I/O, JSON loading and printing now
|
||||||
|
live under ``main()``; the module imports with zero side effects and the
|
||||||
|
entry point is explicitly guarded. ``import _triage_probe_refactored``
|
||||||
|
produces no output and does not terminate the process.
|
||||||
|
|
||||||
|
* **Hardcoded path at module scope.** The JSON path was a bare module-level
|
||||||
|
assignment with no override. It is promoted to the named constant
|
||||||
|
``DEFAULT_PATH`` and made the ``path`` parameter of ``main``, so callers
|
||||||
|
(including tests) can override it without editing source. The default is
|
||||||
|
unchanged, so output is byte-identical when run with no args.
|
||||||
|
|
||||||
|
* **No function extraction / repeated ``d.get``.** The inline print blocks
|
||||||
|
(header, decided iteration, pending iteration) are extracted into the pure
|
||||||
|
helpers ``_print_header``, ``_print_decided`` and ``_print_pending``.
|
||||||
|
The repeated ``d.get("decided", [])`` / ``d.get("pending", [])`` expressions
|
||||||
|
(recomputed at module scope in the original) are evaluated once in ``main``
|
||||||
|
and passed to the helpers as arguments instead.
|
||||||
|
|
||||||
|
Output is byte-identical to the original on the current inbox seed
|
||||||
|
(``inbox/seed-2026-09-26.json``): 6946 bytes, sha256
|
||||||
|
``49d9b87be277211a29c6ba6f685e8516df0f0235c6fa332f52a9d3f00b779f34``.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
DEFAULT_PATH = "/root/troupe-standup/inbox/seed-2026-09-26.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_data(path):
|
||||||
|
"""Load the triage seed JSON from *path*."""
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_header(d, decided, pending):
|
||||||
|
"""Print the seed keys, metadata, and decided/pending counts, then a blank."""
|
||||||
|
print("keys:", list(d.keys()))
|
||||||
|
print("exported_at:", d.get("exported_at"))
|
||||||
|
print("decided_count:", len(decided))
|
||||||
|
print("pending_count:", len(pending))
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def _print_decided(decided):
|
||||||
|
"""Print the decided items (one line each)."""
|
||||||
|
print("=== DECIDED ===")
|
||||||
|
for x in decided:
|
||||||
|
print(x.get("id"), "|", x.get("summary"), "|", x.get("decision"))
|
||||||
|
|
||||||
|
|
||||||
|
def _print_pending(pending):
|
||||||
|
"""Print the pending items (a blank, the header, and one block per item)."""
|
||||||
|
print()
|
||||||
|
print("=== PENDING ===")
|
||||||
|
for x in pending:
|
||||||
|
print("id:", x.get("id"))
|
||||||
|
print(" summary:", x.get("summary"))
|
||||||
|
print(" escalated_by:", x.get("escalated_by"))
|
||||||
|
print(" created_at:", x.get("created_at"))
|
||||||
|
print(" proposed_action:", x.get("proposed_action"))
|
||||||
|
print(" evidence:", (x.get("evidence") or "")[:300].replace("\n", " "))
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def main(path=DEFAULT_PATH):
|
||||||
|
"""Load the triage seed at *path* and print its decided/pending contents."""
|
||||||
|
d = load_data(path)
|
||||||
|
decided = d.get("decided", [])
|
||||||
|
pending = d.get("pending", [])
|
||||||
|
_print_header(d, decided, pending)
|
||||||
|
_print_decided(decided)
|
||||||
|
_print_pending(pending)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verification harness for the _triage_probe_refactored.py refactor.
|
||||||
|
|
||||||
|
Exit 0 = all checks pass; exit 1 = at least one failure.
|
||||||
|
|
||||||
|
The target ``_triage_probe.py`` is a probe that dumps the decided/pending
|
||||||
|
items from the triage inbox seed ``inbox/seed-2026-09-26.json`` (1 decided,
|
||||||
|
12 pending). The original ran the whole probe at module top level, imported
|
||||||
|
``sys`` and never used it, hard-coded the JSON path at module scope, exposed a
|
||||||
|
module-global ``d``, and recomputed ``d.get(...)`` inline. The refactor is
|
||||||
|
*behavior-preserving* on the probe itself (same JSON input, same output).
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
|
||||||
|
1. Refactored file compiles (py_compile).
|
||||||
|
2. Original and refactored produce byte-identical stdout+stderr AND exit code.
|
||||||
|
3. ``if __name__ == "__main__"`` guard present in code (probe does not run at
|
||||||
|
import; docstring-only mentions do not count -- ``_code_only`` strips the
|
||||||
|
module docstring and comments).
|
||||||
|
4. No ``global`` statements in code.
|
||||||
|
5. Import of the refactored module is side-effect-free (no stdout/stderr and
|
||||||
|
no SystemExit at import -- the original's primary smell).
|
||||||
|
6. Pure helpers ``_print_header``, ``_print_decided``, ``_print_pending``,
|
||||||
|
``load_data`` and ``main`` are module-level functions, and ``main`` calls
|
||||||
|
the three print helpers (extraction is real, not just renamed).
|
||||||
|
7. ``main`` delegates via distinct standalone statements (no ``;``-compaction
|
||||||
|
that would collapse the three call boundaries into one line).
|
||||||
|
8. ``main`` accepts a ``path`` parameter (the hardcoded path is now
|
||||||
|
configurable, not a module-level assignment that runs on import).
|
||||||
|
9. Dead ``sys`` import removed: the refactored module imports neither ``sys``
|
||||||
|
nor ``import json, sys``; the original imported ``sys`` and never used it.
|
||||||
|
10. Non-vacuity: the *original* ``_triage_probe.py`` still exhibits the removed
|
||||||
|
smells (no __main__ guard, hardcoded path at module scope, module-level
|
||||||
|
for-loop, no function definitions, still carries the dead ``sys`` import).
|
||||||
|
11. ``main(path)`` honours the override: calling with a custom temp JSON path
|
||||||
|
prints the custom file's contents. This kills the mutation of a ``path``
|
||||||
|
parameter that is accepted but ignored (byte-identical on the real seed
|
||||||
|
would not catch this).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import contextlib
|
||||||
|
import difflib
|
||||||
|
import importlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
# Resolve paths from this file's location (wizard-work/forge/).
|
||||||
|
WORK_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
ORIGINAL = "_triage_probe.py"
|
||||||
|
REFACTORED = "_triage_probe_refactored.py"
|
||||||
|
MOD_NAME = REFACTORED[:-3] # strip .py -> _triage_probe_refactored
|
||||||
|
REAL_SEED_PATH = "/root/troupe-standup/inbox/seed-2026-09-26.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _run(*args: str) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run(args, cwd=WORK_DIR, capture_output=True, text=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _read(name: str) -> str:
|
||||||
|
with open(os.path.join(WORK_DIR, name), encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def _code_only(content: str) -> str:
|
||||||
|
"""Strip the module docstring and comment-only lines.
|
||||||
|
|
||||||
|
Smell-absence checks must inspect code, not documentation: the refactored
|
||||||
|
module's docstring *names* the patterns it removes, which would be false
|
||||||
|
positives as residual smells.
|
||||||
|
"""
|
||||||
|
tree = ast.parse(content)
|
||||||
|
skip: set[int] = set()
|
||||||
|
if (tree.body
|
||||||
|
and isinstance(tree.body[0], ast.Expr)
|
||||||
|
and isinstance(tree.body[0].value, ast.Constant)
|
||||||
|
and isinstance(tree.body[0].value.value, str)):
|
||||||
|
ds = tree.body[0]
|
||||||
|
for ln in range(ds.lineno, (ds.end_lineno or ds.lineno) + 1):
|
||||||
|
skip.add(ln)
|
||||||
|
kept = []
|
||||||
|
for i, line in enumerate(content.splitlines(), 1):
|
||||||
|
if i in skip or line.lstrip().startswith("#"):
|
||||||
|
continue
|
||||||
|
kept.append(line)
|
||||||
|
return "\n".join(kept)
|
||||||
|
|
||||||
|
|
||||||
|
def _imported_names(tree: ast.AST) -> set[str]:
|
||||||
|
names: set[str] = set()
|
||||||
|
for n in ast.walk(tree):
|
||||||
|
if isinstance(n, ast.Import):
|
||||||
|
for a in n.names:
|
||||||
|
names.add(a.name)
|
||||||
|
elif isinstance(n, ast.ImportFrom):
|
||||||
|
for a in n.names:
|
||||||
|
names.add(a.name)
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_import():
|
||||||
|
"""Import MOD_NAME from WORK_DIR, capturing stdout/stderr and any exception.
|
||||||
|
|
||||||
|
Returns (module, exc, out, err). ``exc`` is set when the import raised,
|
||||||
|
including ``SystemExit`` from a guard-less module that calls ``sys.exit`` at
|
||||||
|
module scope.
|
||||||
|
|
||||||
|
CENTRALISED GUARD: an unguarded ``importlib.import_module`` used to let a
|
||||||
|
``SystemExit(0)`` propagate and kill the verifier process with rc 0, masking
|
||||||
|
the ``no_main_guard`` mutant. Both call sites below use this helper so
|
||||||
|
neither can forget the try/except.
|
||||||
|
"""
|
||||||
|
sys.modules.pop(MOD_NAME, None)
|
||||||
|
if WORK_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, WORK_DIR)
|
||||||
|
out, err = io.StringIO(), io.StringIO()
|
||||||
|
try:
|
||||||
|
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
||||||
|
mod = importlib.import_module(MOD_NAME)
|
||||||
|
except BaseException as exc:
|
||||||
|
return None, exc, out.getvalue(), err.getvalue()
|
||||||
|
return mod, None, out.getvalue(), err.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def check_py_compile() -> bool:
|
||||||
|
r = _run("python3", "-m", "py_compile", REFACTORED)
|
||||||
|
if r.returncode != 0:
|
||||||
|
print(f"FAIL: py_compile failed: {r.stderr}")
|
||||||
|
return False
|
||||||
|
print("OK: py_compile passes")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_identical_output() -> bool:
|
||||||
|
orig = _run("python3", ORIGINAL)
|
||||||
|
ref = _run("python3", REFACTORED)
|
||||||
|
ot = orig.stdout + orig.stderr
|
||||||
|
rt = ref.stdout + ref.stderr
|
||||||
|
if ot != rt:
|
||||||
|
print("FAIL: original and refactored output differ")
|
||||||
|
print("".join(difflib.unified_diff(
|
||||||
|
ot.splitlines(keepends=True), rt.splitlines(keepends=True),
|
||||||
|
fromfile="original (" + ORIGINAL + ")",
|
||||||
|
tofile="refactored (" + REFACTORED + ")",
|
||||||
|
)))
|
||||||
|
return False
|
||||||
|
if orig.returncode != ref.returncode:
|
||||||
|
print(f"FAIL: exit codes differ ({orig.returncode} vs {ref.returncode})")
|
||||||
|
return False
|
||||||
|
print(f"OK: identical output (exit {orig.returncode}, {len(ot)} bytes)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_main_guard() -> bool:
|
||||||
|
code = _code_only(_read(REFACTORED))
|
||||||
|
if '__name__ == "__main__"' not in code:
|
||||||
|
print("FAIL: no __main__ guard in code (docstring-only matches do not count)")
|
||||||
|
return False
|
||||||
|
print("OK: __main__ guard present")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_no_globals() -> bool:
|
||||||
|
code = _code_only(_read(REFACTORED))
|
||||||
|
for i, line in enumerate(code.splitlines(), 1):
|
||||||
|
if line.lstrip().startswith("global "):
|
||||||
|
print(f"FAIL: 'global' found at code line {i}: {line.strip()}")
|
||||||
|
return False
|
||||||
|
print("OK: no 'global' statements in code")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_import_side_effect_free() -> bool:
|
||||||
|
"""Importing the refactored module must not run the probe or print."""
|
||||||
|
mod, exc, out, err = _safe_import()
|
||||||
|
if exc is not None:
|
||||||
|
print(f"FAIL: import raised {type(exc).__name__}: {exc!r} "
|
||||||
|
f"(stdout={out!r} stderr={err!r})")
|
||||||
|
return False
|
||||||
|
if out or err:
|
||||||
|
print(f"FAIL: import produced output: stdout={out!r} stderr={err!r}")
|
||||||
|
return False
|
||||||
|
print("OK: import is side-effect-free (no probe output at import)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_helpers_extracted() -> bool:
|
||||||
|
tree = ast.parse(_read(REFACTORED))
|
||||||
|
mod_funcs = {
|
||||||
|
n.name for n in tree.body
|
||||||
|
if isinstance(n, ast.FunctionDef)
|
||||||
|
}
|
||||||
|
for required in ("_print_header", "_print_decided", "_print_pending",
|
||||||
|
"load_data", "main"):
|
||||||
|
if required not in mod_funcs:
|
||||||
|
print(f"FAIL: {required} not defined as module-level function")
|
||||||
|
return False
|
||||||
|
main_func = next(n for n in tree.body
|
||||||
|
if isinstance(n, ast.FunctionDef) and n.name == "main")
|
||||||
|
name_calls = {
|
||||||
|
n.func.id for n in ast.walk(main_func)
|
||||||
|
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
|
||||||
|
}
|
||||||
|
for needed in ("_print_header", "_print_decided", "_print_pending"):
|
||||||
|
if needed not in name_calls:
|
||||||
|
print(f"FAIL: main() does not call {needed}()")
|
||||||
|
return False
|
||||||
|
print("OK: _print_header, _print_decided, _print_pending, load_data, "
|
||||||
|
"main extracted; main calls all three print helpers")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_main_delegation_intact() -> bool:
|
||||||
|
"""main() must delegate to the extracted helpers via distinct statements.
|
||||||
|
|
||||||
|
Guards the function-extraction against a regression that collapses the three
|
||||||
|
delegation calls onto one ``;``-joined line (the ``inline_helpers`` mutant).
|
||||||
|
Collapsing is behavior-preserving (output identical, helpers still defined
|
||||||
|
and invoked) so output-comparison and even AST call-detection cannot catch
|
||||||
|
it. We assert main's body keeps one statement per line (no ``;``), keeping
|
||||||
|
each call boundary genuine.
|
||||||
|
"""
|
||||||
|
tree = ast.parse(_read(REFACTORED))
|
||||||
|
main_func = next(n for n in tree.body
|
||||||
|
if isinstance(n, ast.FunctionDef) and n.name == "main")
|
||||||
|
src_lines = _read(REFACTORED).splitlines()
|
||||||
|
main_lines = src_lines[main_func.lineno - 1: main_func.end_lineno
|
||||||
|
or main_func.lineno]
|
||||||
|
for ln in main_lines:
|
||||||
|
code = ln.split("#", 1)[0] # drop inline comment before testing
|
||||||
|
if ";" in code:
|
||||||
|
print(f"FAIL: main() body compacts statements with ';' "
|
||||||
|
f"({ln.strip()!r}); delegation calls must stay distinct")
|
||||||
|
return False
|
||||||
|
print("OK: main() delegates via distinct standalone statements (no ';'-compaction)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_main_path_parameter() -> bool:
|
||||||
|
"""``main`` must accept a ``path`` parameter (configurable, not hardcoded)."""
|
||||||
|
tree = ast.parse(_read(REFACTORED))
|
||||||
|
main_func = next(n for n in tree.body
|
||||||
|
if isinstance(n, ast.FunctionDef) and n.name == "main")
|
||||||
|
params = {a.arg for a in main_func.args.args}
|
||||||
|
if "path" not in params:
|
||||||
|
print(f"FAIL: main() does not accept a 'path' parameter (params={params})")
|
||||||
|
return False
|
||||||
|
print("OK: main(path) accepts a configurable path parameter")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_dead_sys_import_removed() -> bool:
|
||||||
|
"""The original imports ``sys`` but never uses it; refactored must not import it."""
|
||||||
|
rtree = ast.parse(_read(REFACTORED))
|
||||||
|
r_names = _imported_names(rtree)
|
||||||
|
if "sys" in r_names:
|
||||||
|
print(f"FAIL: refactored still imports sys (imports={sorted(r_names)})")
|
||||||
|
return False
|
||||||
|
otree = ast.parse(_read(ORIGINAL))
|
||||||
|
o_names = _imported_names(otree)
|
||||||
|
if "sys" not in o_names:
|
||||||
|
print("FAIL: original does not import sys (dead-import smell absent)")
|
||||||
|
return False
|
||||||
|
uses = [n for n in ast.walk(otree)
|
||||||
|
if (isinstance(n, ast.Name) and n.id == "sys")
|
||||||
|
or (isinstance(n, ast.Attribute)
|
||||||
|
and isinstance(n.value, ast.Name)
|
||||||
|
and n.value.id == "sys")]
|
||||||
|
if uses:
|
||||||
|
print(f"FAIL: original references sys ({len(uses)} uses) -- not dead")
|
||||||
|
return False
|
||||||
|
print("OK: dead `sys` import removed (original imported sys, never used it)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_original_still_smells() -> bool:
|
||||||
|
"""The original must still carry the smells we removed (non-vacuous)."""
|
||||||
|
src = _read(ORIGINAL)
|
||||||
|
code = _code_only(src)
|
||||||
|
problems = []
|
||||||
|
if '__name__ == "__main__"' in src:
|
||||||
|
problems.append("original already has __main__ guard (refactor not needed)")
|
||||||
|
if REAL_SEED_PATH not in code:
|
||||||
|
problems.append("hardcoded path missing from original (expected)")
|
||||||
|
tree = ast.parse(src)
|
||||||
|
has_module_level_for = any(isinstance(n, ast.For) for n in tree.body)
|
||||||
|
if not has_module_level_for:
|
||||||
|
problems.append("no module-level for loop in original (expected)")
|
||||||
|
func_defs = [n for n in tree.body if isinstance(n, ast.FunctionDef)]
|
||||||
|
if func_defs:
|
||||||
|
problems.append(f"original has function definitions: "
|
||||||
|
f"{[f.name for f in func_defs]}")
|
||||||
|
if "sys" not in _imported_names(tree):
|
||||||
|
problems.append("original no longer imports sys (dead import vanished)")
|
||||||
|
if problems:
|
||||||
|
print("FAIL: original does not exhibit the expected smells:")
|
||||||
|
for p in problems:
|
||||||
|
print(f" - {p}")
|
||||||
|
return False
|
||||||
|
print("OK: original still exhibits the removed smells (refactor is non-vacuous)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_path_override_honoured() -> bool:
|
||||||
|
"""Calling ``main(path=...)`` with a custom file must print that file's data.
|
||||||
|
|
||||||
|
A mutation that accepts a ``path`` parameter but ignores it (always reads
|
||||||
|
DEFAULT_PATH) would pass check 2 (byte-identical on the real seed) but fail
|
||||||
|
here: the custom file's contents appear in the output.
|
||||||
|
"""
|
||||||
|
tmp_dir = tempfile.mkdtemp(prefix="triage_probe_override_")
|
||||||
|
custom = os.path.join(tmp_dir, "custom.json")
|
||||||
|
data = {
|
||||||
|
"source": "override test",
|
||||||
|
"exported_at": "2099-01-01T00:00:00Z",
|
||||||
|
"cutover": "x",
|
||||||
|
"decided": [{"id": 99, "summary": "overridden", "decision": "x",
|
||||||
|
"escalated_by": "tester"}],
|
||||||
|
"pending": [],
|
||||||
|
}
|
||||||
|
with open(custom, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
try:
|
||||||
|
mod, exc, _imp_out, _imp_err = _safe_import()
|
||||||
|
if exc is not None:
|
||||||
|
print(f"FAIL: import raised {type(exc).__name__}: {exc!r} "
|
||||||
|
f"(stdout={_imp_out!r} stderr={_imp_err!r})")
|
||||||
|
return False
|
||||||
|
assert mod is not None # _safe_import: exc set iff module is None
|
||||||
|
out, err = io.StringIO(), io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
||||||
|
rc = mod.main(path=custom)
|
||||||
|
combined = out.getvalue() + err.getvalue()
|
||||||
|
if "overridden" not in combined:
|
||||||
|
print(f"FAIL: main(path=custom) did not read the custom file "
|
||||||
|
f"(rc={rc}, output={combined!r})")
|
||||||
|
return False
|
||||||
|
if REAL_SEED_PATH in combined:
|
||||||
|
print("FAIL: main(path=custom) printed data from DEFAULT_PATH, not "
|
||||||
|
"the custom path")
|
||||||
|
return False
|
||||||
|
print("OK: main(path=custom) honours the override "
|
||||||
|
"(prints custom file, not DEFAULT_PATH)")
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
checks = [
|
||||||
|
check_py_compile,
|
||||||
|
check_identical_output,
|
||||||
|
check_main_guard,
|
||||||
|
check_no_globals,
|
||||||
|
check_import_side_effect_free,
|
||||||
|
check_helpers_extracted,
|
||||||
|
check_main_delegation_intact,
|
||||||
|
check_main_path_parameter,
|
||||||
|
check_dead_sys_import_removed,
|
||||||
|
check_original_still_smells,
|
||||||
|
check_path_override_honoured,
|
||||||
|
]
|
||||||
|
results = [fn() for fn in checks]
|
||||||
|
passed = sum(results)
|
||||||
|
total = len(results)
|
||||||
|
print(f"\n=== Verification: {passed}/{total} checks passed ===")
|
||||||
|
if passed != total:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user