From 4c0e8236258b37949dd9a278021f6bbcea826d1a Mon Sep 17 00:00:00 2001 From: forge Date: Fri, 23 Oct 2026 04:10:47 +0000 Subject: [PATCH] forge: citation_hash_class.py portable --repo default (fixes wrong-tree defect); + verifier --- tools/citation_hash_class.py | 11 +- ...ASH_CLASS_PORTABLE_DEFAULT_VERIFICATION.md | 67 ++++++ ...fy_citation_hash_class_portable_default.py | 220 ++++++++++++++++++ 3 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 wizard-work/forge/CITATION_HASH_CLASS_PORTABLE_DEFAULT_VERIFICATION.md create mode 100644 wizard-work/forge/verify_citation_hash_class_portable_default.py diff --git a/tools/citation_hash_class.py b/tools/citation_hash_class.py index 73d2f66..a57eb91 100644 --- a/tools/citation_hash_class.py +++ b/tools/citation_hash_class.py @@ -84,7 +84,14 @@ USAGE [--json OUT] [--only mismatch|classless|bound|flags|all] [--min-len N] [--context] [--siblings] [--selftest] # 19 fixtures """ -import argparse, collections, hashlib, json, re, subprocess, sys +import argparse, collections, hashlib, json, os, re, subprocess, sys + +# Portable repo root: derived from this file's location, not a host literal. +# Matches the convention all sibling tools (path_claim_check, citation_resolve_check, +# selftest_sweep, write_probe, tool_map, doc_census_check) already settled on; a +# bare invocation grades the checkout it is run out of, never /root/troupe-standup +# from wherever it happens to be copied. (forge lane: Refactor -- wrong-tree defect) +DEFAULT_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MIN_LEN_DEFAULT = 8 PREFIX_LENS = (7, 8, 10, 12, 16, 20, 40, 64) @@ -357,7 +364,7 @@ def selftest(repo): def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - ap.add_argument('--repo', default='/root/troupe-standup') + ap.add_argument('--repo', default=DEFAULT_REPO) ap.add_argument('--rev', default='HEAD') ap.add_argument('--corpus', nargs='*') ap.add_argument('--json') diff --git a/wizard-work/forge/CITATION_HASH_CLASS_PORTABLE_DEFAULT_VERIFICATION.md b/wizard-work/forge/CITATION_HASH_CLASS_PORTABLE_DEFAULT_VERIFICATION.md new file mode 100644 index 0000000..4b69df3 --- /dev/null +++ b/wizard-work/forge/CITATION_HASH_CLASS_PORTABLE_DEFAULT_VERIFICATION.md @@ -0,0 +1,67 @@ +# citation_hash_class.py — portable --repo default (forge lane: Refactor) + +## The smell + +`tools/citation_hash_class.py` defaulted `--repo` to the host literal +`/root/troupe-standup`. The tool's own USAGE docstring states that paths are +resolved against `--repo`, not the caller's cwd — but a bare invocation from +any clone, copy, or other host graded `/root/troupe-standup` from wherever it +happened to run. tool_map.py calls this the "wrong-tree defect": a tool that +grades the tree it lives in, not the tree it names. + +Every sibling tool (path_claim_check, citation_resolve_check, selftest_sweep, +write_probe, tool_map, doc_census_check) already derives its default repo root +from `__file__`. citation_hash_class.py was the last holdout. + +## The refactor (two edits, working tree) + +1. `import` gains `os`. +2. `DEFAULT_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))` + replaces the inline host literal in `ap.add_argument('--repo', default=DEFAULT_REPO)`. + +The portable expression resolves to the parent of `tools/` — i.e. the checkout +root — so in this checkout (living at `/root/troupe-standup`) a bare run still +grades `/root/troupe-standup`: a no-op HERE, and the bug is fixed everywhere else. + +## What is NOT touched + +The actual scanning/census logic (`scan`, `verdict_of`, `selftest`, `classify`) +is byte-for-byte unchanged. Only the argument default and the import are edited. + +## Verification + +Verifier: `verify_citation_hash_class_portable_default.py` + +ORIGINAL = `git show HEAD:tools/citation_hash_class.py` (committed; carries the +host-literal default). +REFACTORED = working tree (portable DEFAULT_REPO). + +``` +[ok ] selftest: original 19/19 rc 0 +[ok ] selftest: refactored 19/19 rc 0 +[ok ] selftest: identical stdout (byte-for-byte) +[ok ] bare live scan: original rc 0 +[ok ] bare live scan: refactored rc 0 +[ok ] bare live scan: identical findings +[ok ] bare live scan: identical stats +[ok ] explicit --repo scan: original rc 0 +[ok ] explicit --repo scan: refactored rc 0 +[ok ] explicit --repo scan: identical findings +[ok ] explicit --repo scan: identical stats +[ok ] refactored DEFAULT_REPO is portable (__file__- based) and free of host literal +[ok ] original DEFAULT_REPO is the host literal (no portable expr) +[ok ] portable DEFAULT_REPO resolves to /root/troupe-standup here +[ok ] live census non-vacuous (findings > 0, stats > 0) + +verify: 15 checks, 15 passed, 0 failed +``` + +Raw run log: `_chc_verify_run.log` (artifacts in this directory). +Exit code: 0. + +## Artifacts + +- Refined tool: `../../tools/citation_hash_class.py` (diff: 2 added lines / 1 changed line) +- Verifier: `verify_citation_hash_class_portable_default.py` +- Raw run log: `_chc_verify_run.log` +- This report: `CITATION_HASH_CLASS_PORTABLE_DEFAULT_VERIFICATION.md` diff --git a/wizard-work/forge/verify_citation_hash_class_portable_default.py b/wizard-work/forge/verify_citation_hash_class_portable_default.py new file mode 100644 index 0000000..b9341f0 --- /dev/null +++ b/wizard-work/forge/verify_citation_hash_class_portable_default.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +"""verify_citation_hash_class_portable_default.py + +Equivalence proof for the behavior-preserving refactor in +tools/citation_hash_class.py (forge lane: Refactor). + +THE SMELL + `--repo` defaulted to a host literal: + ap.add_argument('--repo', default='/root/troupe-standup') + A bare invocation from any other checkout / clone / host graded the path + /root/troupe-standup from HERE -- the wrong tree. tool_map.py names this the + "wrong-tree defect". All six sibling tools (path_claim_check, citation_resolve_check, + selftest_sweep, write_probe, tool_map, doc_census_check) already derive the + default from __file__; citation_hash_class.py was the last holdout. + +THE REFACTOR (two edits on the working tree, uncommitted at HEAD) + 1. import gains `os` + 2. DEFAULT_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ap.add_argument('--repo', default=DEFAULT_REPO) + The portable expression resolves to the parent of tools/ -- i.e. the checkout + root -- so in THIS checkout (living at /root/troupe-standup) a bare run still + grades /root/troupe-standup: a no-op HERE, and the bug is fixed everywhere else. + +ORIGINAL vs REFACTORED + ORIGINAL = git show HEAD:tools/citation_hash_class.py (committed; verdict_of applied, + but still carries the host-literal default) + REFACTORED = working tree (portable DEFAULT_REPO) + +WHAT THIS VERIFIER PROVES + The refactor is behavior-preserving: identical selftest contract, identical live + census (findings + stats) on BOTH the bare default path (the refactor's touchpoint) + and the explicit --repo path, plus a code-level portability check. + +LEGS + [1] selftest: original (git HEAD) passes 19/19, rc 0 + [2] selftest: refactored (working tree) passes 19/19, rc 0 + [3] selftest: stdout is byte-identical (byte-for-byte) + [4] BARE live scan --json (default --repo path): original & refactored both rc 0, + findings identical, stats identical + [5] EXPLICIT --repo live scan --json: findings identical, stats identical + [6] code-level: refactored DEFAULT_REPO portable (__file__-based) and free of the + host-literal default; original carries the host literal and no portable expr + [7] portable expression resolves to /root/troupe-standup HERE + [8] non-vacuity: the live census is non-empty (real findings) + +Exit 0 iff all checks pass, else 1. +""" +import json +import os +import shutil +import subprocess +import sys +import tempfile + +REPO = "/root/troupe-standup" +TOOL = "tools/citation_hash_class.py" +ORIG_SPEC = f"HEAD:{TOOL}" +PORTABLE_EXPR = "os.path.dirname(os.path.dirname(os.path.abspath(__file__)))" +HARD_CODED = "default='/root/troupe-standup'" +SELFTEST_SUMMARY = "19 passed, 0 failed" + + +class Verdict(Exception): + pass + + +def materialize_original(): + """Write the PRE-refactor module (git HEAD) to a temp path; return (dir, path). + HEAD is the commit the refactor descends from before it lands, so this reads the + genuine original.""" + out = tempfile.NamedTemporaryFile(suffix=".py", delete=False, mode="wb") + try: + subprocess.run(["git", "-C", REPO, "show", ORIG_SPEC], + check=True, stdout=out, timeout=30) + except subprocess.CalledProcessError as exc: + raise Verdict("could not read %s: %s" % (ORIG_SPEC, exc)) + finally: + out.close() + return os.path.dirname(out.name), out.name + + +def run(path, args, cwd=REPO, timeout=320): + proc = subprocess.run([sys.executable, path, *args], + capture_output=True, text=True, cwd=cwd, timeout=timeout) + return proc.returncode, proc.stdout, proc.stderr + + +def run_json(path, out_path, repo=None, timeout=320): + """Run citation_hash_class.py --json [, --repo ]; write JSON to out_path. + Returns (rc, stdout, stderr).""" + args = [] + if repo is not None: + args += ["--repo", repo] + args += ["--json", out_path] + proc = subprocess.run([sys.executable, path, *args], + capture_output=True, text=True, cwd=REPO, timeout=timeout) + return proc.returncode, proc.stdout, proc.stderr + + +def load_json(path): + with open(path, encoding="utf-8") as f: + return json.load(f) + + +def code_lines(text): + """Strip naive comments (#) and docstring-open lines so a smell that lives only + in prose is not counted as code -- same convention the other forge verifiers use.""" + out = [] + for ln in text.splitlines(): + s = ln.lstrip() + if s.startswith("#") or s.startswith('"""'): + continue + out.append(ln) + return "\n".join(out) + + +def main(): + cells, failed = [], 0 + + def check(label, ok, detail=""): + cells.append((label, ok)) + nonlocal failed + if not ok: + failed += 1 + print(" [%s] %s%s" + % ("ok " if ok else "FAIL", label, + (f" [{detail}]" if detail and not ok else ""))) + + orig_dir, orig_path = materialize_original() + ref_path = os.path.join(REPO, TOOL) + tmpdir = tempfile.mkdtemp(prefix="chc_verify_", + dir=os.environ.get("TMPDIR", "/tmp")) + try: + # ---- [1-3] selftest parity ----------------------------------------- + rc_o, out_o, _ = run(orig_path, ["--selftest"]) + rc_r, out_r, _ = run(ref_path, ["--selftest"]) + check("selftest: original 19/19 rc 0", + rc_o == 0 and SELFTEST_SUMMARY in out_o, f"rc={rc_o}") + check("selftest: refactored 19/19 rc 0", + rc_r == 0 and SELFTEST_SUMMARY in out_r, f"rc={rc_r}") + check("selftest: identical stdout (byte-for-byte)", + out_o == out_r, f"len_o={len(out_o)} len_r={len(out_r)}") + + # ---- [4] BARE live scan (default --repo path = the refactor touchpoint) --- + oj_path = os.path.join(tmpdir, "orig_bare.json") + rj_path = os.path.join(tmpdir, "ref_bare.json") + rc_o, _, _ = run_json(orig_path, oj_path) + rc_r, _, _ = run_json(ref_path, rj_path) + check("bare live scan: original rc 0", rc_o == 0, f"rc={rc_o}") + check("bare live scan: refactored rc 0", rc_r == 0, f"rc={rc_r}") + bare_ok = False + if os.path.exists(oj_path) and os.path.exists(rj_path): + oj = load_json(oj_path) + rj = load_json(rj_path) + bare_findings = oj["findings"] == rj["findings"] + bare_stats = oj["stats"] == rj["stats"] + check("bare live scan: identical findings", bare_findings, + f"orig={len(oj['findings'])} ref={len(rj['findings'])}") + check("bare live scan: identical stats", bare_stats, + f"orig={dict(oj['stats'])} ref={dict(rj['stats'])}") + bare_ok = bare_findings and bare_stats + + # ---- [5] EXPLICIT --repo live scan (proves scan() logic is untouched) --- + oj2 = os.path.join(tmpdir, "orig_explicit.json") + rj2 = os.path.join(tmpdir, "ref_explicit.json") + rc_o, _, _ = run_json(orig_path, oj2, REPO) + rc_r, _, _ = run_json(ref_path, rj2, REPO) + check("explicit --repo scan: original rc 0", rc_o == 0, f"rc={rc_o}") + check("explicit --repo scan: refactored rc 0", rc_r == 0, f"rc={rc_r}") + if os.path.exists(oj2) and os.path.exists(rj2): + oj = load_json(oj2) + rj = load_json(rj2) + check("explicit --repo scan: identical findings", + oj["findings"] == rj["findings"], + f"orig={len(oj['findings'])} ref={len(rj['findings'])}") + check("explicit --repo scan: identical stats", + oj["stats"] == rj["stats"]) + + # ---- [6] code-level: DEFAULT_REPO portability ---------------------- + ref_text = open(ref_path, encoding="utf-8").read() + orig_text = open(orig_path, encoding="utf-8").read() + ref_code = code_lines(ref_text) + orig_code = code_lines(orig_text) + check("refactored DEFAULT_REPO is portable (__file__-based) and free of host literal", + PORTABLE_EXPR in ref_code and HARD_CODED not in ref_code, + f"portable={PORTABLE_EXPR in ref_code} hardcoded={HARD_CODED in ref_code}") + check("original DEFAULT_REPO is the host literal (no portable expr)", + HARD_CODED in orig_code and PORTABLE_EXPR not in orig_code, + f"hardcoded={HARD_CODED in orig_code} portable={PORTABLE_EXPR in orig_code}") + + # ---- [7] portable expression resolves to /root/troupe-standup HERE --- + resolved = subprocess.run( + [sys.executable, "-c", + "import os;print(os.path.dirname(os.path.dirname(os.path.abspath(%r))))" % ref_path], + capture_output=True, text=True).stdout.strip() + check("portable DEFAULT_REPO resolves to /root/troupe-standup here", + resolved == REPO, f"got={resolved}") + + # ---- [8] non-vacuity ------------------------------------------------ + if os.path.exists(rj_path): + rj = load_json(rj_path) + nonvac = len(rj["findings"]) > 0 and sum(rj["stats"].values()) > 0 + check("live census non-vacuous (findings > 0, stats > 0)", nonvac, + f"findings={len(rj['findings'])} stats_sum={sum(rj['stats'].values())}") + finally: + shutil.rmtree(orig_dir, ignore_errors=True) + shutil.rmtree(tmpdir, ignore_errors=True) + + total = len(cells) + passed = total - failed + print(f"\nverify: {total} checks, {passed} passed, {failed} failed") + return 1 if failed else 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Verdict as e: + print(f"\nVERDICT: MISMATCH -- {e}") + sys.exit(1)