工程(能力审计工具): capability_audit.py 入库——七组机检覆盖三层模型的机检半边,本批不接闸

检查组:A 文档纯度(design/gdd 全树禁 .gd/.py/.sh 路径与断言名,围栏块跳过,.tres 表索引放行)/B 系统文档验收标准非空(豁免两件具名附理由)/C 能力行六列结构与格非空/D 代表用例三核(文件存在、在 run_all.gd manifest、func 函数名可达)/E 真实入口方法在产品码有定义(static func 兼容)/F 最后验证批次为真实 commit/G 覆盖域表 25 篇逐篇登记、状态三档、非未覆盖行能力行可回指能力表、部分/未覆盖行缺口指待办清单。自检四道防扫描面空转(文档数/能力行数/套件数/覆盖表行数任一为零 exit 2)。当前实测:能力行 59 全过、覆盖域表 25 行全过,仅余纯度红 124 处(GDD 存量实现细节,归清理批)。接闸与基线登记待清理批之后单独批执行。
This commit is contained in:
DKChan committed 2026-10-29 01:54:45 +08:00
1 parent 5377b2ce2e
commit ae3c99e34e
1 file changed
+348
+348
View File
@@ -0,0 +1,348 @@
#!/usr/bin/env python3
r"""capability_audit.py — 能力清单、覆盖域表与 GDD 纯度机检(文档域闸件)
职责(test-conventions §8.9 三层模型的机检半边;规则正文=test-conventions §8.9+doc-conventions §5):
A 文档纯度:design/gdd 全树现行文档禁写实现细节——代码路径(.gd/.py/.sh)、测试文件名、断言名;
唯一例外=配置文件索引(.tres 表路径与表名)。围栏代码块内不扫。
B 验收标准:design/gdd/systems/** 每篇须有非空的「## 验收标准」节(豁免件具名在册,附理由)。
C 能力行结构:game/tests/README.md 能力表六列齐、逐行六格非空。
D 代表用例可核:能力行「代表用例」的每条 `文件:函数`——文件存在、在 run_all.gd 套件 manifest 内、
该文件有 `func 函数名(`。
E 真实入口可核:能力行「真实入口」的每个 `类.方法()`,方法名须在产品码(game/ 除 tests//probe//sim/)
有 `func 方法(`。
F 最后验证批次:真实 commit(git cat-file 可解析);条件闸行豁免。
G 覆盖域表:systems 每篇一行;状态∈{已覆盖,部分覆盖,未覆盖};非未覆盖行的「能力行」非空且每项都能
对上能力表里的行名;部分/未覆盖行的缺口非空且指待办清单。
自检四道(防扫描面空转假绿):系统文档数、能力行数、manifest 套件数、覆盖表行数任一为零 ⇒ exit 2。
用法:
python tools/capability_audit.py # 报告模式(红项只打印,不改退出码)
python tools/capability_audit.py --strict # 有红项时 exit 1(供门禁)
exit:0=绿/1=有红项(--strict)/2=扫描面异常
"""
import argparse
import re
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
GDD_DIR = REPO / "design" / "gdd"
SYS_DIR = GDD_DIR / "systems"
README = REPO / "game" / "tests" / "README.md"
RUN_ALL = REPO / "game" / "tests" / "run_all.gd"
PURITY_RE = re.compile(r"\.(gd|py|sh)\b")
ASSERT_RE = re.compile(r"check_quiet\s*\(|\bcheck\s*\(")
FENCE_RE = re.compile(r"^\s*```")
CAP_COLS = ("能力", "真实入口", "代表用例", "最后验证批次", "缺口")
COV_COLS = ("系统文档", "能力行", "状态", "缺口")
STATUS_VALUES = ("已覆盖", "部分覆盖", "未覆盖")
# 验收标准节豁免:无可写验收面的在册件(逐件附理由;新豁免须经用户裁)
ACCEPT_EXEMPT = {
"锻造工坊.md": "工程化系统,验收面由天赋树与元进度两篇承接,本批迁移实现事实后复核",
"生涯.md": "P6 后置未实装,无现成可观测面;实装批补验收标准节",
}
CASE_RE = re.compile(r"([A-Za-z0-9_\-]+\.gd):([A-Za-z_][A-Za-z0-9_]*)")
ENTRY_RE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\s*\(\)")
HASH_RE = re.compile(r"^`?([0-9a-f]{7,40})`?$")
reds = []
selfcheck = []
def red(path, line, check, msg):
reds.append((str(path), line, check, msg))
def read(path):
return path.read_text(encoding="utf-8")
def cells(raw):
return [c.strip() for c in raw.strip().strip("|").split("|")]
def is_sep(cs):
return all(set(c) <= set(":- ") for c in cs if c)
def tables(lines, required):
"""产出国标表格:(header_cells, [(源行号, cells)]),表头须含 required 全部列关键词。"""
i = 0
while i < len(lines):
if lines[i].strip().startswith("|"):
block = []
j = i
while j < len(lines) and lines[j].strip().startswith("|"):
block.append((j + 1, lines[j]))
j += 1
if len(block) >= 2:
header = cells(block[0][1])
if all(any(req in c for c in header) for req in required):
rows = []
for no, raw in block[1:]:
cs = cells(raw)
if is_sep(cs):
continue
rows.append((no, cs))
yield header, rows
i = j
else:
i += 1
def col(header, key):
return next((k for k, c in enumerate(header) if key in c), None)
def strip_fences(text):
out, inside = [], False
for ln in text.splitlines():
if FENCE_RE.match(ln):
inside = not inside
out.append("")
continue
out.append("" if inside else ln)
return out
def system_docs():
return sorted(SYS_DIR.rglob("*.md"))
def manifest():
return re.findall(r'"path":\s*"res://tests/([^"]+)"', read(RUN_ALL))
def product_funcs():
names = set()
for p in (REPO / "game").rglob("*.gd"):
rel = p.relative_to(REPO).as_posix()
if rel.startswith(("game/tests/", "game/probe/", "game/sim/")):
continue
names.update(re.findall(r"^(?:static\s+)?func\s+([A-Za-z_]\w*)\s*\(", read(p), re.M))
return names
def commit_ok(short, cache):
if short not in cache:
r = subprocess.run(["git", "-C", str(REPO), "cat-file", "-e", short + "^{commit}"],
capture_output=True)
cache[short] = r.returncode == 0
return cache[short]
def check_purity():
docs = sorted(GDD_DIR.rglob("*.md"))
if not docs:
selfcheck.append("design/gdd 下无文档")
return
for doc in docs:
for no, ln in enumerate(strip_fences(read(doc)), 1):
for m in PURITY_RE.finditer(ln):
red(doc.relative_to(REPO), no, "纯度", "代码路径 `%s`(禁写实现细节)" % m.group(0))
m = ASSERT_RE.search(ln)
if m:
red(doc.relative_to(REPO), no, "纯度", "断言名 `%s`(禁写实现细节)" % m.group(1))
def check_accept_sections():
for doc in system_docs():
text = read(doc)
m = re.search(r"^## 验收标准[^\n]*\n(.*?)(?=^## |\Z)", text, re.M | re.S)
body = (m.group(1).strip() if m else "")
if not body and doc.name not in ACCEPT_EXEMPT:
red(doc.relative_to(REPO), 1, "验收标准",
"缺非空「## 验收标准」节" if not m else "「## 验收标准」节为空")
elif doc.name in ACCEPT_EXEMPT and body:
red(doc.relative_to(REPO), 1, "验收标准", "豁免件已有验收标准节,请移除豁免")
def capability_rows():
if not README.exists():
selfcheck.append("game/tests/README.md 缺失")
return [], []
lines = read(README).splitlines()
cap_tables = list(tables(lines, ("真实入口", "代表用例", "最后验证批次")))
cov_tables = list(tables(lines, ("系统文档", "能力行", "状态")))
if not cap_tables:
selfcheck.append("README 能力表未找到")
if not cov_tables:
selfcheck.append("README 覆盖域表未找到")
return cap_tables, cov_tables
def check_capability_rows(cap_tables, suites, funcs):
total = 0
names = []
suite_by_base = {}
for rel in suites:
suite_by_base.setdefault(Path(rel).name, []).append(rel)
for header, rows in cap_tables:
idx = {}
for key in CAP_COLS:
k = col(header, key)
if k is None:
red(README.relative_to(REPO), 1, "能力行结构", "缺「%s」列" % key)
break
idx[key] = k
else:
for no, cs in rows:
total += 1
short = lambda k: (cs[idx[k]] if idx[k] < len(cs) else "")
name = short("能力").strip("`")
names.append(name)
empty = [k for k in CAP_COLS if not short(k)]
if len(cs) < max(idx.values()) + 1 or empty:
red(README.relative_to(REPO), no, "能力行结构",
"「%s」行格数不足或空列(%s)" % (name[:16], "/".join(empty) or "列数"))
continue
if "条件闸" in short("代表用例") or "条件闸" in short("缺口"):
continue
check_cases(no, name, short("代表用例"), suite_by_base, funcs)
check_entry(no, name, short("真实入口"), funcs)
check_hash(no, name, short("最后验证批次"))
if total == 0:
selfcheck.append("能力表零数据行")
return names
def check_cases(no, name, cell, suite_by_base, funcs):
chunks = [c.strip().strip("`") for c in re.split(r"[;;]", cell) if c.strip()]
if not chunks:
red(README.relative_to(REPO), no, "代表用例", "「%s」无代表用例" % name[:16])
return
for chunk in chunks:
m = CASE_RE.search(chunk)
if not m:
red(README.relative_to(REPO), no, "代表用例",
"「%s」格格式应为 `文件.gd:函数`:%s" % (name[:16], chunk[:40]))
continue
fname, fn = m.groups()
hits = suite_by_base.get(fname, [])
if not hits:
red(README.relative_to(REPO), no, "代表用例",
"「%s」%s 不在 run_all.gd 套件 manifest" % (name[:16], fname))
continue
if not any(re.search(r"^(?:static\s+)?func\s+%s\s*\(" % re.escape(fn), read(RUN_ALL.parent / rel), re.M)
for rel in hits):
red(README.relative_to(REPO), no, "代表用例",
"「%s」%s 无 `func %s(`" % (name[:16], fname, fn))
def check_entry(no, name, cell, funcs):
tokens = ENTRY_RE.findall(cell)
if not tokens:
red(README.relative_to(REPO), no, "真实入口", "「%s」无可核的 `类.方法()`" % name[:16])
return
for _, method in tokens:
if method not in funcs:
red(README.relative_to(REPO), no, "真实入口",
"「%s」方法 %s() 在产品码无定义" % (name[:16], method))
def check_hash(no, name, cell):
m = HASH_RE.match(cell.strip())
if not m:
red(README.relative_to(REPO), no, "最后验证批次",
"「%s」须为 commit 短哈希:%s" % (name[:16], cell[:24]))
return
if not commit_ok(m.group(1), {}):
red(README.relative_to(REPO), no, "最后验证批次",
"「%s」哈希 %s 不解析为 commit" % (name[:16], m.group(1)))
def check_coverage(cov_tables, names):
want = {d.name for d in system_docs()}
if not want:
selfcheck.append("systems 下无系统文档")
return
total, seen = 0, {}
for header, rows in cov_tables:
idx = {}
ok = True
for key in COV_COLS:
k = col(header, key)
if k is None:
red(README.relative_to(REPO), 1, "覆盖域表", "缺「%s」列" % key)
ok = False
break
idx[key] = k
if not ok:
continue
for no, cs in rows:
total += 1
short = lambda k: (cs[idx[k]] if idx[k] < len(cs) else "")
doc = Path(short("系统文档").strip("`").strip()).name
if not doc:
red(README.relative_to(REPO), no, "覆盖域表", "系统文档格为空")
continue
if doc in seen:
red(README.relative_to(REPO), no, "覆盖域表", "%s 重复登记(见行 %d)" % (doc, seen[doc]))
seen[doc] = no
status = short("状态").strip("`").strip()
if status not in STATUS_VALUES:
red(README.relative_to(REPO), no, "覆盖域表",
"%s 状态须为 已覆盖/部分覆盖/未覆盖:%s" % (doc, status[:16]))
continue
caps = [c.strip() for c in re.split(r"[、;;]", short("能力行")) if c.strip()]
gap = short("缺口").strip()
if status != "未覆盖" and not caps:
red(README.relative_to(REPO), no, "覆盖域表", "%s 非未覆盖但能力行为空" % doc)
for cap in caps:
cap = cap.strip("`")
if cap in ("—", "-"):
continue
if not any(cap in n or n in cap for n in names):
red(README.relative_to(REPO), no, "覆盖域表",
"%s 的能力行「%s」在能力表中不存在" % (doc, cap[:20]))
if status != "已覆盖" and (not gap or "待办清单" not in gap):
red(README.relative_to(REPO), no, "覆盖域表",
"%s 为%s,缺口须非空并指待办清单" % (doc, status))
if total == 0:
selfcheck.append("覆盖域表零数据行")
for doc in sorted(want - set(seen)):
red(README.relative_to(REPO), 1, "覆盖域表", "%s 未在覆盖域表登记" % doc)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--strict", action="store_true")
args = ap.parse_args()
check_purity()
check_accept_sections()
cap_tables, cov_tables = capability_rows()
suites = manifest()
if not suites:
selfcheck.append("run_all.gd manifest 零套件")
funcs = product_funcs()
if not funcs:
selfcheck.append("产品码零 `func`")
names = check_capability_rows(cap_tables, suites, funcs)
check_coverage(cov_tables, names)
if selfcheck:
print("capability_audit 扫描面异常:")
for s in selfcheck:
print(" ! " + s)
print("| ── 能力审计:系统文档 %d 篇 · 能力行 %d · 套件 %d · 覆盖域表 %d 行 · 红项 %d"
% (len(system_docs()), len(names), len(suites),
sum(len(r) for _, r in cov_tables), len(reds)))
for path, line, check, msg in reds:
print(" ✗ [%s] %s:%d %s" % (check, path, line, msg))
if selfcheck:
return 2
if reds and args.strict:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())