forge: citation_hash_class verdict_of() DRY extraction, behavior-preserving
This commit is contained in:
1 parent
f1d40d1da7
commit
033acf3298
2 files changed
+226
No files matched your search
@@ -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.
|
||||
@@ -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())
|
||||
Reference in new issue
Block a user