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