工程(能力审计工具): 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:
1 parent
5377b2ce2e
commit
ae3c99e34e
1 file changed
+348
@@ -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())
|
||||
Reference in new issue
Block a user