chore(文档缺陷订正与机检补件): 四份根文件用词与死指订正+新增引用可核机检件

修文档缺陷四处:AGENTS.md「收口」×2、「不得」×3 换白话;roadmap.md:87「对拍」「标旗」换代;
reports/README.md 指向已迁移的 design/gdd/06-待办清单.md 改指仓库根;待办清单 iter19 遗留第⑦项
前半「数据配置.md:223 历史行死指」实查核销(该文件版本史行已随禁写变更记录规约消亡,现 212 行)。
补机检:tools/doc_ref_audit.py 核文档内「文件:行号」指针的目标存在性与行界,带扫描面为空与
引用数为零双自检;双向变异证据——注入不存在路径 EXIT=1、注入超界行号 EXIT=1、还原 EXIT=0。
doc_term 扫描面扩到四份根文件:扩面前实测这批文件自身含 3 处用语表废止词,先改现实再扩面,
扩面后「疑似现行」0、注入「完美排雷」见红 EXIT=1、还原 EXIT=0。
08d 引用可核暂不注册进闸壳:建档须改 game/tests/.gates/baseline.json,触 game/ 即要求代码域全量测试,
文档域批按测试两入口铁规零跑——注册欠账已登记待办清单。内容锚核对实测 81 处出 58 条未命中(约七成噪声),
按建议级交付不判红。文档域闸全绿。
This commit is contained in:
DKChan committed 2026-10-21 05:51:17 +08:00
1 parent 735a08d889
commit 7493aebca8
8 files changed
+254 -12

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
#!/usr/bin/env python3
r"""doc_ref_audit.py — 文档内代码引用可核检查(机检)
职责:现行文档里凡写「某文件:第几行」,就该指得动。本工具核两件事:
①路径可解析——目标文件在 git 受控文件清单里存在;
②行号在界内——引用的行号不超过目标文件的实际行数。
动机:条目与规约常写 `board_view.gd:737` 这类指针,代码前进后行号漂移、文件迁移改名,
账面就与现实脱节而无人知(2026-10-21 迭代 32 收尾时实测抓到两例)。
扫描面:`design/` 现行文档树 + 四份根文件(`待办清单.md`/`体验验证清单.md`/`roadmap.md`/`AGENTS.md`)。
排除面:`reports/` 与 `design/research/`——两处是中间文档暂存地(doc-conventions 第 11 节),
入内文件按「吸收即死」退役,历史件不回溯改,故不入本闸判据。
自检二道(防假绿):扫描文件数为零 ⇒ FAIL;抽取到的引用数为零 ⇒ FAIL。
(正则被改写失手或路径写错时,检查会静默空转并报绿——与 static_scan 各段同规。)
判据分两档:
红(判 FAIL)=路径不存在/行号超界;
建议(只报告)=同名文件有多条路径需要人工定位/`--anchors` 的内容锚没在目标行附近找到。
用法:
python tools/doc_ref_audit.py # 全扫,报告模式
python tools/doc_ref_audit.py --strict # 有红项时 exit 1(供门禁)
python tools/doc_ref_audit.py --anchors # 追加内容锚核对(只报告,不判红)
python tools/doc_ref_audit.py <file.md> ... # 只扫指定文件
exit:0=全绿 / 1=有红项(--strict)/ 2=扫描面异常(目录缺失)
"""
import argparse
import io
import os
import re
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
SCAN_DIRS = ["design"]
SCAN_EXCLUDE_DIRS = ["design/research"]
ROOT_FILES = ["待办清单.md", "体验验证清单.md", "roadmap.md", "AGENTS.md"]
# 引用形态:路径或文件名 + 扩展名 + 半/全角冒号 + 行号。反引号可包在中间或外面,
# 因此先抓到扩展名为止,冒号允许紧跟或在反引号之后。
REF_RE = re.compile(
r"([\w./一-鿿-]+\.(?:gd|tres|sh|py|json|md|cfg|yml|yaml|txt|png|html))"
r"[`”』」]?\s*[::]\s*(\d+)"
)
# 内容锚:同一行内的反引号片段或「」内文本,取长度 ≥4 的第一个非引用片段
ANCHOR_RE = re.compile(r"`([^`\n]{4,60})`|「([^」\n]{4,60})」")
ANCHOR_WINDOW = 12
# 留档信号(与 doc_term_audit 同口径):同句带日期或退役/注销类说明时,指向已不存在的路径
# 属历史叙述而非现行指针,降为建议级报出、不判红。
ARCHIVE_RE = re.compile(
r"\d{4}-\d{2}-\d{2}|退役|注销|已删|已废除|已废|废除|作废|历史|原名|改名|迁入|迁到|移到|迁至"
)
def tracked_files():
out = subprocess.run(
["git", "-c", "core.quotepath=false", "-C", str(REPO), "ls-files"],
capture_output=True, text=True, encoding="utf-8", errors="ignore",
)
if out.returncode != 0:
return None
return {l.strip().strip('"').replace("\\", "/") for l in out.stdout.splitlines() if l.strip()}
def collect_docs(explicit):
if explicit:
return [p for p in explicit if Path(p).is_file()]
paths = []
for d in SCAN_DIRS:
root = REPO / d
if not root.is_dir():
return None
for p in sorted(root.rglob("*.md")):
rel = p.relative_to(REPO).as_posix()
if any(rel.startswith(x + "/") for x in SCAN_EXCLUDE_DIRS):
continue
paths.append(rel)
for f in ROOT_FILES:
if (REPO / f).is_file():
paths.append(f)
return paths
def resolve(name, doc_rel, index, bybase):
"""返回 (命中路径列表, 解析方式)。方式=exact / ambiguous / none。"""
name = name.strip("./")
if "/" in name:
for cand in (
os.path.normpath(os.path.join(os.path.dirname(doc_rel), name)).replace("\\", "/"),
os.path.normpath(name).replace("\\", "/"),
):
if cand in index:
return [cand], "exact"
return [], "none"
cands = sorted(bybase.get(name, []))
if not cands:
return [], "none"
if len(cands) == 1:
return cands, "exact"
return cands, "ambiguous"
def line_count(path, cache):
if path not in cache:
try:
cache[path] = len(io.open(REPO / path, encoding="utf-8", errors="ignore").read().splitlines())
except OSError:
cache[path] = 0
return cache[path]
def anchor_of(line, name):
"""取同句里可当内容锚的片段:反引号或「」内、长度 ≥4、且自身不是文件路径。"""
for m in ANCHOR_RE.finditer(line):
frag = (m.group(1) or m.group(2)).strip()
if name.split("/")[-1] in frag:
continue
if re.search(r"\.(gd|tres|sh|py|json|md|yaml|yml|html|png)$", frag):
continue
if re.fullmatch(r"[\d:~\-]+", frag):
continue
return frag
return None
def check(doc_rel, index, bybase, cache, anchors=False):
reds, advises = [], []
text = io.open(REPO / doc_rel, encoding="utf-8", errors="ignore").read()
for ln, line in enumerate(text.splitlines(), 1):
for m in REF_RE.finditer(line):
name = m.group(1).lstrip("①②③④⑤⑥⑦⑧⑨⑩〔[[((「」·、。,,;;")
num = int(m.group(2))
hits, how = resolve(name, doc_rel, index, bybase)
if how == "none":
if ARCHIVE_RE.search(line):
advises.append("%s:%d 引用已退役件 指 `%s`——同句带日期或退役/注销类说明,按留档处理" % (doc_rel, ln, name))
else:
reds.append("%s:%d 引用可核/路径不存在 文档指 `%s`,git 受控文件清单里没有这件" % (doc_rel, ln, name))
continue
if how == "ambiguous":
advises.append("%s:%d 同名多路径 指 `%s`——命中 %d 件(%s),需写全路径" % (
doc_rel, ln, name, len(hits), ", ".join(hits[:3])))
target = hits[0]
total = line_count(target, cache)
if num > total:
if ARCHIVE_RE.search(line):
advises.append("%s:%d 行号已越界(留档句) 指 `%s:%d`,目标现 %d 行——同句带退役/日期说明" % (
doc_rel, ln, name, num, total))
else:
reds.append("%s:%d 引用可核/行号超界 指 `%s:%d`,目标文件现只有 %d 行" % (doc_rel, ln, name, num, total))
continue
if anchors:
frag = anchor_of(line, name)
if frag:
lines = io.open(REPO / target, encoding="utf-8", errors="ignore").read().splitlines()
lo, hi = max(0, num - 1 - ANCHOR_WINDOW), min(len(lines), num + ANCHOR_WINDOW)
window = "\n".join(lines[lo:hi])
if frag not in window:
advises.append("%s:%d 内容锚未命中 指 `%s:%d`,同句片段「%s」不在该线 ±%d 行内" % (
doc_rel, ln, os.path.basename(name), num, frag[:28], ANCHOR_WINDOW))
return reds, advises
def main():
ap = argparse.ArgumentParser(description="文档内代码引用可核检查")
ap.add_argument("files", nargs="*", help="指定文件;缺省扫 design/ 现行树+四份根文件")
ap.add_argument("--strict", action="store_true", help="有红项时 exit 1")
ap.add_argument("--anchors", action="store_true", help="追加内容锚核对(只报告)")
args = ap.parse_args()
index = tracked_files()
if index is None:
print("引用可核 FAIL:git ls-files 取不到受控文件清单(扫描面异常)")
return 2
docs = collect_docs(args.files)
if docs is None:
print("引用可核 FAIL:扫描目录缺失(design/ 不在位)")
return 2
if not docs:
print("引用可核 FAIL:扫描面为空——路径写错导致的静默假绿")
return 1
bybase = {}
for p in index:
bybase.setdefault(p.rsplit("/", 1)[-1], []).append(p)
cache, all_reds, all_adv = {}, [], []
total_refs = 0
for doc in docs:
reds, adv = check(doc, index, bybase, cache, anchors=args.anchors)
total_refs += doc_ref_count(doc)
all_reds += reds
all_adv += adv
if total_refs == 0:
print("引用可核 FAIL:抽取到的引用数为零——正则或扫描面失手,属假绿防线")
return 1
for r in all_reds:
print(r)
print("── 引用可核:扫描 %d 件文档 · 抽取引用 %d 处 · 红项 %d · 建议 %d" % (
len(docs), total_refs, len(all_reds), len(all_adv)))
if all_adv:
for a in all_adv[:25]:
print(" (建议)" + a)
if len(all_adv) > 25:
print(" (建议)…另有 %d 条" % (len(all_adv) - 25))
return 1 if (args.strict and all_reds) else 0
def doc_ref_count(doc_rel):
text = io.open(REPO / doc_rel, encoding="utf-8", errors="ignore").read()
return sum(len(REF_RE.findall(l)) for l in text.splitlines())
if __name__ == "__main__":
sys.exit(main())
+8 -2
View File
@@ -14,8 +14,9 @@ r"""doc_term_audit.py — GDD 退役术语扫描(半自动)
python tools/doc_term_audit.py <file.md> ... # 只扫指定文件
python tools/doc_term_audit.py --json # 机器可读
口径:扫 design/gdd/ 与 design/art/ 两处文档域(2026-10-21 迭代 32 卡①反哺批扩面 art,
用户裁)——design/research(历史提案)、docs/(样例)与 production/ 不在扫描域;
口径:扫 design/gdd/ 与 design/art/ 两处文档域 + 四份根文件(`待办清单.md`/`体验验证清单.md`/
`roadmap.md`/`AGENTS.md`,2026-10-21 迭代 32 卡①反哺批扩面,用户裁;根文件同属现行文本)
——design/research(历史提案)与 reports/(中间文档)不在扫描域;
词表扩充 = 改 RETIRED_TERMS(受 doc-conventions §4 词表冻结约束:唯一入口=用户实读反馈并经用户裁决)。
"""
import argparse
@@ -71,6 +72,10 @@ RETIRED_TERMS = [
]
DEFAULT_DIRS = ["design/gdd", "design/art"]
# 根规则与流程文件同属现行文本,2026-10-21 迭代 32 反哺批复测后纳入扫描面:
# 当日实测这批文件自身含 3 处用语表废止词(roadmap 的「对拍」「标旗」、AGENTS 的「收口」×2),
# 随同批订正后判红归零才扩面——先改现实、再让闸盯住现实,避免带红扩面。
DEFAULT_FILES = ["待办清单.md", "体验验证清单.md", "roadmap.md", "AGENTS.md"]
# 用语规范 A 表补充词专项补扫:随批/台账 两词本身是上方行级白名单信号(行内出现即归
# 留档类),主扫描对它们自豁免;本通道只做节外残留的专项报告,供后续触碰批清偿。
@@ -191,6 +196,7 @@ def main():
for d in DEFAULT_DIRS:
paths += sorted(Path(d).rglob("*.md"))
paths += sorted(Path(d).rglob("*.yaml"))
paths += [Path(f) for f in DEFAULT_FILES]
paths = [p for p in paths if Path(p).exists()]
findings = scan(paths) + scan_supplement(paths)
+3
View File
@@ -185,6 +185,9 @@ gate "07 doc_link" "doc_link.log" "$PY" "$REPO/tools/doc_link_audit.py" --strict
gate "08 doc_term" "doc_term.log" "$PY" "$REPO/tools/doc_term_audit.py" --strict
# ── 8c. 流水线闸(待办清单条目状态机+roadmap 迭代总表+一次交叉核,判据=doc-conventions §12)──
gate "08c 流水线闸" "pipeline.log" "$PY" "$REPO/tools/pipeline_gate.py"
# 8d 引用可核(tools/doc_ref_audit.py,2026-10-21 交付)暂不注册:新闸件无基线记录必判「闸况变化」拦回执,
# 而建档要改 game/tests/.gates/baseline.json——触 game/ 即要求代码域全量测试(process-conventions §3.10 推域口径),
# 文档域批按测试两入口铁规零跑游戏测试。注册与建档随下一个触 game/ 的批次一并做(待办清单在册)。
fi
# ── 6c. 回放确定性对拍(条件闸件;属代码域)──
if in_domain code; then