tools: 固化文档校验器为 GATE-DOCS(此前只存在于 /tmp,保证不可复现)

四个校验器原先散落在 /tmp,本会话 /tmp 就被清过一次(validate_docs.py
中途消失),仓库里没有任何引用——「222 个行号锚点 0 error」这个结论在脚本
被清之后就无从再证,属于静默漂移。现固化进 scripts/docs_check/:

- validate_docs.py  代码块围栏闭合 / 行内反引号偶数 / 本地链接与 #锚点可达
- check_tables.py   表格表头、分隔行、数据行列数一致(行内代码里的 | 不计)
- audit_anchors.py  每个 foo.rs:N / foo.rs:N-M 锚点指向存在且非空的目标行
- check_sec_refs.py 每个 §N.N 交叉引用解析到归属文档的真实标题
- run_all.sh        驱动脚本,任一失败即 exit 1,仿 GATE-P0 的用法

改动说明(相对 /tmp 原版)
- ROOT 由硬编码绝对路径改为 Path(__file__).resolve().parents[2],干净 clone
  即可运行,无需改任何路径。四个文件已 diff 确认仅此一处差异。
- audit_anchors.py 加了 docstring 写明能力边界:只证明锚点可达,不证明该行
  内容与所述一致,无法发现「指向错行」的锚点。
- run_all.sh 用 grep -m1 '^===' 取摘要而非 head -1:validate_docs.py 摘要前
  带一个空行(原版自带瑕疵,保留原样以免偏离已验证版本),按位置取会取错。

双向验证:干净树 PASS/exit 0;注入 worker.rs:99999 与 §99.9 后分别被
audit_anchors 与 check_sec_refs 报出、exit 1;git checkout 还原后回到 PASS。

AGENTS.md 记录跑法与「可达 ≠ 准确」的区分。GATE-P0(cargo)不受影响,本次
无任何 src/ 改动。
This commit is contained in:
anonsaber 2026-09-28 21:58:07 +08:00
parent c2f6f32093
commit a2297c21e8
6 changed files with 361 additions and 0 deletions

View File

