#!/usr/bin/env python # -*- coding: utf-8 -*- """ 文档一致性校验。 被校的东西都是历史上真实出错过的: 1. 表格列数一致(错位会让 Markdown 渲染成一堆竖线) 2. 接口数:逐节「标题声称数」vs「实际表行数」——只看总数会漏掉 "这节多算 3 个、那节少算 3 个、总数恰好相等"这种情况 3. 错误码数量与文档声称一致 4. 关键数字跨文档一致(FR / VAL / 任务 / 人日 / 表数 / 接口) 5. 原型屏数与 figcaption 数一致 用法 ---- python scripts/verify-docs.py # 校验,失败退出码 1 """ from __future__ import annotations import io import os import re import sys if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) D = os.path.join(ROOT, "docs") BT = chr(96) ok_count = 0 bad: list[str] = [] def ok(msg: str) -> None: global ok_count ok_count += 1 print(f" [OK] {msg}") def fail(msg: str) -> None: bad.append(msg) print(f" [FAIL] {msg}") def read(*parts: str) -> str: p = os.path.join(ROOT, *parts) return io.open(p, encoding="utf-8").read() if os.path.exists(p) else "" # ── 1. 表格列数 ──────────────────────────────────────── def check_tables(name: str, text: str) -> None: lines = text.split("\n") bad_rows = 0 i = 0 while i < len(lines): if lines[i].strip().startswith("|") and i + 1 < len(lines) \ and re.match(r"^\|[\s:|-]+\|$", lines[i + 1].strip()): n = lines[i].count("|") j = i + 2 while j < len(lines) and lines[j].strip().startswith("|"): if lines[j].count("|") != n: bad_rows += 1 j += 1 i = j else: i += 1 if bad_rows: fail(f"{name}:{bad_rows} 行表格列数不一致") else: ok(f"{name}:表格列数一致") # ── 2. 接口数逐节核对 ────────────────────────────────── def check_api_count() -> int: s = read("docs", "03-接口设计.md") if not s: fail("找不到 03-接口设计.md") return 0 sec = s[s.index("## §4"):s.index("## §5")] parts = re.split(r"^(### 4\.\d+ .+)$", sec, flags=re.M) claim_sum = actual_sum = 0 mismatches = [] i = 1 while i < len(parts) - 1: title, body = parts[i].strip(), parts[i + 1] i += 2 m = re.search(r"[((](\d+)[))]\s*$", title) claim = int(m.group(1)) if m else None # ⚠️ 允许 ⭐ 出现在方法之前(| ⭐ GET | ... |) actual = len(re.findall(r"^\|\s*(?:⭐\s*)?(GET|POST|PUT|DELETE|PATCH)\s*\|", body, re.M)) # 索引节(交叉引用别处已计数的接口),不重复计入 if "索引" in title or "新增" in title: continue if claim is None: continue claim_sum += claim actual_sum += actual if claim != actual: mismatches.append(f"{title[:30]} 声称{claim} 实际{actual}") if mismatches: fail(f"03 接口数逐节不一致:{'; '.join(mismatches)}") elif claim_sum == actual_sum: ok(f"03 接口数逐节一致 = {actual_sum}") else: fail(f"03 接口数:声称 {claim_sum} vs 实际 {actual_sum}") return actual_sum # ── 3. 错误码 ────────────────────────────────────────── def check_error_codes() -> int: s = read("docs", "03-接口设计.md") sec = s[s.index("## §15"):] codes = set(re.findall( r"^\|\s*(?:⭐\s*)?" + re.escape(BT) + r"(\d{4,5})" + re.escape(BT) + r"\s*\|", sec, re.M)) total = len(codes) + 1 # + 成功码 0 claimed = re.search(r"本节共 \*\*(\d+) 个\*\*", s) if claimed and int(claimed.group(1)) != total: fail(f"03 错误码:实际 {total} vs 文档声称 {claimed.group(1)}") else: ok(f"03 错误码 = {total}") return total # ── 4. 跨文档关键数字 ────────────────────────────────── def check_cross_doc(api: int, err: int) -> None: docs = { "PRD": read("docs", "数据信息填报系统PRD.md"), "03": read("docs", "03-接口设计.md"), "05": read("docs", "05-开发任务拆解.md"), "README": read("README.md"), "交接摘要": read("docs", "交接摘要.md"), } checks = [ ("接口", str(api)), ("错误码", str(err)), ("任务", "100"), ("人日", "387"), ] for label, value in checks: hit = [n for n, t in docs.items() if value in t] if len(hit) >= 3: ok(f"{label} {value} 出现于 {len(hit)} 份文档") else: fail(f"{label} {value} 仅出现于 {hit}") # ── 5. 原型屏数 ──────────────────────────────────────── def check_screens() -> None: total = 0 for f in ["student-app.html", "teacher-app.html", "admin-web.html"]: s = read("docs", "prototype", f) total += len(re.findall(r" int: print("=" * 66) print(" 文档一致性校验") print("=" * 66) print("\n[1] 表格列数") for f in ["README.md"]: check_tables(f, read(f)) for f in os.listdir(D): if f.endswith(".md"): check_tables(f, read("docs", f)) print("\n[2] 接口数(逐节核对)") api = check_api_count() print("\n[3] 错误码") err = check_error_codes() print("\n[4] 跨文档关键数字") check_cross_doc(api, err) print("\n[5] 原型屏数") check_screens() print("\n" + "=" * 66) if bad: print(f" 结果: {ok_count} 项通过,{len(bad)} 项失败") for b in bad: print(f" - {b}") else: print(f" 结果: {ok_count} 项全部通过") print("=" * 66) return 1 if bad else 0 if __name__ == "__main__": raise SystemExit(main())