diff --git a/wizard-work/forge/CITATION_HASH_CLASS_DRY_REFACTOR_VERIFICATION.md b/wizard-work/forge/CITATION_HASH_CLASS_DRY_REFACTOR_VERIFICATION.md new file mode 100644 index 0000000..b772e34 --- /dev/null +++ b/wizard-work/forge/CITATION_HASH_CLASS_DRY_REFACTOR_VERIFICATION.md @@ -0,0 +1,92 @@ +# CITATION_HASH_CLASS DRY REFACTOR VERIFICATION + +Tool: `tools/citation_hash_class.py` (forge lane: DRY, verdict-classification logic) +Date: 2026-10-21 +Original: `git show HEAD:tools/citation_hash_class.py` +Refactored: working tree (`tools/citation_hash_class.py`) + +## The smell + +The verdict-classification logic -- the four-branch `if/elif/else` chain that +maps `(bf, fam, after_size)` to one of `BOUND_UNRESOLVED`, `CLASS_MISMATCH`, +`CLASSLESS_AFTER_SIZE`, or `ok` -- was inlined **twice**: once in `scan()` +(the production census path) and once in `selftest()` (the fixture verifier). +The two copies had **quietly diverged**: + +- `scan()` guarded CLASS_MISMATCH with `fam not in ('AMBIGUOUS', 'UNRESOLVED')` +- `selftest()` omitted that guard, so `AMBIGUOUS + bf != fam` would have been + classified `CLASS_MISMATCH` in the test where it is `ok` in production. + +No existing fixture exercised the AMBIGUOUS+bound path, so the divergence was +invisible. Two copies of the same rule is a latent fault waiting for a fixture +to cover it. + +## The refactor + +One `verdict_of(bf, fam, after_size)` function, replacing both inline blocks. +The canonical logic is the one `scan()` already used (the production path), so +the selftest copy is now corrected to match, not the other way around. + +```python +def verdict_of(bf, fam, after_size): + if bf and fam == 'UNRESOLVED': + return 'BOUND_UNRESOLVED' + if bf and fam not in ('AMBIGUOUS', 'UNRESOLVED') and bf != fam: + return 'CLASS_MISMATCH' + if not bf and after_size and fam in ('BLOB_OID', 'CONTENT_SHA256'): + return 'CLASSLESS_AFTER_SIZE' + return 'ok' +``` + +`scan()` now calls `verdict = verdict_of(bf, fam, after_size)`. +`selftest()` now calls `got = verdict_of(bf, fam, after)`. + +## Equivalence argument + +- The verdict is a pure function of `(bf, fam, after_size)`. Both callers + compute `bf` from `bound_family()`, `fam` from `resolve()`, and `after_size`/`after` + from the same `AFTER_SIZE` regex on the same window -- all unchanged. +- `verdict_of()` encodes the `scan()` branch conditions verbatim, so every + production result is identical by construction. +- The `selftest()` branch for CLASS_MISMATCH is the only one that changed (it + gains the `fam not in ('AMBIGUOUS', 'UNRESOLVED')` guard), and no fixture + covers AMBIGUOUS+bound, so the 19-cell battery is unaffected. + +## Evidence (measured) + +`wizard-work/forge/verify_citation_hash_class_dry_refactor.py`: + +``` + [ok ] selftest: original 19/19 rc 0 + [ok ] selftest: refactored 19/19 rc 0 + [ok ] selftest: identical stdout + [ok ] verdict_of matches old inline logic on all VERDICT_FIXTURES + [ok ] divergence: AMBIGUOUS+bound now 'ok' (not CLASS_MISMATCH) + [ok ] live scan: identical findings JSON + [ok ] live scan: identical stats + +verify: 7 checks, 7 passed, 0 failed +``` + +### Verdict breakdown + +- **[1-3] SELFTEST PARITY.** Both original and refactored pass 19/19 with + byte-identical stdout. The refactoring did not alter the selftest contract. + +- **[4] VERDICT OF AGREEMENT.** `verdict_of()` returns the same verdict as the + old inline `scan()` logic on every existing VERDICT_FIXTURE (4 rows: + ok, CLASSLESS_AFTER_SIZE x2, CLASS_MISMATCH). + +- **[5] DIVERGENCE CLOSED.** The refactored `verdict_of()` is confirmed to + agree with the old `scan()` guard for the AMBIGUOUS+bound case: returns + `'ok'`, not `'CLASS_MISMATCH'`. The selftest copy now matches production. + +- **[6-7] LIVE SCAN PARITY.** Running both the original and refactored tool + end-to-end with `--json` over the repo's actual corpus produces identical + findings arrays and identical stats dicts. The census is unchanged. + +## Verdict + +PASS -- verdict-classification DRY consolidation is behavior-preserving across +all 19 selftest fixtures and the full live corpus scan; the AMBIGUOUS+bound +divergence between the two old copies is closed. diff --git a/wizard-work/forge/verify_citation_hash_class_dry_refactor.py b/wizard-work/forge/verify_citation_hash_class_dry_refactor.py new file mode 100644 index 0000000..93abbc7 --- /dev/null +++ b/wizard-work/forge/verify_citation_hash_class_dry_refactor.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Verify the verdict_of() DRY extraction from citation_hash_class.py is behavior-preserving. + +Compares the ORIGINAL (pre-refactor) verdict logic against the REFACTORED version: +1. selftest output is byte-identical (19/19 both) +2. verdict_of() agrees with the old inline logic on every existing fixture +3. The live scan on the repo produces identical JSON census +4. The AMBIGUOUS+bound divergence (which the old selftest had but scan() didn't) + is now FIXED: refactored uses one logic, not two +""" +import importlib.util +import json +import subprocess +import sys + +REPO = "/root/troupe-standup" +ORIG = "/root/.hermes/wizard-outputs/forge/citation_hash_class.orig.py" + + +def load_module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def run_selftest(path): + """Run --selftest on the given path; return (rc, stdout, stderr).""" + proc = subprocess.run( + [sys.executable, path, "--selftest"], + capture_output=True, text=True, cwd=REPO, timeout=120) + return proc.returncode, proc.stdout, proc.stderr + + +def main(): + cells, failed = [], 0 + + def check(label, ok, detail=""): + cells.append((label, ok)) + nonlocal failed + if not ok: + failed += 1 + print(f" [{'ok ' if ok else 'FAIL'}] {label}" + + (f" [{detail}]" if detail and not ok else "")) + + orig = load_module("orig", ORIG) + ref = load_module("ref", f"{REPO}/tools/citation_hash_class.py") + + # --- [1] selftest: both pass 19/19, identical summary line --- + rc_o, out_o, _ = run_selftest(ORIG) + rc_r, out_r, _ = run_selftest(f"{REPO}/tools/citation_hash_class.py") + check("selftest: original 19/19 rc 0", + rc_o == 0 and "19 passed, 0 failed" in out_o, + f"rc={rc_o}") + check("selftest: refactored 19/19 rc 0", + rc_r == 0 and "19 passed, 0 failed" in out_r, + f"rc={rc_r}") + check("selftest: identical stdout", out_o == out_r) + + # --- [2] verdict_of matches the old inline scan() logic on VERDICT_FIXTURES --- + # The old scan() logic (from the original source) for verdict: + # if bf and fam == 'UNRESOLVED': -> 'BOUND_UNRESOLVED' + # elif bf and fam not in ('AMBIGUOUS','UNRESOLVED') and bf != fam: -> 'CLASS_MISMATCH' + # elif not bf and after_size and fam in ('BLOB_OID','CONTENT_SHA256'): -> 'CLASSLESS_AFTER_SIZE' + # else: 'ok' + def old_veridict(bf, fam, after_size): + if bf and fam == 'UNRESOLVED': + return 'BOUND_UNRESOLVED' + if bf and fam not in ('AMBIGUOUS', 'UNRESOLVED') and bf != fam: + return 'CLASS_MISMATCH' + if not bf and after_size and fam in ('BLOB_OID', 'CONTENT_SHA256'): + return 'CLASSLESS_AFTER_SIZE' + return 'ok' + + all_ok = True + for text, tok, fam, want in orig.VERDICT_FIXTURES: + i = text.index(tok) + bf, _ = orig.bound_family(text, i, i + len(tok), 40) + after = bool(orig.AFTER_SIZE.search(text[max(0, i - 40):i])) + old_v = old_veridict(bf, fam, after) + new_v = ref.verdict_of(bf, fam, after) + if old_v != new_v: + all_ok = False + print(f" DIVERGE on {text!r}: old={old_v} new={new_v}") + check("verdict_of matches old inline logic on all VERDICT_FIXTURES", + all_ok) + + # --- [3] The AMBIGUOUS+bound divergence is resolved --- + # Old scan(): AMBIGUOUS + bf != fam -> 'ok' (excluded by guard) + # Old selftest(): AMBIGUOUS + bf != fam -> 'CLASS_MISMATCH' (guard missing) + # Refactored: verdict_of uses the scan() guard, so AMBIGUOUS+bound -> 'ok' + check("divergence: AMBIGUOUS+bound now 'ok' (not CLASS_MISMATCH)", + ref.verdict_of('BLOB_OID', 'AMBIGUOUS', False) == 'ok' + and ref.verdict_of('BLOB_OID', 'AMBIGUOUS', False) + == old_veridict('BLOB_OID', 'AMBIGUOUS', False)) + + # --- [4] Live scan JSON on the repo is identical --- + # Run both versions producing JSON and compare the findings. + import tempfile, os + with tempfile.NamedTemporaryFile(suffix='.json', delete=False, mode='w') as f: + orig_json_path = f.name + with tempfile.NamedTemporaryFile(suffix='.json', delete=False, mode='w') as f: + ref_json_path = f.name + try: + subprocess.run([sys.executable, ORIG, "--json", orig_json_path], + capture_output=True, text=True, cwd=REPO, timeout=120, + check=True) + subprocess.run([sys.executable, f"{REPO}/tools/citation_hash_class.py", + "--json", ref_json_path], + capture_output=True, text=True, cwd=REPO, timeout=120, + check=True) + with open(orig_json_path) as f: + oj = json.load(f) + with open(ref_json_path) as f: + rj = json.load(f) + # Compare findings (rows) -- these are the verdicts produced by scan() + same_findings = oj["findings"] == rj["findings"] + check("live scan: identical findings JSON", same_findings, + f"orig={len(oj['findings'])} ref={len(rj['findings'])}") + # Compare stats + same_stats = oj["stats"] == rj["stats"] + check("live scan: identical stats", same_stats) + finally: + os.unlink(orig_json_path) + os.unlink(ref_json_path) + + total = len(cells) + passed = total - failed + print(f"\nverify: {total} checks, {passed} passed, {failed} failed") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main())