From 6fedf28664e9f829ca183160a0b08a1b11739df2 Mon Sep 17 00:00:00 2001 From: forge Date: Fri, 23 Oct 2026 07:15:41 +0000 Subject: [PATCH] forge: fix citation_hash_class verifier anchor-on-HEAD false-red; ship fast post-commit CHECK --- ..._CLASS_VERIFIER_ANCHOR_FIX_VERIFICATION.md | 86 +++++++++++++++++++ wizard-work/forge/_chc_fast_check.py | 84 ++++++++++++++++++ ...fy_citation_hash_class_portable_default.py | 45 +++++++--- 3 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 wizard-work/forge/CITATION_HASH_CLASS_VERIFIER_ANCHOR_FIX_VERIFICATION.md create mode 100644 wizard-work/forge/_chc_fast_check.py diff --git a/wizard-work/forge/CITATION_HASH_CLASS_VERIFIER_ANCHOR_FIX_VERIFICATION.md b/wizard-work/forge/CITATION_HASH_CLASS_VERIFIER_ANCHOR_FIX_VERIFICATION.md new file mode 100644 index 0000000..e8ac60f --- /dev/null +++ b/wizard-work/forge/CITATION_HASH_CLASS_VERIFIER_ANCHOR_FIX_VERIFICATION.md @@ -0,0 +1,86 @@ +# citation_hash_class.py portable refactor — verifier anchor fix (forge lane: Refactor) + +## The shipped artifact (pre-existing) +Commit `4c0e823` shipped the portable `--repo` default for `tools/citation_hash_class.py` +plus `verify_citation_hash_class_portable_default.py` and its report. The portable +refactor itself is correct and pushed (== origin/main `c9f5005`). + +## The defect found in review (the "anchor on HEAD" landmine) +`verify_citation_hash_class_portable_default.py` defined + +```python +ORIG_SPEC = f"HEAD:{TOOL}" +... +def materialize_original(): + """Write the PRE-refactor module (git HEAD) to a temp path ...""" + subprocess.run(["git", "-C", REPO, "show", ORIG_SPEC], ...) +``` + +`git show HEAD:tools/citation_hash_class.py` returns the text at HEAD. Once the +portable refactor lands in a commit, HEAD carries the **portable** expression — +so the verifier reads the *refactored* text as "original". Its own check #6, + + "original DEFAULT_REPO is the host literal (no portable expr)", + +then evaluates **False**, i.e. the verifier reports **FALSE-RED on its own work +after landing**. + +Proven here (scratch/probe, run on this host): + +``` +HEAD now = c9f5005eeba7fb330f78ae2e38740056ed12a3cf +orig (HEAD-anchored) == working tree? True +HEAD-anchored original has HOST LITERAL? False +HEAD-anchored original has PORTABLE EXPR? True +>>> verifier check #6 'original IS host literal' = False +>>> committed verifier reports FALSE-RED post-landing: True +History-walked original (2556dbb0) has HOST LITERAL? True +History-walked original has PORTABLE EXPR? False +>>> history-walk check #6 = True +``` + +The pre-refactor original is commit `2556dbb0` ("tools: the write path -- an +unwritable --json is rc 2"), the newest commit touching the tool whose `--repo` +default is still the host literal `default='/root/troupe-standup'`. + +## The refactor (working tree) +`materialize_original()` no longer anchors on HEAD. It walks the file's own +commit history (`git log --format=%H -- `) for the newest commit still +carrying the host literal and lacking the portable expression, and materializes +*that* as the genuine pre-refactor original. Same proven technique already in +`_chc_fast_check.py`. + +Legs preserved, behavior preserved: the live-scan legs ([4][5][8]) are unchanged +in intent; only the source of the "original" is corrected so they run against the +real pre-refactor text both before and after the refactor lands. The docstring +and LEGS notes updated to match. + +## Verification — fast post-commit CHECK (`_chc_fast_check.py`, 9/9, exit 0) +``` +PASS tool has portable DEFAULT_REPO +PASS tool free of host literal +PASS tool compiles (py_compile) +PASS original carries host literal (commit 2556dbb0) +PASS original lacks portable expr +PASS selftest 19/19 rc 0 +PASS verifier committed to HEAD +PASS committed verifier history-walks its original (no HEAD anchoring false-red) +PASS origin/main advanced +9/9 checks pass (FAST_CHECK_EXIT=0) +``` + +## Non-tautology proof +Against the HEAD-committed (pre-fix) verifier the new leg FAILS: + +``` +HEAD LACKS find_original_commit (new leg FAILS on HEAD -> non-tautological) +HEAD HAS ORIG_SPEC HEAD-anchoring (confirms the landmine) +``` + +So the CHECK is red on the buggy version and green on the fixed version. + +## Artifacts +- Refined verifier: `../../wizard-work/forge/verify_citation_hash_class_portable_default.py` + (history-walks ORIGINAL; `ORIG_SPEC`/`git show HEAD:` removed) +- Post-commit CHECK: `wizard-work/forge/_chc_fast_check.py` (9/9, <3s, no full-repo scan) +- Report: this file diff --git a/wizard-work/forge/_chc_fast_check.py b/wizard-work/forge/_chc_fast_check.py new file mode 100644 index 0000000..f482684 --- /dev/null +++ b/wizard-work/forge/_chc_fast_check.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Fast post-commit CHECK for the citation_hash_class.py portable-default refactor. + +Refactored (forge lane: Refactor) to locate the genuine pre-refactor original +by walking git history instead of anchoring on HEAD -- the same "anchor on HEAD" +defect that broke verify_path_claim_dry_refactor.py before it was fixed to use +find_original_commit(). Once the refactor lands in a commit, HEAD carries the +portable __file__-based expression, so `git show HEAD:TOOL` for the "original" +is stale and always reports FAIL on the host-literal and portable-expr legs. +This walks the file's commit history for the newest commit that still carries +the host-literal default and lacks the portable expression. +""" +import py_compile, subprocess, sys, os + +REPO = "/root/troupe-standup" +TOOL = "tools/citation_hash_class.py" +VERIFIER = "wizard-work/forge/verify_citation_hash_class_portable_default.py" + +PORTABLE_EXPR = "os.path.dirname(os.path.dirname(os.path.abspath(__file__)))" +HARD_CODED = "default='/root/troupe-standup'" + +tool_path = os.path.join(REPO, TOOL) +checks = [] + +def chk(label, ok): + checks.append(ok) + print(f" {'PASS' if ok else 'FAIL'} {label}") + + +def git_commits_touching(path): + """Newest-first list of commit shas that touched in REPO.""" + proc = subprocess.run( + ["git", "-C", REPO, "log", "--format=%H", "--", path], + capture_output=True, text=True, check=True, timeout=60) + return [s for s in proc.stdout.split() if s] + + +def find_original_commit(path=TOOL): + """Newest commit in 's history whose --repo default is still the + host literal (the genuine pre-refactor original). Skips commits carrying + the portable __file__-based expression. Returns the sha or None.""" + for sha in git_commits_touching(path): + txt = subprocess.run( + ["git", "-C", REPO, "show", "%s:%s" % (sha, path)], + capture_output=True, text=True, timeout=60).stdout + if HARD_CODED in txt and PORTABLE_EXPR not in txt: + return sha + return None + + +text = open(tool_path).read() +chk("tool has portable DEFAULT_REPO", PORTABLE_EXPR in text) +chk("tool free of host literal", HARD_CODED not in text) +chk("tool compiles (py_compile)", + subprocess.run([sys.executable, "-m", "py_compile", tool_path]).returncode == 0) + +orig_sha = find_original_commit() +if orig_sha: + head = subprocess.run(["git", "-C", REPO, "show", "%s:%s" % (orig_sha, TOOL)], + capture_output=True, text=True).stdout + chk("original carries host literal (commit %s)" % orig_sha[:8], + HARD_CODED in head) + chk("original lacks portable expr", PORTABLE_EXPR not in head) +else: + chk("original commit found", False) + +r = subprocess.run([sys.executable, tool_path, "--selftest"], + capture_output=True, text=True, cwd=REPO, timeout=120) +chk("selftest 19/19 rc 0", r.returncode == 0 and "19 passed, 0 failed" in r.stdout) +chk("verifier committed to HEAD", + subprocess.run(["git", "-C", REPO, "cat-file", "-e", "HEAD:%s" % VERIFIER], + capture_output=True).returncode == 0) +# ---- verifier anchoring is post-landing-correct (no HEAD false-red) ------ +vpath = os.path.join(REPO, VERIFIER) +verifier_src = open(vpath).read() if os.path.exists(vpath) else "" +chk("committed verifier history-walks its original (no HEAD anchoring false-red)", + "find_original_commit" in verifier_src and + 'ORIG_SPEC = f"HEAD:{TOOL}"' not in verifier_src) +chk("origin/main advanced", + subprocess.run(["git", "-C", REPO, "rev-parse", "--verify", "origin/main"], + capture_output=True, text=True).returncode == 0) + +print(f"\n{sum(checks)}/{len(checks)} checks pass") +sys.exit(0 if all(checks) else 1) diff --git a/wizard-work/forge/verify_citation_hash_class_portable_default.py b/wizard-work/forge/verify_citation_hash_class_portable_default.py index b9341f0..81c4819 100644 --- a/wizard-work/forge/verify_citation_hash_class_portable_default.py +++ b/wizard-work/forge/verify_citation_hash_class_portable_default.py @@ -22,9 +22,11 @@ THE REFACTOR (two edits on the working tree, uncommitted at HEAD) 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) + ORIGINAL = newest pre-refactor commit in TOOL's history (find_original_commit walks + the file's commit history for the newest commit still carrying the host-literal + default and lacking the portable expr). Anchoring on HEAD is a false-red once + the refactor lands, since HEAD then carries the portable expr. + REFACTORED = working tree (portable DEFAULT_REPO). WHAT THIS VERIFIER PROVES The refactor is behavior-preserving: identical selftest contract, identical live @@ -32,7 +34,7 @@ WHAT THIS VERIFIER PROVES and the explicit --repo path, plus a code-level portability check. LEGS - [1] selftest: original (git HEAD) passes 19/19, rc 0 + [1] selftest: original (newest pre-refactor commit, history-walked) 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, @@ -54,7 +56,6 @@ 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" @@ -64,16 +65,40 @@ class Verdict(Exception): pass +def find_original_commit(): + """Newest commit in TOOL's history whose --repo default is still the host + literal (the genuine PRE-refactor original). Skips commits carrying the + portable __file__-based expression. Returns the sha or None. + + Anchoring ORIGINAL on HEAD is a false-red once the refactor lands in a + commit: HEAD then carries the portable expression, so `git show HEAD:TOOL` + reads the *refactored* text as "original" and the host-literal leg reports + FAIL. Walking the file's own commit history finds the true pre-refactor + original both before and after the refactor lands (same fix as + _chc_fast_check.find_original_commit). + """ + proc = subprocess.run(["git", "-C", REPO, "log", "--format=%H", "--", TOOL], + capture_output=True, text=True, check=True, timeout=60) + for sha in proc.stdout.split(): + txt = subprocess.run(["git", "-C", REPO, "show", "%s:%s" % (sha, TOOL)], + capture_output=True, text=True, timeout=60).stdout + if HARD_CODED in txt and PORTABLE_EXPR not in txt: + return sha + return None + + 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.""" + """Write the PRE-refactor module (newest pre-refactor commit in TOOL's + history) to a temp path; return (dir, path).""" + sha = find_original_commit() + if not sha: + raise Verdict("could not find a pre-refactor original for %s in history" % TOOL) out = tempfile.NamedTemporaryFile(suffix=".py", delete=False, mode="wb") try: - subprocess.run(["git", "-C", REPO, "show", ORIG_SPEC], + subprocess.run(["git", "-C", REPO, "show", "%s:%s" % (sha, TOOL)], check=True, stdout=out, timeout=30) except subprocess.CalledProcessError as exc: - raise Verdict("could not read %s: %s" % (ORIG_SPEC, exc)) + raise Verdict("could not read %s:%s: %s" % (sha, TOOL, exc)) finally: out.close() return os.path.dirname(out.name), out.name