chore(acm2-75): 补文档校验盲区并接入 CI 门禁

This commit is contained in:
windyboy
2026-09-21 10:35:01 +08:00
parent 6da5a0021a
commit 32bbd459c1
4 changed files with 61 additions and 3 deletions
+56 -2
View File
@@ -4,9 +4,12 @@
校验五件事:
① docs 顶层 Markdown 固定为六个文件;
② 稳定 ID 定义唯一性与语法:每个 ID 恰好定义一次,且全仓引用均有定义;
③ 旧文件名、章节号引用已闭合 G 标记零命中(docs/legacy/ 外);
③ 旧文件名、章节号引用已闭合 G 标记与字母后缀编号零命中(docs/legacy/ 外);
④ 仓库内 Markdown 链接有效。
⑤ 给出 Git 基线时,持久 ID 集合与活跃 G 集合保持不变。
⑥ `application.yml` 的 `msgx.*` 叶子键逐个登记在 reference.md`msgx.*` 是逐键登记的参数族,
光看文档查不出「代码有键、注册表没登记」的漂移;`mailbox.*`、`datasources.*`、`kafka.*`
按 reference.md 的约定成组登记,不做逐键检查。
用法:scripts/check-docs.py [仓库根目录] [Git 基线]
"""
@@ -56,6 +59,34 @@ USE_PATTERNS = {
FIRST_COL = re.compile(r"^\|\s*`?([^|`]+?)`?\s*\|")
# 字母后缀编号(编号数字后面直接接小写字母)不在 ID 语法内。裸 `\bINV-\d+\b` 匹配不到它,
# 于是这类 ID 的定义与引用会一起从校验里消失:曾有一批这样的引用只出现在正文、没有定义,
# 校验器却全数漏过。
SUFFIXED_ID = re.compile(r"\b(?:US|OPS|C|PRE|INV|CLM|Q)-\d+[a-z]\b")
# application.yml 叶子键:有值(非空、非纯注释)的键才算叶子,父节点不算。
YML_KEY_LINE = re.compile(r"^(\s*)([A-Za-z0-9_-]+):\s*(.*)$")
def yml_leaf_keys(path: Path, prefix: str) -> list[tuple[str, int]]:
"""按缩进还原 `application.yml` 的叶子键全路径,只保留指定前缀。"""
keys: list[tuple[str, int]] = []
stack: list[tuple[int, str]] = []
for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if not line.strip() or line.lstrip().startswith("#"):
continue
m = YML_KEY_LINE.match(line)
if not m:
continue
indent, key, value = len(m.group(1)), m.group(2), m.group(3).strip()
stack = [item for item in stack if item[0] < indent]
stack.append((indent, key))
if value and not value.startswith("#"):
full = ".".join(item[1] for item in stack)
if full.startswith(prefix):
keys.append((full, lineno))
return keys
def is_placeholder(kind: str, ident: str) -> bool:
if ident in PLACEHOLDER_WORDS:
@@ -234,13 +265,28 @@ def main() -> int:
if not failures:
print(f"OK ID 注册表:{totals} 个定义各一处且位于所属注册表")
# ⑥ 参数键登记:代码里有、注册表里没有的 `msgx.*` 键,是文档查不出的那类漂移
app_yml = ROOT / "src/main/resources/application.yml"
if app_yml.exists():
ref_text = (DOCS / "reference.md").read_text(encoding="utf-8")
unregistered = [
f"src/main/resources/application.yml:{lineno} -> {key}"
for key, lineno in yml_leaf_keys(app_yml, "msgx.")
if key not in ref_text
]
if unregistered:
failures.append("application.yml 的 msgx.* 参数未登记在 reference.md")
failures += unregistered
else:
print("OK application.yml 的 msgx.* 参数均已登记在 reference.md")
# ③ 旧文件名、章节号引用与已闭合 G 标记零命中(legacy 外)
stale_name = re.compile(
r"(design|invariants|contracts|flight-state|user-stories|spec-boundary-closure"
r"|message-lifecycle|runbooks)\.md")
section_ref = re.compile(r"(?:§\s*\d|(?:第\s*)?\d+(?:\.\d+)*\s*节)")
closed_g = re.compile(r"G-[A-Z][A-Z0-9-]*.*(?:✓|已闭合|已关闭)")
stale_hits, section_hits, closed_g_hits = [], [], []
stale_hits, section_hits, closed_g_hits, suffixed_hits = [], [], [], []
for f in all_repo_files:
try:
text = f.read_text(encoding="utf-8")
@@ -253,6 +299,9 @@ def main() -> int:
section_hits.append(f"{f.relative_to(ROOT)}:{i}")
if "legacy" not in f.parts and closed_g.search(line):
closed_g_hits.append(f"{f.relative_to(ROOT)}:{i}")
if "legacy" not in f.parts:
for ident in SUFFIXED_ID.findall(line):
suffixed_hits.append(f"{f.relative_to(ROOT)}:{i} -> {ident}")
if stale_hits:
failures.append("存在指向已删除文档的文件名引用")
failures += stale_hits
@@ -268,6 +317,11 @@ def main() -> int:
failures += closed_g_hits
else:
print("OK 已闭合 G 标记零命中")
if suffixed_hits:
failures.append("存在字母后缀编号(ID 不含字母后缀,同一编号只有一条定义)")
failures += suffixed_hits
else:
print("OK 字母后缀编号零命中")
# ④ 仓库内 Markdown 链接
link_files = list(DOCS.rglob("*.md")) + [ROOT / "README.md", ROOT / "AGENTS.md"]
+2 -1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
# 文档体系机械校验入口:顶层结构、ID 注册表与全仓引用、陈旧引用、活跃 G、Markdown 链接。
# 文档体系机械校验入口:顶层结构、ID 注册表与全仓引用、application.yml 的 msgx.* 参数登记、
# 陈旧引用、活跃 G、字母后缀编号、Markdown 链接。
# 用法:scripts/check-docs.sh [Git 基线] (在仓库根目录执行)
set -uo pipefail
cd "$(dirname "$0")/.."