iter41-卡2:呈现层结构收敛批:67清+2豁免+2展平改善——拆私有族+六状态件+ui_kit收敛+束收拢+print删;含u14补做、并master并案与收尾产出物

This commit is contained in:
DKChan committed 2026-10-29 00:14:52 +08:00
1 parent bc75dab615
commit 7beb9bfdd5
94 files changed
+3417 -5314

No files matched your search

+3 -3
View File
@@ -23,8 +23,8 @@ r"""doc_link_audit.py — design 文档链接健康检查
python tools/doc_link_audit.py <file.md> ... # 只扫指定文件
python tools/doc_link_audit.py --json # 机器可读
口径:只扫 design/gdd/——design/art、reports/
(历史素材/提案)不在扫描域,但作为链接目标被校验。排除 ``` 围栏代码块与 ; 开头注释行。
口径:扫 design/gdd/ 与 design/art/(2026-10-28 美术族成文整备批收编)——reports/
(提案过程件)不在扫描域,但作为链接目标被校验。排除 ``` 围栏代码块与 ; 开头注释行。
"""
import argparse
import json
@@ -32,7 +32,7 @@ import re
import sys
from pathlib import Path
DEFAULT_DIRS = ["design/gdd"]
DEFAULT_DIRS = ["design/gdd", "design/art"]
PROTO_PREFIX = "game"
ALLOWED_EXTS = ("md", "yaml", "yml", "tres", "gd", "py", "json", "csv")
+19 -1
View File
@@ -2,7 +2,7 @@
r"""gdd_lint.py — GDD lean 格式门禁
用法:
python tools/gdd_lint.py # 检查 design/gdd/ 根层 GDD + roadmap.md(默认范围)
python tools/gdd_lint.py # 检查 design/gdd/ 根层 GDD + design/art/ + roadmap.md(默认范围)
python tools/gdd_lint.py --changelog-only # 只跑 R4 变更记录节禁令,扫描面扩到全 design/ 树
python tools/gdd_lint.py <file.md> ... # 检查指定文件
python tools/gdd_lint.py --json # 机器可读输出(供 gate/CI 使用)
@@ -45,6 +45,13 @@ RULE_EXEMPT = {
"术语表.md": {"R5"},
}
# 美术域收编(2026-10-28 美术族成文整备批):design/art/ 为高密度规格/清单/提示词操作单体裁,
# 体裁类规则(R1 段长/R5 流程图/R7 括号跨度/R8 规则表/R9 节点行数)不适用、豁免防误报;
# R11 的锚表是 GDD 行协议/格面资源专属计数,对美术域同样不适用。
# 跨体裁有效面照常执法:R3 修订注记、R4 变更记录节、R6 表格列数。
ART_DOMAIN_EXEMPT = {"R1", "R5", "R7", "R8", "R9", "R11"}
ART_DIR = "design/art"
DEFAULT_DIR = "design/gdd"
DEFAULT_EXCLUDE = {"game-concept.md", "game-pillars.md", "systems-index.md"}
# R4 变更记录节标题(2026-10-19 用户裁「沿革只进 git」——原「必须有版本历史节」反转为此禁令,
@@ -171,7 +178,11 @@ def lint_file(path):
# R1 段落长度
fname = Path(path).name
ex = RULE_EXEMPT.get(fname, set()) # 规则粒度分域豁免(见 RULE_EXEMPT 注)
if str(path).replace("\\", "/").startswith(ART_DIR.replace("\\", "/") + "/"):
ex = ex | ART_DOMAIN_EXEMPT
for p in paras:
if "R1" in ex:
break
if len(p) > MAX_PARA:
issues.append(f"R1 散文段 {len(p)} 字(>{MAX_PARA}): {p[:50]}…")
@@ -249,6 +260,7 @@ def lint_file(path):
# R11 计数词漂移对照(锚表 CURRENT_COUNTS;轨迹措辞行豁免——变更记录节本身归 R4 禁设)
in_code11 = False
r11_active = "R11" not in ex
for raw in text.splitlines():
stripped = raw.strip()
if stripped.startswith("```"):
@@ -256,6 +268,8 @@ def lint_file(path):
continue
if in_code11 or stripped.startswith(";") or R11_TRACE_RE.search(raw):
continue
if not r11_active:
continue
# yaml/yml 的 # 行内注释是登记轨迹,不入正文扫描
line = raw.split("#", 1)[0] if str(path).endswith((".yaml", ".yml")) else raw
for pat, fix in CURRENT_COUNTS:
@@ -296,6 +310,10 @@ def main():
if p.name not in DEFAULT_EXCLUDE
and not re.search(r"review|report|handoff", p.name, re.I)
)
targets += sorted(
p for p in Path(ART_DIR).glob("*.md")
if not re.search(r"review|report|handoff", p.name, re.I)
)
if not targets:
print(f"未找到待检文件({DEFAULT_DIR}/)", file=sys.stderr)
sys.exit(2)
+29 -10
View File
@@ -52,6 +52,8 @@ LEDGER_RULE_DATE = (2026, 10, 21)
LEDGER_HEADING_RE = re.compile(r"^#{2,4}\s*.*(验收对照表|改动[—\-]验收)")
LEDGER_REQUIRED = ("需求点", "改动", "验收测试", "真实入口")
LEDGER_PROOF_MARKS = ("撤销", "证明")
BINDING_RULE_DATE = (2026, 10, 28) # req-card §8:换代为判据绑定面列,撤销/变异证明列同日禁列
BANNED_PROOF_MARKS = ("撤销证明", "变异证明")
AUDIT_MARK = "验收审计"
DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}")
@@ -451,7 +453,7 @@ def card_date(path):
return tuple(int(x) for x in found[-1].split("-"))
def check_ledger(text, path, issues):
def check_ledger(text, path, when, issues):
"""C3 改动—验收对照表结构检查(生效 2026-10-21)。
只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空。
@@ -491,13 +493,29 @@ def check_ledger(text, path, issues):
"缺「%s」列" % name))
return
idx[name] = hit
proof_hit = next((k for k, cell in enumerate(header)
if any(mark in cell for mark in LEDGER_PROOF_MARKS)), None)
if proof_hit is None:
issues.append((str(path), header_line, "验收对照表缺列",
"缺「撤销证明」列"))
return
idx["证明"] = proof_hit
banned = next((cell for cell in header
if any(mark in cell for mark in BANNED_PROOF_MARKS)), None)
binding_hit = next((k for k, cell in enumerate(header)
if "判据绑定面" in cell), None)
if when >= BINDING_RULE_DATE:
if banned is not None:
issues.append((str(path), header_line, "验收对照表禁列",
"「%s」列已随 2026-10-24/28 两裁禁列(req-card §8.4,"
"验收断言绑真实入口+判据绑定面)" % banned))
return
if binding_hit is None:
issues.append((str(path), header_line, "验收对照表缺列",
"缺「判据绑定面」列(2026-10-28 判据口径换代,req-card §8)"))
return
idx["绑定面"] = binding_hit
else:
proof_hit = next((k for k, cell in enumerate(header)
if any(mark in cell for mark in LEDGER_PROOF_MARKS)), None)
if proof_hit is None:
issues.append((str(path), header_line, "验收对照表缺列",
"缺「撤销证明」列"))
return
idx["证明"] = proof_hit
rows = 0
for i in range(header_line, len(lines)):
@@ -531,7 +549,8 @@ def check_cards(root, issues):
C1 判据:「关联文档同步面」表内,目标格指向 design/ 或根规则文件、而义务格仍写
「随批」者红(2026-10-17 req-card §6 改口径后,这类点只能列 reports/ 反哺点清单)。
C2 判据:全文既无「设计事实源」也无「唯一事实源」者红(两种写法都与库内实态对齐)。
C3 判据:有「改动—验收对照表」节;表头含需求点/改动/验收测试/真实入口/证明列;
C3 判据:有「改动—验收对照表」节;表头含需求点/改动/验收测试/真实入口+证明列
(2026-10-28 起换代为「判据绑定面」列,并禁「撤销证明/变异证明」列);
至少一行;必填格非空;全文含「验收审计」声明。语义覆盖不在此判。
"""
dirn = root / CARD_DIR
@@ -550,7 +569,7 @@ def check_cards(root, issues):
"须含「设计事实源」或「唯一事实源」声明句(req-card §2 三选一,"
"GDD 节+版本号/样张+art-bible 版本/工程形态证据指认)"))
if when >= LEDGER_RULE_DATE:
check_ledger(text, path, issues)
check_ledger(text, path, when, issues)
if AUDIT_MARK not in text:
issues.append((str(path), 1, "独立验收审计未声明",
"须在卡内写明独立验收审计入口与报告文件(2026-10-21 起,"
-159
View File
@@ -1,159 +0,0 @@
#!/usr/bin/env python3
"""撤销证明自动化工具:一条命令完成「改前绿 → 真撤销复红 → 还原复绿」三连并出回执。
撤销证明的判据(test-conventions §8.4):只撤销该改动、跑能覆盖它的最窄测试、对应断言
必须变红、还原后复绿;撤销后仍绿=断言没钉在改动上,判缺漏。本工具把这三连机械化——
合法性属于「真撤销」(回退本批真实改动),全项目禁做变异类测试(改断言极性、置空状态、
改名键、删调用行等人为制造红=伪造证据)。
用法:
python tools/undo_proof.py --cmd "<最窄测试命令>" --revert-commit <该批提交>
python tools/undo_proof.py --cmd "<最窄测试命令>" --revert-files <f1,f2> --base <rev>
可选 --out <json 路径> 落回执;--timeout <秒> 限制单次测试命令时长(缺省 3600)。
--revert-commit:按该笔提交触过的文件集回退到其父提交版本(改动须已提交在分支上)。
--revert-files+--base:把指定文件回退到 <base>(默认 HEAD)版本——适合改动尚未单独
成笔、混在当前尖里的场景。
测试命令在仓库根以 shell 执行;单套件直跑形态见 game/tests/README.md(测试工具手册)。
流程与退出码:
① 改前基线:测试命令必须绿(红=改前就红,无证明意义,中止)。
② 真撤销:指定文件回退到基准版本 → 重跑命令,必须红(红=断言钉在该改动上)。
③ 还原:文件恢复到 HEAD → 重跑命令,必须绿。
④ 任何一步异常都先还原再退出;三步全过=退出码 0 并打印回执,否则退出码 1。
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
REPO_DEFAULT = Path(__file__).resolve().parent.parent
REPO = REPO_DEFAULT
def run(args: list[str], check: bool = True) -> str:
r = subprocess.run(["git", "-C", str(REPO)] + args,
capture_output=True, text=True, encoding="utf-8", errors="replace",
timeout=60)
if check and r.returncode != 0:
sys.stderr.write("git %s 失败:%s\n" % (" ".join(args), (r.stderr or "").strip()))
sys.exit(1)
return (r.stdout or "").strip()
def run_cmd(cmd: str, timeout: int) -> int:
r = subprocess.run(cmd, shell=True, cwd=str(REPO), capture_output=True,
text=True, encoding="utf-8", errors="replace", timeout=timeout)
if r.returncode != 0:
tail = "\n".join((r.stdout or "").splitlines()[-15:] or ["(无输出)"])
sys.stderr.write(" | 测试命令失败(exit=%d),输出尾部:\n%s\n" % (r.returncode, tail))
return r.returncode
def _in_rev(rev: str, f: str) -> bool:
r = subprocess.run(["git", "-C", str(REPO), "cat-file", "-e", "%s:%s" % (rev, f)],
capture_output=True, timeout=30)
return r.returncode == 0
def apply_rev(rev: str, files: list[str]) -> None:
# 逐文件:基准版本存在 → 恢复到该版本;基准没有(该批新增的文件)→ 从工作区删除。
for f in files:
if _in_rev(rev, f):
run(["checkout", rev, "--", f])
else:
subprocess.run(["git", "-C", str(REPO), "rm", "-q", "-f", "--", f],
capture_output=True, text=True, timeout=30)
def main() -> None:
ap = argparse.ArgumentParser(description="自动三连出撤销证明红绿回执")
ap.add_argument("--cmd", required=True, help="最窄测试命令(shell 形,仓库根执行)")
ap.add_argument("--revert-commit", default=None, help="该批提交:按其触过的文件集回退到父版本")
ap.add_argument("--revert-files", default=None, help="逗号分隔的文件清单(与 --base 配用)")
ap.add_argument("--base", default="HEAD", help="回退基准版本(默认 HEAD)")
ap.add_argument("--timeout", type=int, default=3600, help="单次测试命令时长上限(秒)")
ap.add_argument("--repo", default=None, help="仓库/worktree 路径(缺省=本脚本所在仓库)")
ap.add_argument("--out", default=None, help="回执 JSON 落盘路径")
args = ap.parse_args()
global REPO
REPO = Path(args.repo).resolve() if args.repo else REPO_DEFAULT
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
if bool(args.revert_commit) == bool(args.revert_files):
ap.error("--revert-commit 与 --revert-files 二选一")
if args.revert_commit:
files = [x.strip() for x in run(
["-c", "core.quotepath=false", "show", "--name-only", "--format=",
args.revert_commit]).splitlines() if x.strip()]
base = "%s^" % args.revert_commit
if not files:
sys.stderr.write("[撤销证明] 提交 %s 未触碰任何文件,无可撤销\n" % args.revert_commit)
sys.exit(2)
else:
files = [x.strip() for x in args.revert_files.split(",") if x.strip()]
base = args.base
dirty = run(["-c", "core.quotepath=false", "status", "--porcelain", "--"] + files)
if dirty:
sys.stderr.write("[撤销证明] 指定文件带未提交改动,撤销与还原的基准不清,先提交:\n%s\n" % dirty)
sys.exit(2)
steps: list[dict[str, object]] = []
result = "pass"
def step(name: str, expect: str, code: int) -> bool:
ok = (code == 0) if expect == "绿" else (code != 0)
steps.append({"step": name, "expect": expect, "exit": code, "ok": ok})
print("[撤销证明] %s:期望%s,实测 exit=%d → %s"
% (name, expect, code, "符合" if ok else "不符合"))
return ok
print("[撤销证明] 对象文件 %d 个;基准 %s;命令:%s" % (len(files), base, args.cmd))
try:
if not step("① 改前基线", "绿", run_cmd(args.cmd, args.timeout)):
result = "fail(改前就红——先修绿再做撤销证明)"
else:
apply_rev(base, files)
try:
if not step("② 真撤销复红", "红", run_cmd(args.cmd, args.timeout)):
result = "fail(撤销后仍绿=断言没钉在该改动上,判缺漏)"
else:
apply_rev("HEAD", files)
if not step("③ 还原复绿", "绿", run_cmd(args.cmd, args.timeout)):
result = "fail(还原后未复绿——还原面有残留,查工作区)"
finally:
apply_rev("HEAD", files)
except subprocess.TimeoutExpired:
result = "fail(测试命令超时)"
finally:
# 兜底还原:无论走到哪一步,退出前把对象文件恢复到 HEAD 态
apply_rev("HEAD", files)
residue = run(["-c", "core.quotepath=false", "status", "--porcelain", "--"] + files)
if residue:
result = "fail(退出前工作区仍有对象文件残留改动,手工核对)"
print("─" * 56)
print("[撤销证明] 结论:%s" % result)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
json.dump({"cmd": args.cmd, "base": base, "files": files,
"steps": steps, "result": result}, f, ensure_ascii=False, indent=1)
print("[撤销证明] 回执已落 %s" % args.out)
sys.exit(0 if result == "pass" else 1)
if __name__ == "__main__":
main()