MessageWeave/scripts/docs_check/check_sec_refs.py

104 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Check that every intra-document `§X.Y` / `§X` reference resolves to a real
heading in the same file.
Cross-document refs (e.g. `docs/design.md §5.7` or `design.md §5.7`) are also
checked, against the *target* file's heading set.
This is the gap that let `docs/design.md` §597/§658 and `docs/roadmap.md` §24
point at a design.md §3.4 that never existed.
"""
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
DOCS = ["AGENTS.md", "README.md", "README.zh-CN.md", "docs/design.md",
"docs/deployment.md", "docs/reference.md", "docs/retired.md",
"docs/roadmap.md", "docs/charter.md"]
HEADING = re.compile(r"^(#{2,4})\s+(\d+(?:\.\d+)?\.?)\s")
# §2.3, §5.7, §3 (bare), also `§2.3/§3.4` chains handled by finditer
SEC = re.compile(r"§(\d+(?:\.\d+)?)")
# an explicit target file sitting immediately before the §ref
TARGET_BEFORE = re.compile(
r"(?:docs/)?(AGENTS\.md|README\.md|README\.zh-CN\.md|design\.md|"
r"deployment\.md|reference\.md|retired\.md|roadmap\.md|charter\.md)\s*[`\s]*"
r"§(\d+(?:\.\d+)?)")
def headings(path):
"""Return the set of section numbers present as headings."""
out = set()
with open(path, encoding="utf-8") as fh:
for line in fh:
m = HEADING.match(line)
if m:
out.add(m.group(2).rstrip("."))
return out
def main():
names = {os.path.basename(p): p for p in DOCS}
heads = {os.path.basename(p): headings(os.path.join(ROOT, p)) for p in DOCS}
errors = []
checked = 0
for rel in DOCS:
base = os.path.basename(rel)
full = os.path.join(ROOT, rel)
with open(full, encoding="utf-8") as fh:
for lineno, line in enumerate(fh, 1):
if line.lstrip().startswith("#"):
continue
# Resolve against the current file by default; a table row
# whose first cell names a document, or an earlier explicit
# `foo.md §N`, overrides it for the rest of the line.
target = base
override_pos = -1
if line.lstrip().startswith("|"):
first_cell = line.split("|")[1:2]
m_cell = re.search(r"(?:docs/)?([A-Za-z0-9_.-]+\.md)",
first_cell[0]) if first_cell else None
if m_cell:
target = m_cell.group(1)
override_pos = line.find(m_cell.group(1))
for m in TARGET_BEFORE.finditer(line):
target = m.group(1)
override_pos = m.end()
checked += 1
if m.group(2) not in heads.get(target, set()):
errors.append(
f"{rel}:{lineno}: §{m.group(2)} does not exist "
f"in {target}")
for m in SEC.finditer(line):
# RFC-style three-part numbering (`§7.2.3`) is external,
# not a doc reference.
if line[m.end():m.end() + 1] == ".":
continue
if m.start() <= override_pos:
# already consumed as a cross-doc ref
continue
num = m.group(1)
# chained refs (`§2.3、§3.2、§5.7`) inherit `target`
pos = m.start()
prefix = line[:pos]
if re.search(r"(?:\.md)[`\s]*$", prefix):
target = os.path.basename(
re.search(r"([A-Za-z0-9_.-]+\.md)[`\s]*$",
prefix).group(1))
checked += 1
if num not in heads.get(target, set()):
errors.append(
f"{rel}:{lineno}: §{num} does not exist in {target}")
print(f"=== SECTION-REF AUDIT ({checked} refs checked, "
f"{len(errors)} errors) ===")
for e in errors:
print(" " + e)
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())