工程(能力清单改版——验证账本改实现指导地图): 能力清单定位改实现指导+阅读导航+义务声明,只宣布「要验证什么」不记验证状态(落地账归改动—验收对照表+合回前独立审计)——game/tests/README.md 八张能力表瘦身四列(能力|系统|真实入口|代表用例,删「最后验证批次」「缺口」两列,存量行内容保留不回溯验证)、引区块改版(义务清单定位+写卡当场入表+实现照单落名+收尾核对,废「滞后一个迭代」),覆盖域表保留现状不动;test-conventions §8.3 维护锚整段改写、§8.9 三层模型中层「覆盖索引」改「义务清单」并同步法则与机器闸句;tools/capability_audit.py 判据随改(CAP_COLS 收窄四列含系统列非空、删「最后验证批次=真实 commit」检查、代表用例与真实入口改双通道=代码在或卡在 reports/需求卡片*.md——两头都无=红、条件闸行豁免保留代表用例口径);tools/pipeline_gate.py 对照表能力行收紧(「新增:<能力名>」同样要求该能力行已在清单在场——写卡与入表同 commit)。新判据经造红探针孤立验证(两头无=红/卡在=绿/代码在=绿;新增不在表=红)

This commit is contained in:
DKChan committed 2026-10-29 04:02:20 +08:00
1 parent fafb4fee0a
commit 2a087c9ead
4 files changed
+136 -139

No files matched your search