@ -13,6 +13,12 @@
> `docs/deployment.md` §2.1、`docs/reference.md` §3/§4、`README` 中英双版;⑥ 落地 `/search` 与两项可靠性修复, > `docs/deployment.md` §2.1、`docs/reference.md` §3/§4、`README` 中英双版;⑥ 落地 `/search` 与两项可靠性修复,
> `docs/design.md` §5.7、`docs/reference.md` §6.3/§6.4、`docs/roadmap.md` 代码缺口区同步更新。 > `docs/design.md` §5.7、`docs/reference.md` §6.3/§6.4、`docs/roadmap.md` 代码缺口区同步更新。
> 各文档的行号锚点以代码 `bfe0fd8` 为准,代码变动后须同步重锚。 > 各文档的行号锚点以代码 `bfe0fd8` 为准,代码变动后须同步重锚。
>
> **改完文档或动了 `src/` 行号,先跑 `bash scripts/docs_check/run_all.sh`**(GATE-DOCS,4 个检查全 exit 0 才算过):
> `validate_docs.py`(代码块围栏/行内反引号/本地链接与锚点可达)、`check_tables.py`(表格列数一致,行内代码里的 `|` 不计)、
> `audit_anchors.py`(`foo.rs:N` 行号锚点存在且非空行)、`check_sec_refs.py`(`§N.N` 交叉引用指向真实标题)。
> **注意 `audit_anchors.py` 只证明锚点可达,不证明该行内容与所述一致**——它无法发现「指向了错行」的锚点,那需要人工读目标行。
> 引「0 errors」时请区分这两种含义。校验器自身随仓库版本化,无需 `/tmp` 副本。
--- ---

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Check every `foo.rs:N` / `foo.rs:N-M` line anchor in the docs.
LIMITATION: this only proves the cited line *exists and is not blank*. It does
not prove the line contains what the prose claims about it. A doc can pass this
audit with anchors that point at the wrong line -- a human must read the target
line to catch that. Keep this in mind when quoting "0 errors" as proof of
accuracy: it proves reachability, not correctness.
"""
import re, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
DOCS = ['AGENTS.md', 'README.md', 'README.zh-CN.md', 'docs/design.md',
'docs/deployment.md', 'docs/reference.md', 'docs/retired.md', 'docs/roadmap.md']
REF = re.compile(r'(?:(src|cloudflare-worker/src)/)?([A-Za-z0-9_]+)\.(?:rs|js):(\d+)(?:\s*[,–\-]\s*(\d+))?')
errors = []
checked = 0
for d in DOCS:
lines = (ROOT / d).read_text().splitlines()
in_fence = False
for i, line in enumerate(lines, 1):
if re.match(r'^\s*(```|~~~)', line):
in_fence = not in_fence
continue
if in_fence:
continue
for m in REF.finditer(line):
sub, base, a, b = m.group(1), m.group(2), m.group(3), m.group(4)
if sub == 'cloudflare-worker/src':
p = ROOT / sub / f'{base}.js'
elif sub == 'src' or sub is None:
p = ROOT / 'src' / f'{base}.rs'
if not p.exists() and sub is None:
q = ROOT / 'cloudflare-worker/src' / f'{base}.js'
if q.exists():
p = q
else:
p = ROOT / sub / f'{base}.rs'
if not p.exists():
errors.append(f'{d}:{i} unknown file {p.name}')
continue
src = p.read_text().splitlines()
hi = int(b) if b else int(a)
if hi > len(src):
errors.append(f'{d}:{i} {p.name}:{a}-{b} out of range (file has {len(src)} lines)')
continue
lo = int(a)
targets = {lo} | ({hi} if b else set())
for n in sorted(targets):
checked += 1
if not src[n - 1].strip():
errors.append(f'{d}:{i} {p.name}:{a}{"-" + b if b else ""} -> line {n} is blank')
print(f'=== ANCHOR AUDIT ({checked} line-refs checked, {len(errors)} errors) ===')
for e in errors:
print(e)
sys.exit(1 if errors else 0)

View File

@ -0,0 +1,103 @@
#!/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"]
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)\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())

View File

@ -0,0 +1,54 @@
import re, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
DOCS = ['AGENTS.md', 'README.md', 'README.zh-CN.md', 'docs/design.md',
'docs/deployment.md', 'docs/reference.md', 'docs/retired.md', 'docs/roadmap.md']
CODE = re.compile(r'`[^`\n]*`')
SEP = re.compile(r'^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$')
def cells(line):
"""Split a table row into cells, ignoring pipes inside inline code spans."""
blanks = [' ' * len(s) for s in CODE.findall(line)]
stripped = CODE.sub(lambda m: blanks.pop(0), line)
s = stripped.strip()
if s.startswith('|'):
s = s[1:]
if s.endswith('|'):
s = s[:-1]
return s.split('|')
errors = []
for d in DOCS:
lines = (ROOT / d).read_text().splitlines()
in_fence = False
i = 0
while i < len(lines):
if re.match(r'^\s*(```|~~~)', lines[i]):
in_fence = not in_fence
i += 1
continue
if in_fence:
i += 1
continue
# detect table start: a row containing a pipe, followed by a separator row
if '|' in lines[i] and i + 1 < len(lines) and SEP.match(lines[i + 1]):
n_head = len(cells(lines[i]))
n_sep = len(cells(lines[i + 1]))
if n_head != n_sep:
errors.append(f'{d}:{i+1} header/separator column mismatch: {n_head} vs {n_sep}')
j = i + 2
while j < len(lines) and '|' in lines[j] and not re.match(r'^\s*$', lines[j]):
n = len(cells(lines[j]))
if n != n_head:
errors.append(f'{d}:{j+1} row has {n} cells, header has {n_head}: {lines[j].strip()[:90]}')
j += 1
i = j
continue
i += 1
print(f'=== TABLE STRUCTURE ({len(errors)} mismatches) ===')
for e in errors:
print(e)
sys.exit(1 if errors else 0)

53
scripts/docs_check/run_all.sh Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env bash
# GATE-DOCS: run every docs-check validator and exit non-zero if any fails.
#
# Run after editing docs or after a code commit that moves line numbers:
# bash scripts/docs_check/run_all.sh
#
# What each check proves -- read before quoting the totals as "accurate":
#
# validate_docs markdown fences balanced, inline backticks even, and every
# local link + #anchor target exists.
# check_tables markdown tables: header/separator/row column counts agree,
# pipes inside inline code spans are not counted.
# audit_anchors every `foo.rs:N` / `foo.rs:N-M` anchor in the docs points at
# a line that EXISTS and is not blank. LIMITATION: it does not
# prove the line says what the prose claims. A doc can pass
# with anchors on the wrong line -- someone must read them.
# check_sec_refs every "§N.N" cross-reference resolves to a real heading in
# the document it is attributed to.
#
# audit_anchors and check_sec_refs only scan prose: lines inside fenced code
# blocks are skipped, so examples and tables holding code do not raise noise.
#
# Root is derived from this file's own location, so the whole suite works from a
# clean clone without editing any path.
set -uo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
rc=0
fail=0
for script in validate_docs check_tables audit_anchors check_sec_refs; do
out="$(python3 "$here/$script.py" 2>&1)"
code=$?
# Each check prints "=== NAME (n errors) ===" as its summary, but not
# necessarily as its first line (validate_docs.py leads with a blank line),
# so match on the "===" prefix instead of assuming a position.
summary="$(printf '%s\n' "$out" | grep -m1 '^===' || true)"
printf '%s\n' "${summary:-$script: no summary line emitted}"
if [ "$code" -ne 0 ]; then
printf '%s\n' "$out" | grep -v '^===' | grep -v '^[[:space:]]*$' \
| sed 's/^/ /' || true
rc=1
fail=$((fail + 1))
fi
done
if [ "$fail" -eq 0 ]; then
echo "GATE-DOCS: PASS (4/4 checks clean)"
else
echo "GATE-DOCS: FAIL ($fail check(s) reported errors)"
fi
exit "$rc"

View File

@ -0,0 +1,85 @@
import re, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
DOCS = ['AGENTS.md', 'README.md', 'README.zh-CN.md', 'docs/design.md',
'docs/deployment.md', 'docs/reference.md', 'docs/retired.md', 'docs/roadmap.md']
errors = []
def slug(t):
t = re.sub(r'[^\w\s\-]', '', t.strip().lower(), flags=re.UNICODE)
return t.replace(' ', '-')
def heading_set(text):
return set(slug(m.group(2)) for m in re.finditer(r'^(#{1,6})\s+(.*)$', text, re.M))
HEADS = {d: heading_set((ROOT / d).read_text()) for d in DOCS}
def markdown_links(text):
for m in re.finditer(r'(?<!!)\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)', text):
yield m.group(1)
for d in DOCS:
lines = (ROOT / d).read_text().splitlines()
in_fence = False
for i, line in enumerate(lines, 1):
if re.match(r'^\s*(```|~~~)', line):
in_fence = not in_fence
continue
if in_fence:
continue
if line.count('`') % 2:
errors.append(f'{d}:{i} odd backtick count')
for url in markdown_links(line):
if url.startswith(('http://', 'https://', 'mailto:')):
continue
if url.startswith('#'):
frag = url[1:]
if frag and frag not in HEADS[d]:
errors.append(f'{d}:{i} in-page anchor "{frag}" not found')
continue
if '#' in url:
tgt_rel, frag = url.split('#', 1)
else:
tgt_rel, frag = url, ''
tgt_rel = tgt_rel or d
base = (ROOT / d).parent
try:
resolved = Path(base, tgt_rel).resolve()
tgt_rel = str(resolved.relative_to(ROOT))
except ValueError:
errors.append(f'{d}:{i} escapes repo: {url}')
continue
if not resolved.exists():
errors.append(f'{d}:{i} broken link: {url}')
continue
if frag:
hs = HEADS.get(tgt_rel)
if hs is None:
if resolved.suffix == '.md':
hs = heading_set(resolved.read_text())
else:
continue
if frag not in hs:
errors.append(f'{d}:{i} anchor "{frag}" not in {tgt_rel}')
if in_fence:
errors.append(f'{d}: unclosed fenced code block')
# cross-document section references (allowed: same-doc "本文件 §", AGENTS.md index table)
for d in DOCS:
lines = (ROOT / d).read_text().splitlines()
in_fence = False
for i, line in enumerate(lines, 1):
if re.match(r'^\s*(```|~~~)', line):
in_fence = not in_fence
continue
if in_fence or '`' in line or line.lstrip().startswith(('#', '|')):
continue
if re.search(r'docs/[a-z]+\.md|README(?:\.zh-CN)?\.md', line) and '§' in line:
if d == 'AGENTS.md' or '本文件' in line:
continue
errors.append(f'{d}:{i} cross-doc section ref: {line.strip()}')
print(f'\n=== VALIDATION ({len(errors)} errors) ===')
for e in errors:
print(e)
sys.exit(1 if errors else 0)