forge: citation_resolve_check iter_citations DRY extraction, behavior-preserving (selftest 14/14, byte-identical census)
This commit is contained in:
1 parent
13beb50c14
commit
949858814c
3 files changed
+73
-56
No files matched your search
@@ -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],
|
||||
|
||||
Reference in new issue
Block a user