+34 -46
View File
@@ -1,18 +1,19 @@
#!/usr/bin/env python3
r"""capability_audit.py — 能力清单、覆盖域表与 GDD 纯度机检(文档域闸件)
职责(test-conventions §8.9 三层模型的机检半边;规则正文=test-conventions §8.9+doc-conventions §5):
职责(test-conventions §8.9 三层模型的机检半边;规则正文=test-conventions §8.3+§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 每篇一行;状态∈{已覆盖,部分覆盖,未覆盖};非未覆盖行的「能力行」非空且每项都能
C 能力行结构:game/tests/README.md 能力表四列齐、逐行四格非空。
D 代表用例可核(双通道):能力行「代表用例」的每条 `文件:函数`——代码在(文件在 run_all.gd 套件
manifest 内且该文件有 `func 函数名(`)或卡在(该文件名或函数名能在 reports/需求卡片*.md 中找到,
供写卡时当场入表的计划用例锚定);两头都无=红(清单挂了无凭证义务)。
E 真实入口可核(双通道):能力行「真实入口」的每个 `类.方法()`——产品码(game/ 除 tests//probe//sim/)
有 `func 方法(`,或该方法名能在 reports/需求卡片*.md 中找到(计划入口同 D 口径);两头都无=红。
F 覆盖域表:systems 每篇一行;状态∈{已覆盖,部分覆盖,未覆盖};非未覆盖行的「能力行」非空且每项都能
对上能力表里的行名;部分/未覆盖行的缺口非空且指待办清单。
条件闸行豁免(代表用例含「条件闸」字样)不参与 D/E。
自检四道(防扫描面空转假绿):系统文档数、能力行数、manifest 套件数、覆盖表行数任一为零 ⇒ exit 2。
@@ -23,7 +24,6 @@ exit:0=绿/1=有红项(--strict)/2=扫描面异常
"""
import argparse
import re
import subprocess
import sys
from pathlib import Path
@@ -32,12 +32,13 @@ GDD_DIR = REPO / "design" / "gdd"
SYS_DIR = GDD_DIR / "systems"
README = REPO / "game" / "tests" / "README.md"
RUN_ALL = REPO / "game" / "tests" / "run_all.gd"
CARDS_DIR = REPO / "reports"
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 = ("能力", "真实入口", "代表用例", "最后验证批次", "缺口")
CAP_COLS = ("能力", "系统", "真实入口", "代表用例")
COV_COLS = ("系统文档", "能力行", "状态", "缺口")
STATUS_VALUES = ("已覆盖", "部分覆盖", "未覆盖")
@@ -49,7 +50,6 @@ ACCEPT_EXEMPT = {
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 = []
@@ -129,12 +129,11 @@ def product_funcs():
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 cards_corpus():
"""reports/需求卡片*.md 全文拼接(双通道的「卡在」锚面);无卡返回空串。"""
if not CARDS_DIR.is_dir():
return ""
return "\n".join(read(p) for p in sorted(CARDS_DIR.glob("需求卡片-*.md")))
def check_purity():
@@ -168,7 +167,7 @@ def capability_rows():
selfcheck.append("game/tests/README.md 缺失")
return [], []
lines = read(README).splitlines()
cap_tables = list(tables(lines, ("真实入口", "代表用例", "最后验证批次")))
cap_tables = list(tables(lines, ("真实入口", "代表用例")))
cov_tables = list(tables(lines, ("系统文档", "能力行", "状态")))
if not cap_tables:
selfcheck.append("README 能力表未找到")
@@ -177,7 +176,7 @@ def capability_rows():
return cap_tables, cov_tables
def check_capability_rows(cap_tables, suites, funcs):
def check_capability_rows(cap_tables, suites, funcs, cards):
total = 0
names = []
suite_by_base = {}
@@ -202,17 +201,16 @@ def check_capability_rows(cap_tables, suites, funcs):
red(README.relative_to(REPO), no, "能力行结构",
"「%s」行格数不足或空列(%s)" % (name[:16], "/".join(empty) or "列数"))
continue
if "条件闸" in short("代表用例") or "条件闸" in short("缺口"):
if "条件闸" in short("代表用例"):
continue
check_cases(no, name, short("代表用例"), suite_by_base, funcs)
check_entry(no, name, short("真实入口"), funcs)
check_hash(no, name, short("最后验证批次"))
check_cases(no, name, short("代表用例"), suite_by_base, funcs, cards)
check_entry(no, name, short("真实入口"), funcs, cards)
if total == 0:
selfcheck.append("能力表零数据行")
return names
def check_cases(no, name, cell, suite_by_base, funcs):
def check_cases(no, name, cell, suite_by_base, funcs, cards):
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])
@@ -224,37 +222,27 @@ def check_cases(no, name, cell, suite_by_base, funcs):
"「%s」格格式应为 `文件.gd:函数`:%s" % (name[:16], chunk[:40]))
continue
fname, fn = m.groups()
# 双通道(test-conventions §8.9):代码在(套件 manifest 内且 func 在文件里)
# 或卡在(文件名/函数名见于 reports/需求卡片*.md——写卡时当场入表的计划用例)。
hits = suite_by_base.get(fname, [])
if not hits:
code_ok = bool(hits) and any(
re.search(r"^(?:static\s+)?func\s+%s\s*\(" % re.escape(fn), read(RUN_ALL.parent / rel), re.M)
for rel in hits)
card_ok = bool(cards) and (fname in cards or fn in cards)
if not code_ok and not card_ok:
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))
"「%s」%s 无凭证——代码面(manifest+func)与需求卡片均未锚定" % (name[:16], chunk[:40]))
def check_entry(no, name, cell, funcs):
def check_entry(no, name, cell, funcs, cards):
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:
if method not in funcs and not (cards and method in cards):
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)))
"「%s」方法 %s() 无凭证——产品码无定义且需求卡片未载" % (name[:16], method))
def check_coverage(cov_tables, names):
@@ -324,7 +312,7 @@ def main():
funcs = product_funcs()
if not funcs:
selfcheck.append("产品码零 `func`")
names = check_capability_rows(cap_tables, suites, funcs)
names = check_capability_rows(cap_tables, suites, funcs, cards_corpus())
check_coverage(cov_tables, names)
if selfcheck:
+16 -5
View File
@@ -582,10 +582,19 @@ def check_ledger(text, path, when, issues, root):
issues.append((str(path), i + 1, "能力行格式",
"「能力行」格须写 既有:<能力名>/新增:<能力名>:%s"
% seg[:24]))
elif m.group(1) == "既有" and m.group(2).strip("`").strip() not in cap_names:
issues.append((str(path), i + 1, "能力行不可核",
"既有能力「%s」不在 game/tests/README.md 能力表"
% m.group(2).strip("`").strip()[:24]))
continue
# 2026-10-29 能力清单改版:写卡与入表同 commit——「新增:」同样要求该能力行
# 已在 game/tests/README.md 能力表在场(写卡时当场入表,test-conventions §8.3)。
cap_name = m.group(2).strip("`").strip()
if cap_name not in cap_names:
if m.group(1) == "新增":
issues.append((str(path), i + 1, "新增能力未入表",
"新增能力「%s」不在 game/tests/README.md 能力表——"
"写卡须同批把能力行写入能力清单" % cap_name[:24]))
else:
issues.append((str(path), i + 1, "能力行不可核",
"既有能力「%s」不在 game/tests/README.md 能力表"
% cap_name[:24]))
if rows == 0:
issues.append((str(path), header_line, "验收对照表零行",
"表内没有数据行"))
@@ -601,7 +610,9 @@ def check_cards(root, issues):
C2 判据:全文既无「设计事实源」也无「唯一事实源」者红(两种写法都与库内实态对齐)。
C3 判据:有「改动—验收对照表」节;表头含需求点/改动/验收测试/真实入口+证明列
(2026-10-28 起换代为「判据绑定面」列,并禁「撤销证明/变异证明」列);
至少一行;必填格非空;全文含「验收审计」声明。语义覆盖不在此判。
能力行列(2026-10-29 起)既有与新增的名称都须在 game/tests/README.md 能力表在场
(新增=写卡同批入表,test-conventions §8.3);至少一行;必填格非空;全文含「验收审计」声明。
语义覆盖不在此判。
"""
dirn = root / CARD_DIR
if not dirn.is_dir():