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:
1 parent
735a08d889
commit
7493aebca8
8 files changed
+254
-12
No files matched your search
@@ -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())
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user