forge: triage_dump refactor Q86 closure (14/14, byte-identical)

This commit is contained in:
forge committed 2026-10-27 03:49:48 +00:00
1 parent e24d442e18
commit c304ca6020
4 files changed
+295 -42

No files matched your search

@@ -2,7 +2,7 @@
**Lane:** Refactor — code review and refactors **Lane:** Refactor — code review and refactors
**Artifact committed to:** `wizard-work/forge/` (`_triage_dump.py`, `_triage_dump_refactored.py`, `verify_triage_dump_refactor.py`, `_triage_dump_refactor_CHECK_EVIDENCE.txt`) **Artifact committed to:** `wizard-work/forge/` (`_triage_dump.py`, `_triage_dump_refactored.py`, `verify_triage_dump_refactor.py`, `_triage_dump_refactor_CHECK_EVIDENCE.txt`)
**Verdict:** 11/11 checks pass; probe output byte-identical to the original (6695 bytes, sha256 `1bd715fb…`). **Verdict:** 14/14 checks pass; probe output byte-identical to the original (6695 bytes, sha256 `1bd715fb…`).
## Target ## Target
@@ -12,8 +12,8 @@ triage inbox seed `inbox/seed-2026-09-26.json`. The original ran its **entire**
probe at module top level: it opened the JSON, loaded it into the module-global 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__"` `d`, and printed everything at import time, with no `if __name__ == "__main__"`
guard, **zero function definitions**, two module-level `for` loops, a hardcoded guard, **zero function definitions**, two module-level `for` loops, a hardcoded
JSON path at module scope, and the module-global `d` coupling data flow to the host-absolute JSON path at module scope, and the module-global `d` coupling data
import side effect. flow to the import side effect.
## Smells fixed (behavior-preserving) ## Smells fixed (behavior-preserving)
@@ -22,12 +22,16 @@ import side effect.
explicitly guarded with `if __name__ == "__main__": main()`. explicitly guarded with `if __name__ == "__main__": main()`.
- *Measured:* `check_import_side_effect_free` — importing produces no stdout/ - *Measured:* `check_import_side_effect_free` — importing produces no stdout/
stderr and no `SystemExit`. stderr and no `SystemExit`.
2. **Hardcoded path at module scope.** The bare `path = "/root/.../seed...json"` 2. **Hardcoded host-absolute path at module scope (Q86).** The bare
assignment is promoted to the named constant `DEFAULT_PATH` and made the `path = "/root/troupe-standup/inbox/seed-...json"` literal is replaced by a
`path` parameter of `main()`. Callers/tests can override it without editing tree-relative resolution (`_HERE` -> repo root -> `inbox/seed-...json`)
source; the default is unchanged, so output is byte-identical when run with no promoted to the named constant `DEFAULT_PATH` and the `path` parameter of
args. `main()`. On this host the resolution is identical to the old literal
- *Measured:* `check_main_path_parameter` — `main` AST has a `path` arg. (output byte-identical), but a clone now grades *its own* inbox instead of
silently grading `/root/...` on the host.
- *Measured:* `check_main_path_parameter`; `check_seed_path_portable` — no
host-absolute literal in the refactored source and `DEFAULT_PATH` resolves
inside the tree.
3. **No function extraction / module-global coupling.** The two inline print 3. **No function extraction / module-global coupling.** The two inline print
loops (decided, pending) are extracted into the pure helpers `_print_decided` loops (decided, pending) are extracted into the pure helpers `_print_decided`
and `_print_pending` (plus `load_data`); `d.get("decided", [])` and and `_print_pending` (plus `load_data`); `d.get("decided", [])` and
@@ -40,7 +44,7 @@ import side effect.
## Verification harness ## Verification harness
`verify_triage_dump_refactor.py` — 11 checks, each a real subprocess/AST `verify_triage_dump_refactor.py` — 14 checks, each a real subprocess/AST
assertion (no tautology): assertion (no tautology):
| # | check | what it proves | | # | check | what it proves |
@@ -54,8 +58,11 @@ assertion (no tautology):
| 7 | main delegation | `main()` body has no `;`-compacted statements (extraction boundary intact) | | 7 | main delegation | `main()` body has no `;`-compacted statements (extraction boundary intact) |
| 8 | `main(path)` param | `main` accepts a configurable `path` parameter | | 8 | `main(path)` param | `main` accepts a configurable `path` parameter |
| 9 | no dead imports | `json` retained and referenced; no unreferenced imports introduced | | 9 | no dead imports | `json` retained and referenced; no unreferenced imports introduced |
| 10 | original still smells | non-vacuous: original still lacks guard, still hardcodes the path, still has module-level `for`, still has zero function defs | | 10 | original still smells | non-vacuous: original still lacks guard, still hardcodes the host-absolute path, still has module-level `for`, still has zero function defs |
| 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 | | 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 |
| 12 | no import-time I/O (M6 killer) | M6 (`_bootstrap = json.load(open(DEFAULT_PATH))` at module scope, no print) survives check 5 by being *silent*, not clean. Static AST walk of top-level statements + dynamic `builtins.open` spy both require zero calls. |
| 13 | M6 mutant detected (non-vacuous) | The check-12 detector fires on the measured M6 mutation applied to a temp copy, so it is sensitive rather than theater. |
| 14 | seed path portable (Q86) | No host-absolute seed literal in the refactored source; `DEFAULT_PATH` resolves inside the tree and matches the harness's `REAL_SEED_PATH`. Closes the "a clone grades the host's inbox" gap. |
## Evidence (measured, this run) ## Evidence (measured, this run)
@@ -72,8 +79,11 @@ OK: main(path) accepts a configurable path parameter
OK: all refactored imports are referenced (json retained, no dead imports) OK: all refactored imports are referenced (json retained, no dead imports)
OK: original still exhibits the removed smells (refactor is non-vacuous) OK: original still exhibits the removed smells (refactor is non-vacuous)
OK: main(path=custom) honours the override (prints custom file, not DEFAULT_PATH) OK: main(path=custom) honours the override (prints custom file, not DEFAULT_PATH)
OK: no import-time I/O (0 module-scope io calls; 0 runtime open calls)
OK: M6 mutant detected at module scope: [(51, 'json.load'), (51, 'open')]
OK: seed path is tree-portable (resolves inside tree): /root/troupe-standup/inbox/seed-2026-09-26.json
=== Verification: 11/11 checks passed === (exit 0) === Verification: 14/14 checks passed ===
``` ```
Re-run under the optimised interpreter (per the lane's "python3 and -O" evidence Re-run under the optimised interpreter (per the lane's "python3 and -O" evidence
@@ -81,9 +91,7 @@ convention):
``` ```
$ python3 -O verify_triage_dump_refactor.py $ python3 -O verify_triage_dump_refactor.py
OK: main(path=custom) honours the override (prints custom file, not DEFAULT_PATH) === Verification: 14/14 checks passed ===
=== Verification: 11/11 checks passed === (exit 0)
``` ```
Full capture in `wizard-work/forge/_triage_dump_refactor_CHECK_EVIDENCE.txt`. Full capture in `wizard-work/forge/_triage_dump_refactor_CHECK_EVIDENCE.txt`.
@@ -92,9 +100,9 @@ Full capture in `wizard-work/forge/_triage_dump_refactor_CHECK_EVIDENCE.txt`.
Check 10 guarantees the refactor actually removed something: the *original* Check 10 guarantees the refactor actually removed something: the *original*
`_triage_dump.py` still exhibits every smell the refactored version no longer `_triage_dump.py` still exhibits every smell the refactored version no longer
does (no `__main__` guard, hardcoded path at module scope, two module-level does (no `__main__` guard, hardcoded host-absolute path at module scope, two
`for` loops, zero function definitions, module-global `d`). A no-op "refactor" module-level `for` loops, zero function definitions, module-global `d`). A no-op
would fail this. "refactor" would fail this.
``` ```
$ python3 - <<'PY' $ python3 - <<'PY'
@@ -111,6 +119,12 @@ main-guard count -> 0
module-level for -> 2 module-level for -> 2
``` ```
Checks 12+13 are a paired non-vacuity pair for the import-time-IO detector:
the clean module has zero module-scope io calls (check 12), and the M6 mutant
(a silent module-scope `json.load(open(DEFAULT_PATH))`) is detected by the same
detector (check 13). A detector that matches nothing would be theater; M6 proves
check 12 is surgical.
## Mutation kill ## Mutation kill
Check 11 (path override) is the key discriminator the byte-comparison cannot Check 11 (path override) is the key discriminator the byte-comparison cannot
@@ -119,15 +133,18 @@ see: a `path` parameter that is accepted but ignored (always reads
wrong data on a custom file. The check feeds a synthetic JSON containing wrong data on a custom file. The check feeds a synthetic JSON containing
`"overridden"` and asserts that token appears and the real seed path does not. `"overridden"` and asserts that token appears and the real seed path does not.
Check 10 (original still smells) is the structural discriminator: re-adding any Checks 3/8/6/12 form the structural discriminators: re-adding any of the removed
of the removed smells to the refactored file is caught by checks 3/8/6, and a smells to the refactored file is caught, and a no-op refactor fails check 10
no-op refactor fails check 10 outright. outright.
## Independent byte-identical proof (outside the harness) ## Independent byte-identical proof (outside the harness)
`_triage_dump.py` (the in-tree original; a byte-identical copy of the repo-root
throwaway `.triage_dump.py`) vs. `_triage_dump_refactored.py`:
``` ```
$ python3 .triage_dump.py 1>/tmp/oout 2>/tmp/oerr $ python3 _triage_dump.py 1>/tmp/oout 2>/tmp/oerr
$ python3 wizard-work/forge/_triage_dump_refactored.py 1>/tmp/rout 2>/tmp/rerr $ python3 _triage_dump_refactored.py 1>/tmp/rout 2>/tmp/rerr
$ sha256sum /tmp/oout /tmp/rout $ sha256sum /tmp/oout /tmp/rout
1bd715fbd97a6fc25a4c43581b20c231981118408a61689b4a9cd281cc3cc25b /tmp/oout 1bd715fbd97a6fc25a4c43581b20c231981118408a61689b4a9cd281cc3cc25b /tmp/oout
1bd715fbd97a6fc25a4c43581b20c231981118408a61689b4a9cd281cc3cc25b /tmp/rout 1bd715fbd97a6fc25a4c43581b20c231981118408a61689b4a9cd281cc3cc25b /tmp/rout
@@ -137,6 +154,8 @@ $ cmp /tmp/oerr /tmp/rerr && echo STDERR_IDENTICAL
STDERR_IDENTICAL (cmp exit 0) STDERR_IDENTICAL (cmp exit 0)
``` ```
VERDICT: PASS -- triage_dump probe refactor is behavior-preserving; byte-identical output (6695 bytes, sha256 `1bd715fb…`), 11/11 on python3 and -O, exit 0 VERDICT: PASS -- triage_dump probe refactor is behavior-preserving; byte-identical
output (6695 bytes, sha256 `1bd715fb…`), 14/14 on python3 and -O, exit 0; Q86
gaps closed (import-time I/O detected, seed path tree-portable).
CHECK: python3 /root/troupe-standup/wizard-work/forge/verify_triage_dump_refactor.py >/dev/null 2>&1 CHECK: python3 /root/troupe-standup/wizard-work/forge/verify_triage_dump_refactor.py >/dev/null 2>&1
@@ -1,3 +1,4 @@
$ python3 verify_triage_dump_refactor.py
OK: py_compile passes OK: py_compile passes
OK: identical output (exit 0, 6695 bytes) OK: identical output (exit 0, 6695 bytes)
OK: __main__ guard present OK: __main__ guard present
@@ -9,5 +10,28 @@ OK: main(path) accepts a configurable path parameter
OK: all refactored imports are referenced (json retained, no dead imports) OK: all refactored imports are referenced (json retained, no dead imports)
OK: original still exhibits the removed smells (refactor is non-vacuous) OK: original still exhibits the removed smells (refactor is non-vacuous)
OK: main(path=custom) honours the override (prints custom file, not DEFAULT_PATH) OK: main(path=custom) honours the override (prints custom file, not DEFAULT_PATH)
OK: no import-time I/O (0 module-scope io calls; 0 runtime open calls)
OK: M6 mutant detected at module scope: [(51, 'json.load'), (51, 'open')]
OK: seed path is tree-portable (resolves inside tree): /root/troupe-standup/inbox/seed-2026-09-26.json
=== Verification: 11/11 checks passed === === Verification: 14/14 checks passed ===
$ python3 -O verify_triage_dump_refactor.py
OK: py_compile passes
OK: identical output (exit 0, 6695 bytes)
OK: __main__ guard present
OK: no 'global' statements in code
OK: import is side-effect-free (no probe output at import)
OK: _print_decided, _print_pending, load_data, main extracted; main calls both print helpers
OK: main() delegates via distinct standalone statements (no ';'-compaction)
OK: main(path) accepts a configurable path parameter
OK: all refactored imports are referenced (json retained, no dead imports)
OK: original still exhibits the removed smells (refactor is non-vacuous)
OK: main(path=custom) honours the override (prints custom file, not DEFAULT_PATH)
OK: no import-time I/O (0 module-scope io calls; 0 runtime open calls)
OK: M6 mutant detected at module scope: [(51, 'json.load'), (51, 'open')]
OK: seed path is tree-portable (resolves inside tree): /root/troupe-standup/inbox/seed-2026-09-26.json
=== Verification: 14/14 checks passed ===
exit codes: plain=0 optimised=0
+22 -11
View File
@@ -2,10 +2,11 @@
"""Triage dump probe (refactored, forge-lane Refactor). """Triage dump probe (refactored, forge-lane Refactor).
Refactored from ``_triage_dump.py`` (a byte-identical copy of the repo-root Refactored from ``_triage_dump.py`` (a byte-identical copy of the repo-root
``.triage_dump.py``). The original ran its entire probe at **module top throwaway probe ``.triage_dump.py``). The original ran its entire probe at
level** -- opening the JSON, loading it into the module-global ``d``, and **module top level** -- opening the JSON, loading it into the module-global
printing all output at import time, with no ``if __name__ == "__main__"`` guard. ``d``, and printing all output at import time, with no
Importing it executed the probe and wrote to stdout. ``if __name__ == "__main__"`` guard. Importing it executed the probe and
wrote to stdout.
Smells fixed in this refactor (behavior-preserving on the probe itself): Smells fixed in this refactor (behavior-preserving on the probe itself):
@@ -14,17 +15,20 @@ Smells fixed in this refactor (behavior-preserving on the probe itself):
entry point is explicitly guarded. ``import _triage_dump_refactored`` entry point is explicitly guarded. ``import _triage_dump_refactored``
produces no output and does not terminate the process. produces no output and does not terminate the process.
* **Hardcoded path at module scope.** The JSON path was a bare module-level * **Hardcoded host-absolute path at module scope.** The JSON path was a
assignment with no override. It is promoted to the named constant bare ``/root/.../inbox/seed-...json`` literal with no override, so a clone
``DEFAULT_PATH`` and made the ``path`` parameter of ``main()``, so callers graded *this host's* inbox. It is now resolved from this file's location
(including tests) can override it without editing source. The default is (walked up to the repo root + ``inbox/seed-...json``) and promoted to the
unchanged, so output is byte-identical when run with no args. named constant ``DEFAULT_PATH`` / the ``path`` parameter of ``main()``, so
callers (including tests) can override it. On this host the resolution is
identical to the old literal, so output stays byte-identical; on a clone it
resolves into the clone's tree. See Q86.
* **No function extraction / module-global coupling.** The two inline print * **No function extraction / module-global coupling.** The two inline print
loops (decided, pending) are extracted into the pure formatting helpers loops (decided, pending) are extracted into the pure formatting helpers
``_print_decided`` and ``_print_pending`` that take their data as arguments ``_print_decided`` and ``_print_pending`` that take their data as arguments
rather than reading the module-global ``d``. The module-global ``d`` is rather than reading the module-global ``d``. The module-global ``d`` is
removed: data flows from ``main`` into the helpers as parameters. gone: data flows from ``main`` into the helpers as parameters.
Output is byte-identical to the original on the current inbox seed Output is byte-identical to the original on the current inbox seed
(``inbox/seed-2026-09-26.json``): 6695 bytes, sha256 (``inbox/seed-2026-09-26.json``): 6695 bytes, sha256
@@ -33,8 +37,15 @@ Output is byte-identical to the original on the current inbox seed
from __future__ import annotations from __future__ import annotations
import json import json
import os
DEFAULT_PATH = "/root/troupe-standup/inbox/seed-2026-09-26.json" # Resolve the seed from THIS file's location so the probe is self-contained:
# it grades the tree it lives in, not this host's inbox (Q86 portability gap).
# wizard-work/forge/ is two levels under the repo root, which holds inbox/.
_HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_PATH = os.path.join(
os.path.dirname(os.path.dirname(_HERE)), "inbox", "seed-2026-09-26.json"
)
def load_data(path): def load_data(path):
@@ -36,8 +36,20 @@ Checks:
for-loops, zero function definitions, runs at import). for-loops, zero function definitions, runs at import).
11. ``main(path)`` honours the override: calling with a custom temp JSON path 11. ``main(path)`` honours the override: calling with a custom temp JSON path
prints the custom file's data and not the real seed path. This kills the prints the custom file's data and not the real seed path. This kills the
mutation of a ``path`` parameter that is accepted but ignored (byte-identical mutation of a ``path`` parameter that is accepted but ignored (byte-
on the real seed would not catch this). identical on the real seed would not catch this).
12. No import-time I/O (M6 killer): a static AST walk of the top-level
statements plus a runtime ``builtins.open`` spy both require zero calls.
Mutant M6 (``_bootstrap = json.load(open(DEFAULT_PATH))`` at module scope,
no print) survived check 5 (rc 0, zero output) by being *silent*, not
clean -- the read happened but was invisible to the silence test.
13. M6 is detected by check 12's io-detector (non-vacuity): the detector fires
on the measured mutant applied to a temp copy, so it is sensitive rather
than theater.
14. The seed path is tree-portable: no host-absolute literal in the refactored
source, and ``DEFAULT_PATH`` resolves inside the tree. Closes the Q86
"a clone grades the host's inbox" gap -- on this host the resolution is
identical to the old literal, so check 2 stays byte-identical.
""" """
from __future__ import annotations from __future__ import annotations
@@ -58,7 +70,17 @@ WORK_DIR = os.path.dirname(os.path.abspath(__file__))
ORIGINAL = "_triage_dump.py" ORIGINAL = "_triage_dump.py"
REFACTORED = "_triage_dump_refactored.py" REFACTORED = "_triage_dump_refactored.py"
MOD_NAME = REFACTORED[:-3] # strip .py -> _triage_dump_refactored MOD_NAME = REFACTORED[:-3] # strip .py -> _triage_dump_refactored
REAL_SEED_PATH = "/root/troupe-standup/inbox/seed-2026-09-26.json" SEED_FILENAME = "seed-2026-09-26.json"
# The original probe's literal -- used only to assert the ORIGINAL still carries
# the host-absolute smell (clone-independent, unlike comparing against a resolved
# real-seed path).
HOST_SEED_LITERAL = "/root/troupe-standup/inbox/seed-2026-09-26.json"
# The seed is derived from THIS file's location (walked up to the repo root),
# not a host-absolute literal, so a clone grades its own inbox (Q86). On this
# host the resolution is identical to the old literal -> check 2 stays green.
_VERIFY_DIR = os.path.dirname(os.path.abspath(__file__))
REAL_SEED_PATH = os.path.join(
os.path.dirname(os.path.dirname(_VERIFY_DIR)), "inbox", SEED_FILENAME)
def _run(*args: str) -> subprocess.CompletedProcess: def _run(*args: str) -> subprocess.CompletedProcess:
@@ -295,15 +317,35 @@ def check_no_dead_imports() -> bool:
return True return True
def _original_hardcodes_absolute_seed(src: str) -> bool:
"""True if the original assigns an absolute path literal to ``path`` at
module scope -- the host-absolute smell the refactor removes.
AST-based so the test is clone-independent: the original's literal is
host-absolute by design (``/root/.../inbox/seed-....json``), which would
break a naive ``REAL_SEED_PATH in code`` test on any checkout whose
REAL_SEED_PATH is tree-relative (i.e. everywhere, after this fix)."""
tree = ast.parse(src)
for stmt in tree.body:
if (isinstance(stmt, ast.Assign)
and isinstance(stmt.value, ast.Constant)
and isinstance(stmt.value.value, str)
and stmt.value.value.startswith("/")):
for t in stmt.targets:
if isinstance(t, ast.Name) and t.id == "path":
return True
return False
def check_original_still_smells() -> bool: def check_original_still_smells() -> bool:
"""The original must still carry the smells we removed (non-vacuous).""" """The original must still carry the smells we removed (non-vacuous)."""
src = _read(ORIGINAL) src = _read(ORIGINAL)
code = _code_only(src)
problems = [] problems = []
if _guard_present(src): if _guard_present(src):
problems.append("original already has __main__ guard (refactor not needed)") problems.append("original already has __main__ guard (refactor not needed)")
if REAL_SEED_PATH not in code: if not _original_hardcodes_absolute_seed(src):
problems.append("hardcoded path missing from original (expected)") problems.append("module-scope absolute `path = <literal>` missing from "
"original (expected)")
tree = ast.parse(src) tree = ast.parse(src)
has_module_level_for = any(isinstance(n, ast.For) for n in tree.body) has_module_level_for = any(isinstance(n, ast.For) for n in tree.body)
if not has_module_level_for: if not has_module_level_for:
@@ -366,6 +408,160 @@ def check_path_override_honoured() -> bool:
shutil.rmtree(tmp_dir, ignore_errors=True) shutil.rmtree(tmp_dir, ignore_errors=True)
# ----------------------------------------------------------------------
# Q86: the refactor-lane harness proved SILENCE at import, not ABSENCE of I/O,
# and its seed path was host-absolute. The two fixes below close both gaps.
#
# A Call to any of these at MODULE SCOPE runs while the module is imported --
# the M6 mutant (`_bootstrap = json.load(open(DEFAULT_PATH))`, no print) hides
# from check 5 because it emits no stdout, so it needs its own detector.
_IO_CALLS = frozenset({
"open", "json.load", "json.loads", "pickle.load", "pickle.loads",
"csv.reader", "csv.DictReader", "urllib.request.urlopen", "codecs.open",
})
def _call_name(call: ast.Call):
"""Dotted name of a Call's function (``json.load``, ``open``, ...), or
None for a callable expression this detector does not model."""
parts = []
cur = call.func
while isinstance(cur, ast.Attribute):
parts.append(cur.attr)
cur = cur.value
if isinstance(cur, ast.Name):
parts.append(cur.id)
return ".".join(reversed(parts))
return None
def _calls_at_module_scope(src: str) -> list:
"""``[(lineno, name)]`` for io-reading Call nodes at module scope.
A Call counts as "module scope" only when it is NOT nested inside a
FunctionDef / AsyncFunctionDef / ClassDef body -- those run lazily, at call
time, and are exactly the I/O the refactor is allowed to keep (it lives
under ``main()``). The ``if __name__ == "__main__"`` guard's ``main()``
call resolves to ``main`` (not in the io set), so it is correctly ignored.
"""
tree = ast.parse(src)
found: list = []
def scan(node):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
return # do not descend -- calls inside a def body are lazy
if isinstance(node, ast.Call):
name = _call_name(node)
if name in _IO_CALLS:
found.append((node.lineno, name))
for child in ast.iter_child_nodes(node):
scan(child)
for stmt in tree.body:
scan(stmt)
return found
def check_no_import_time_io() -> bool:
"""Check 5 proves the import is SILENT, not that no I/O happens. M6
(`_bootstrap = json.load(open(DEFAULT_PATH))` at module scope, no print)
survives check 5 (rc 0, zero output) while reading the seed at import.
Two independent detectors, both must read zero on the clean module:
(a) static: AST-walk the top-level statements for an open/json.load call;
(b) dynamic: patch builtins.open to a recorder, re-import the module, and
require zero calls. The import machinery reads .py source via
_io.FileIO (not builtins.open), so this does NOT false-positive on a
clean module -- measured separately (0 calls on a fresh import)."""
src = _read(REFACTORED)
static = _calls_at_module_scope(src)
if static:
print("FAIL: static io-call(s) at module scope: %s" % static)
return False
# Dynamic backstop: require zero open() calls during a fresh import.
import builtins
calls: list = []
real_open = builtins.open
def _spy_open(*args, **kwargs):
calls.append(args[0] if args else kwargs.get("file"))
raise RuntimeError("__check_no_import_time_io__ import-time open()")
builtins.open = _spy_open
try:
_mod, exc, _out, _err = _safe_import()
finally:
builtins.open = real_open
if exc is not None:
# A RuntimeError carrying our marker is the very failure we reject;
# any other exception is a real import error and is ungradable here.
if isinstance(exc, RuntimeError) and "__check_no_import_time_io__" in str(exc):
print("FAIL: runtime import-time open() on %r (rc 2)" % calls)
return False
print("FAIL: import raised %s: %r (stdout=%r stderr=%r)"
% (type(exc).__name__, exc, _out, _err))
return False
if calls:
print("FAIL: import made %d open() call(s): %r" % (len(calls), calls))
return False
print("OK: no import-time I/O (0 module-scope io calls; 0 runtime open calls)")
return True
def check_m6_mutant_killed() -> bool:
"""Non-vacuity for check 12: a detector that matches nothing is theater.
Apply the measured M6 mutation -- a silent module-scope
``json.load(open(DEFAULT_PATH))`` inserted before the first def -- to a temp
copy of the source and confirm the static io-detector fires on it. The
clean module has zero such calls (check 12); the mutant has two, so the
detector is sensitive rather than vacuous. (M6 is the mutant measured for
Q86: it returns 11/11 under the old check 5.)"""
src = _read(REFACTORED)
m6_expr = "_bootstrap = json.load(open(DEFAULT_PATH))\n"
mutated = src.replace("def load_data(path):",
m6_expr + "def load_data(path):", 1)
if m6_expr not in mutated:
# guard against the insertion anchor drifting in the source
print("FAIL: M6 mutation could not be applied (anchor 'def load_data' not found)")
return False
sites = _calls_at_module_scope(mutated)
if not sites:
print("FAIL: M6 mutant NOT detected by the io-detector (check 12 is vacuous?)")
return False
print("OK: M6 mutant detected at module scope: %s" % sites)
return True
def check_seed_path_portable() -> bool:
"""Q86 gap #2: the seed path must derive from the tree, not a host-absolute
literal, so a clone grades its own inbox. Measured against two claims:
(a) the refactored source holds no host-absolute seed literal;
(b) importing the module, its DEFAULT_PATH resolves to a path INSIDE this
file's tree, and that resolution matches the harness's REAL_SEED_PATH.
A clone with its inbox moved aside now grades the clone (and fails on a
missing seed) instead of silently grading /root/... on the host."""
src = _read(REFACTORED)
if HOST_SEED_LITERAL in src:
print("FAIL: refactored still hardcodes a host-absolute seed path")
return False
mod, exc, out, err = _safe_import()
if exc is not None or mod is None:
print("FAIL: import raised %r (stdout=%r stderr=%r)" % (exc, out, err))
return False
resolved = os.path.realpath(mod.DEFAULT_PATH)
repo_root = os.path.realpath(os.path.dirname(os.path.dirname(WORK_DIR)))
if not (resolved == repo_root
or resolved.startswith(repo_root + os.sep)):
print("FAIL: DEFAULT_PATH %r is outside the tree root %r"
% (resolved, repo_root))
return False
if os.path.realpath(REAL_SEED_PATH) != resolved:
print("FAIL: harness REAL_SEED_PATH (%r) != module DEFAULT_PATH (%r)"
% (os.path.realpath(REAL_SEED_PATH), resolved))
return False
print("OK: seed path is tree-portable (resolves inside tree): %s" % resolved)
return True
def main() -> None: def main() -> None:
checks = [ checks = [
check_py_compile, check_py_compile,
@@ -379,6 +575,9 @@ def main() -> None:
check_no_dead_imports, check_no_dead_imports,
check_original_still_smells, check_original_still_smells,
check_path_override_honoured, check_path_override_honoured,
check_no_import_time_io,
check_m6_mutant_killed,
check_seed_path_portable,
] ]
results = [fn() for fn in checks] results = [fn() for fn in checks]
passed = sum(results) passed = sum(results)