chore(acm2-75): 补文档校验盲区并接入 CI 门禁
This commit is contained in:
@@ -10,6 +10,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Check docs
|
||||||
|
run: python3 scripts/check-docs.py .
|
||||||
- name: Set up JDK 25
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ out/
|
|||||||
# 本地 agent 工具目录(R07)
|
# 本地 agent 工具目录(R07)
|
||||||
.opencode/
|
.opencode/
|
||||||
.zcode/
|
.zcode/
|
||||||
|
.qoder/
|
||||||
|
|
||||||
# --- 系统 ---
|
# --- 系统 ---
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+56
-2
@@ -4,9 +4,12 @@
|
|||||||
校验五件事:
|
校验五件事:
|
||||||
① docs 顶层 Markdown 固定为六个文件;
|
① docs 顶层 Markdown 固定为六个文件;
|
||||||
② 稳定 ID 定义唯一性与语法:每个 ID 恰好定义一次,且全仓引用均有定义;
|
② 稳定 ID 定义唯一性与语法:每个 ID 恰好定义一次,且全仓引用均有定义;
|
||||||
③ 旧文件名、章节号引用与已闭合 G 标记零命中(docs/legacy/ 外);
|
③ 旧文件名、章节号引用、已闭合 G 标记与字母后缀编号零命中(docs/legacy/ 外);
|
||||||
④ 仓库内 Markdown 链接有效。
|
④ 仓库内 Markdown 链接有效。
|
||||||
⑤ 给出 Git 基线时,持久 ID 集合与活跃 G 集合保持不变。
|
⑤ 给出 Git 基线时,持久 ID 集合与活跃 G 集合保持不变。
|
||||||
|
⑥ `application.yml` 的 `msgx.*` 叶子键逐个登记在 reference.md:`msgx.*` 是逐键登记的参数族,
|
||||||
|
光看文档查不出「代码有键、注册表没登记」的漂移;`mailbox.*`、`datasources.*`、`kafka.*`
|
||||||
|
按 reference.md 的约定成组登记,不做逐键检查。
|
||||||
|
|
||||||
用法:scripts/check-docs.py [仓库根目录] [Git 基线]
|
用法:scripts/check-docs.py [仓库根目录] [Git 基线]
|
||||||
"""
|
"""
|
||||||
@@ -56,6 +59,34 @@ USE_PATTERNS = {
|
|||||||
|
|
||||||
FIRST_COL = re.compile(r"^\|\s*`?([^|`]+?)`?\s*\|")
|
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:
|
def is_placeholder(kind: str, ident: str) -> bool:
|
||||||
if ident in PLACEHOLDER_WORDS:
|
if ident in PLACEHOLDER_WORDS:
|
||||||
@@ -234,13 +265,28 @@ def main() -> int:
|
|||||||
if not failures:
|
if not failures:
|
||||||
print(f"OK ID 注册表:{totals} 个定义各一处且位于所属注册表")
|
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 外)
|
# ③ 旧文件名、章节号引用与已闭合 G 标记零命中(legacy 外)
|
||||||
stale_name = re.compile(
|
stale_name = re.compile(
|
||||||
r"(design|invariants|contracts|flight-state|user-stories|spec-boundary-closure"
|
r"(design|invariants|contracts|flight-state|user-stories|spec-boundary-closure"
|
||||||
r"|message-lifecycle|runbooks)\.md")
|
r"|message-lifecycle|runbooks)\.md")
|
||||||
section_ref = re.compile(r"(?:§\s*\d|(?:第\s*)?\d+(?:\.\d+)*\s*节)")
|
section_ref = re.compile(r"(?:§\s*\d|(?:第\s*)?\d+(?:\.\d+)*\s*节)")
|
||||||
closed_g = re.compile(r"G-[A-Z][A-Z0-9-]*.*(?:✓|已闭合|已关闭)")
|
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:
|
for f in all_repo_files:
|
||||||
try:
|
try:
|
||||||
text = f.read_text(encoding="utf-8")
|
text = f.read_text(encoding="utf-8")
|
||||||
@@ -253,6 +299,9 @@ def main() -> int:
|
|||||||
section_hits.append(f"{f.relative_to(ROOT)}:{i}")
|
section_hits.append(f"{f.relative_to(ROOT)}:{i}")
|
||||||
if "legacy" not in f.parts and closed_g.search(line):
|
if "legacy" not in f.parts and closed_g.search(line):
|
||||||
closed_g_hits.append(f"{f.relative_to(ROOT)}:{i}")
|
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:
|
if stale_hits:
|
||||||
failures.append("存在指向已删除文档的文件名引用")
|
failures.append("存在指向已删除文档的文件名引用")
|
||||||
failures += stale_hits
|
failures += stale_hits
|
||||||
@@ -268,6 +317,11 @@ def main() -> int:
|
|||||||
failures += closed_g_hits
|
failures += closed_g_hits
|
||||||
else:
|
else:
|
||||||
print("OK 已闭合 G 标记零命中")
|
print("OK 已闭合 G 标记零命中")
|
||||||
|
if suffixed_hits:
|
||||||
|
failures.append("存在字母后缀编号(ID 不含字母后缀,同一编号只有一条定义)")
|
||||||
|
failures += suffixed_hits
|
||||||
|
else:
|
||||||
|
print("OK 字母后缀编号零命中")
|
||||||
|
|
||||||
# ④ 仓库内 Markdown 链接
|
# ④ 仓库内 Markdown 链接
|
||||||
link_files = list(DOCS.rglob("*.md")) + [ROOT / "README.md", ROOT / "AGENTS.md"]
|
link_files = list(DOCS.rglob("*.md")) + [ROOT / "README.md", ROOT / "AGENTS.md"]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# 文档体系机械校验入口:顶层结构、ID 注册表与全仓引用、陈旧引用、活跃 G、Markdown 链接。
|
# 文档体系机械校验入口:顶层结构、ID 注册表与全仓引用、application.yml 的 msgx.* 参数登记、
|
||||||
|
# 陈旧引用、活跃 G、字母后缀编号、Markdown 链接。
|
||||||
# 用法:scripts/check-docs.sh [Git 基线] (在仓库根目录执行)
|
# 用法:scripts/check-docs.sh [Git 基线] (在仓库根目录执行)
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|||||||
Reference in New Issue
Block a user