forge: confirm citation_resolve_check iter_citations DRY refactor 8/8 behavior-preserving (live verify, 442-file/2401-cite census)

This commit is contained in:
forge committed 2026-10-21 07:25:41 +00:00
1 parent caf157e729
commit d310b35a0b
1 file changed
+125
@@ -0,0 +1,125 @@
# iter_citations DRY Refactor — Landing Verification
Tool: `tools/citation_resolve_check.py` (forge lane: Refactor — DRY consolidation).
State: refactor written into the working tree, **not** committed (see *Scope note*).
Original (HEAD): `caf157e` / blob `efa77a2e4ee5e835e8b2ac35c036a66dd65a0b1` (488 lines, no `iter_citations`).
Refactored (working tree): 500 lines, `iter_citations()` present.
## The smell
The citation-walking loop — `fence_mask()` + `blocks()` + `PAT.finditer()` + the
mention/explicit/bare classification and `classify_explicit`/`classify_bare` dispatch — was
inlined **twice**:
1. `scan()` — the production census. Tallies `fenced_citations`/`fenced_mentions`/`mentions`
stats, then appends a full row.
2. `_expect()` — the selftest fixture grader. Silently **drops** the fenced/mention stats
(only appends graded citations).
One walk, two rule sets for "is this a mention / is this quoted." A classification change in
`scan()` would not be mirrored in `_expect()`, so the self-test would still pass on its 14
fixtures while the census went wrong. Latent fault.
## The refactor
One `iter_citations(text, here, store)` generator yielding `Cite` namedtuples, consumed by
both callers. `Cite` carries the full classification
(`abs_line`, `in_fence`, `kind`, `target`, `want`, `verdict`, `have`,
`named_has`, `self_has`, `end`, `ctx`), so each consumer decides what to do with a row —
not how to walk the text:
```
Cite = collections.namedtuple("Cite", [...])
def iter_citations(text, here, store):
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
...
(yields one Cite per match, in source order)
```
`scan()` branches per `Cite.kind`/`Cite.in_fence`; `_expect()` skips
`in_fence or kind == "mention"` and appends `(c.kind, c.verdict, c.target, c.want)`.
Equivalence: same `fence_mask()` per file, same `blocks()` scope, same `PAT` ordering, same
`last` threading, same `abs_line`; `here` threads as `p` in `scan()` and as the `here` arg
in `_expect()`. The `mention` predicate `m.group("path") and m.group("line") is None` is
exactly `c.kind == "mention"`, so fenced/mention buckets collapse identically.
## Evidence (measured — run against current repo state)
`wizard-work/forge/verify_citation_resolve_check_dry_refactor.py` (committed in `8f592aa`)
runs the original from `git show HEAD:tools/citation_resolve_check.py` and the refactored from
the working tree end-to-end, then compares bytes. Verbatim output of this run
(`wizard-outputs/forge/iter_citations_verify_run.txt`):
```
[ok ] selftest: original 14/14 rc 0
[ok ] selftest: refactored 14/14 rc 0
[ok ] selftest: identical stdout (byte-for-byte)
[ok ] live scan: original ran rc 0
[ok ] live scan: refactored ran rc 0
[ok ] live scan: identical findings (citations)
[ok ] live scan: identical stats
[ok ] live scan: identical default stdout (rc 0, byte-identical)
verify: 8 checks, 8 passed, 0 failed
```
Exit code 0.
Self-test on the refactored (working-tree) tool:
```
self-test: 14 fixtures, 14 passed, 0 failed
```
Exit code 0.
Live census (both versions against the SAME current HEAD, 442 markdown files):
- EXPLICIT citations: 524 — 166 RESOLVES / 0 STALE_LINE / 358 EXTERNAL_PATH
- BARE candidate citations: 1877 — 150 AS_NAMED / 794 AS_SELF / 57 AMBIGUOUS / 268 EXTERNAL_NAMED / 608 UNRESOLVED
- Total graded citations: 2,401 (524 explicit + 1,877 bare)
- mentions (no line): 8,570; fenced skipped: 6 citations, 491 mentions
- findings (citations[]) and stats{} are **byte-identical** between original and refactored.
## Reconciling the stale scan snapshots
`wizard-outputs/forge/` carries two older capture files whose corpus counts differ:
```
citation_resolve_scan_original.txt : "corpus: 441 markdown files", "file mentions with no line: 8565"
citation_resolve_scan_refactored.txt: "corpus: 442 markdown files", "file mentions with no line: 8570"
```
That 441-vs-442 / 8565-vs-8570 delta is **not** a refactor divergence: the two snapshots were
taken at different repo moments (one markdown file was added between them; "mentions" rose by
exactly 5, matching 5 new mentions in that file). The verifier recomputes BOTH versions
against the **same** current HEAD in the same process and reports `identical stats` — so the
refactor introduces zero divergence; the snapshots are simply stale relative to the tip.
## Scope note (why the source is not committed here)
Per the operator's commit scope for this run (`git add wizard-work/forge/`), only this
verification artifact is landed. The source change to `tools/citation_resolve_check.py` stays
in the working tree, unstaged. This is intentional and correct: the verifier derives its
"original" via `git show HEAD:tools/citation_resolve_check.py`, so the source must remain
unstaged (HEAD = original, working tree = refactored) for that comparison to stay meaningful.
Landing the source is a separate, deliberate step (the established forge pattern: a
"forge: verify ... DRY refactor" commit precedes/a but the source lands in its own commit,
cf. `86d5b27`/`4c89084`). This artifact documents that the refactor is verified
behavior-preserving as-is, ready to land.
## Verdict
PASS — the `iter_citations()` DRY consolidation in `tools/citation_resolve_check.py` is
behavior-preserving: 14/14 self-test fixtures, identical findings (2,401 rows) and identical
stats across the full 442-file live census, and byte-identical default stdout, between the
HEAD original and the working-tree refactored build (8/8 checks, exit 0). The mention/fenced
predicate is unified without altering any result.
## CHECK (rerun the verifier)
```
python3 /root/troupe-standup/wizard-work/forge/verify_citation_resolve_check_dry_refactor.py
```
Exits 0 on full pass (8/8), 1 otherwise.