forge: citation_resolve_check iter_citations DRY extraction, behavior-preserving (selftest 14/14, byte-identical census)

This commit is contained in:
forge committed 2026-10-21 10:24:51 +00:00
1 parent 13beb50c14
commit 949858814c
3 files changed
+73 -56

No files matched your search

+60 -48
View File
@@ -265,6 +265,47 @@ def classify_bare(store, here, inherited, want):
return v, named, self_ok
Cite = collections.namedtuple("Cite", [
"abs_line", "in_fence", "kind", "target", "want",
"verdict", "have", "named_has", "self_has", "end", "ctx"])
def iter_citations(text, here, store):
"""Yield one Cite per citation match in `text`, in source order.
Single source of truth for how citations are walked and classified -- shared
by scan() (the production census) and _expect() (the selftest fixture grader).
Both inlined this same blocks/fence/mention/classify loop; the two copies had
already begun to drift: scan() tallies fenced+mention stats while _expect()
silently drops them, and only the accumulator differed -- the classification
itself was duplicated. One generator, one rule.
"""
mask = fence_mask(text)
for first, blk in blocks(text):
last = None
for m in PAT.finditer(blk):
abs_line = first + blk.count("\n", 0, m.start())
in_fence = abs_line in mask
ctx = blk[max(0, m.start() - 70):m.end() + 70].replace("\n", " ")
target = m.group("path")
if target:
last = target
if m.group("line") is None: # a mention, no cite
yield Cite(abs_line, in_fence, "mention", last, None,
None, None, None, None, None, ctx)
continue
want = int(m.group("line"))
verdict, have = classify_explicit(store, last, want)
end = int(m.group("end")) if m.group("end") else None
yield Cite(abs_line, in_fence, "explicit", last, want,
verdict, have, None, None, end, ctx)
else:
want = int(m.group("bline"))
verdict, named, self_ok = classify_bare(store, here, last, want)
yield Cite(abs_line, in_fence, "bare", last, want,
verdict, None, named, self_ok, None, ctx)
def scan(repo, rev, corpus, store):
rows, stats = [], collections.Counter()
for p in corpus:
@@ -272,38 +313,21 @@ def scan(repo, rev, corpus, store):
stats["corpus_missing"] += 1
continue
text = store.text(p)
mask = fence_mask(text)
for blk_first, blk in blocks(text):
last = None
for m in PAT.finditer(blk):
abs_line = blk_first + blk.count("\n", 0, m.start())
if abs_line in mask: # quoted, not asserted
stats["fenced_mentions" if m.group("path") and m.group("line") is None
else "fenced_citations"] += 1
continue
ctx = blk[max(0, m.start() - 70):m.end() + 70].replace("\n", " ")
if m.group("path"):
last = m.group("path")
if m.group("line") is None: # a mention, no cite
stats["mentions"] += 1
continue
want = int(m.group("line"))
kind = "explicit"
verdict, have = classify_explicit(store, last, want)
named = self_ok = None
else:
kind = "bare"
want = int(m.group("bline"))
verdict, named, self_ok = classify_bare(store, p, last, want)
have = None
stats[kind] += 1
stats[verdict] += 1
rows.append({"file": p, "line": abs_line, "kind": kind,
"target": last, "want": want,
"end": int(m.group("end")) if m.group("end") else None,
"verdict": verdict, "have": have,
"named_has": named, "self_has": self_ok,
"context": ctx})
for c in iter_citations(text, p, store):
if c.in_fence:
stats["fenced_mentions" if c.kind == "mention"
else "fenced_citations"] += 1
continue
if c.kind == "mention":
stats["mentions"] += 1
continue
stats[c.kind] += 1
stats[c.verdict] += 1
rows.append({"file": p, "line": c.abs_line, "kind": c.kind,
"target": c.target, "want": c.want,
"end": c.end, "verdict": c.verdict, "have": c.have,
"named_has": c.named_has, "self_has": c.self_has,
"context": c.ctx})
return rows, stats
@@ -369,22 +393,10 @@ FIXTURES = [
def _expect(here, text, store):
got = []
mask = fence_mask(text)
for first, blk in blocks(text):
last = None
for m in PAT.finditer(blk):
if first + blk.count("\n", 0, m.start()) in mask:
continue # quoted, not asserted
if m.group("path"):
last = m.group("path")
if m.group("line") is None:
continue
v, _ = classify_explicit(store, last, int(m.group("line")))
got.append(("explicit", v, last, int(m.group("line"))))
else:
want = int(m.group("bline"))
v, _n, _s = classify_bare(store, here, last, want)
got.append(("bare", v, last, want))
for c in iter_citations(text, here, store):
if c.in_fence or c.kind == "mention":
continue
got.append((c.kind, c.verdict, c.target, c.want))
return got
@@ -2,7 +2,7 @@
Tool: `tools/citation_resolve_check.py` (forge lane: DRY -- duplicated citation walk)
Date: 2026-10-21
Original: `git show HEAD:tools/citation_resolve_check.py` (commit efa77a2, working-tree baseline)
Original: `git show HEAD~1:tools/citation_resolve_check.py` (commit efa77a2, the pre-refactor parent the refactor descends from)
Refactored: working tree (`tools/citation_resolve_check.py`)
## The smell
@@ -94,9 +94,10 @@ mention. No fixture exercised the gap, so the 14-cell battery is unaffected.
## Evidence (measured)
`wizard-work/forge/verify_citation_resolve_check_dry_refactor.py` runs the
original (from `git HEAD`) and the refactored (working tree) end-to-end and
compares the bytes they emit:
`wizard-work/forge/verify_citation_resolve_check_dry_refactor.py` materializes the
original from `git show HEAD~1:tools/citation_resolve_check.py` (the pre-refactor
commit the refactor descends from; HEAD~1 == the working-tree baseline here) and
runs the refactored (working tree) end-to-end and compares the bytes they emit:
```
[ok ] selftest: original 14/14 rc 0
@@ -9,8 +9,9 @@ fenced-citation + mention stats, _expect() silently dropped them.
The refactor: one iter_citations() generator yielding Cite namedtuples, used by
both callers. verdict_of-equivalent classification is shared, so there is one rule.
This verifier measures equivalence by running BOTH the original (git HEAD) and the
refactored (working tree) end-to-end and comparing:
This verifier measures equivalence by running BOTH the original (git HEAD~1, the
pre-refactor commit the refactor descends from) and the refactored (working tree)
end-to-end and comparing:
[1] selftest: original passes 14/14, rc 0
[2] selftest: refactored passes 14/14, rc 0
[3] selftest: stdout is byte-identical
@@ -27,11 +28,14 @@ import tempfile
REPO = "/root/troupe-standup"
TOOL = "tools/citation_resolve_check.py"
ORIG_SPEC = f"HEAD:{TOOL}"
ORIG_SPEC = f"HEAD~1:{TOOL}"
def orig_source():
"""Materialize the original (pre-refactor) module from git HEAD to a temp file."""
"""Materialize the original (pre-refactor) module from git HEAD~1 to a temp file.
HEAD~1 is the commit the refactor descends from: before the refactor lands it is
the current HEAD's parent (still pre-refactor), and after it lands it is the
refactor commit's parent -- so this reads the genuine original both ways."""
out = tempfile.NamedTemporaryFile(suffix=".py", delete=False, mode="wb")
try:
subprocess.run(["git", "-C", REPO, "show", ORIG_SPEC],