diff --git a/tools/citation_resolve_check.py b/tools/citation_resolve_check.py index a76c961..caf09e5 100644 --- a/tools/citation_resolve_check.py +++ b/tools/citation_resolve_check.py @@ -101,17 +101,30 @@ WORKED CATCH -- this tool's first finding is one of MY OWN artifacts python3 citation_resolve_check.py --only unresolved --context | grep volt.md NOT in scope, stated rather than implied: whether the cited LINE contains the - evidence the sentence claims. That needs the claim, not just the pointer. And a - citation REPRODUCED INSIDE QUOTES is still a citation to this scanner, which - does not parse quotation marks -- so a document that reports a defective + evidence the sentence claims. That needs the claim, not just the pointer. A + citation reproduced inside INLINE quotes is still a citation to this scanner, + which does not parse quotation marks -- so a document that reports a defective citation verbatim appears to contain it. Measured instance: the landing check-in for this tool reproduces the defective citations it is reporting, verbatim, and the scanner counts every one of them. +A FENCED BLOCK IS QUOTED MATERIAL, NOT THIS DOCUMENT'S PROSE -- made 2026-10-19 + `tools/checkin_check.py` and `tools/queue_index.py` both refuse to read + structure out of a ``` fence, on the rule that a quoted heading is not a + heading. This scanner had no fence state, so a fenced listing that carries + `path:line` rows was graded as the containing document's own citation. The + question was left open on 2026-10-19 ("a quoted tool output is not a claim") + and is closed here by measurement rather than taste: over the whole corpus at + HEAD the fenced rows are 0.26% of the census -- six out of 2,273. Every one + is a bare `:line` inside a code listing -- a number the listing quotes, not + one the document asserts. The exclusion removes no prose verdict (pre/post row + diff: exactly those six rows, no verdict changed) and the count is PRINTED + rather than silently dropped, so a reader sees what was set aside. + USAGE python3 citation_resolve_check.py [--repo DIR] [--rev HEAD] [--corpus PATH...] [--json OUT] [--only stale|asself|unresolved|ambiguous|external|externalnamed|named|all] - [--context] [--selftest] # 11 fixtures + [--context] [--selftest] # 14 fixtures """ import argparse import collections @@ -139,6 +152,28 @@ PAT = re.compile( r"(?:`?:(?P[0-9]{1,7})(?:\s*[-\u2013,]\s*(?P[0-9]{1,7}))?)?" r"|(?[0-9]{3,7})(?![0-9])" % EXT) +# A FENCED BLOCK IS QUOTED MATERIAL, NOT THIS DOCUMENT'S PROSE (2026-10-19). +# The same rule `queue_index.py` and `checkin_check.py` already apply to headings: +# a quoted structure is not structure. Here the quoted thing is a citation. The +# mask is computed per FILE (not per block) because a fence can contain blank +# lines and so straddle two of `blocks()`' paragraphs; line numbers stay absolute, +# which is what makes the join sound. The count is reported, never silently +# dropped: `fenced skipped` is printed on every run. +FENCE = re.compile(r"^\s*(```|~~~)") + + +def fence_mask(text): + """1-based line numbers that are a fence marker or sit inside a fence.""" + out, in_fence = set(), False + for i, raw in enumerate(text.split("\n"), 1): + if FENCE.match(raw): + out.add(i) + in_fence = not in_fence + continue + if in_fence: + out.add(i) + return out + def git(repo, *a): return subprocess.run(["git", "-C", repo, *a], capture_output=True).stdout @@ -233,10 +268,15 @@ 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") @@ -310,14 +350,27 @@ FIXTURES = [ # a range is reported with its end line (QUEUE, "recoveries/helm-pin-hash-convention.md:85-90", [("explicit", "RESOLVES", "recoveries/helm-pin-hash-convention.md", 85)]), + # a fenced block is quoted material, not the containing document's prose -- + # the same rule queue_index.py and checkin_check.py apply to headings + (QUEUE, "```\ncheckins/2026-10-15-volt.md:12\n```", []), + # ... and the fence CLOSES: the same citation in prose is graded again, the + # positive control that shows the mask is a state and not a blanket refusal + (QUEUE, "```\ncheckins/2026-10-15-volt.md:12\n```\n" + "see checkins/2026-10-15-volt.md:12", + [("explicit", "RESOLVES", "checkins/2026-10-15-volt.md", 12)]), + # an unterminated fence quotes to end of file, the fence's documented cost + (QUEUE, "```\nWORK-QUEUE.md:999999", []), ] def _expect(here, text, store): got = [] - for _first, blk in blocks(text): + 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: @@ -388,6 +441,9 @@ def main(): f"check them without inferring the referent)") print(f"file mentions with no line: {stats['mentions']} (unit: occurrences) " f"-- these seed the inheritance scope") + print(f"fenced skipped: {stats['fenced_citations']} citations, " + f"{stats['fenced_mentions']} mentions (unit: rows) -- a fenced block is " + f"quoted material, not this document's prose") by_target = collections.Counter(r["target"] for r in rows if r["kind"] == "explicit" and r["target"]) print(f"distinct explicitly cited targets: {len(by_target)} (unit: distinct paths)")