merge: 分支并入 master(推进 84 提交,2026-10-21)——research/ 治理批按现master重取四处冲突
解冲突取法=master 现文为准、本批新增语义打上去,不照抄分支旧文本: - doc-conventions §11:活文档清单与「暂存地非档案库」行取本批新增全文(content-archive 退役内容留底/research/ 无常驻例外/跨会话交接文件禁入库/根 playnote 定性),清单称呼按 docmove 批现文写作「待办清单」,「落档必写接收方」「迭代实现报告」两行照收 master 版(旧日期注记不复活)。 - 08-Alpha版本标准:Alpha 出口裁决与营地五卡入 Alpha 的全部现行行照收 master,仅把 playnote 链接按新位置改指 ../../playnote.md。 - tools/repo_structure_audit.py:ROOT_FILES 取 master 全集(含待办清单/体验验证清单两件根文件)后追加 playnote.md。 - design/research/Alpha未完成事项梳理-2026-10-13.md:modify/delete 冲突按删除解——master 侧自分叉只改过两处指路句(f100e192/281d471e),其结论已由 08 v1.11-v1.12 与待办清单承载,吸收判据今日重核成立。 随批补两处 master 前进后暴露的同步面:AGENTS.md Project Layout 根目录段补 playnote.md 一行(白名单收了新根文件而目录说明未跟上);技能层 batch-closure 两份副本(.zcode 原件→整文件覆盖 .qoder)补「research/ 无常驻例外」的候选呈报口径与 playnote 不入退役候选。
This commit is contained in:
commit
224a947559
132 files changed
+2162
-2565
No files matched your search
@@ -3,9 +3,8 @@
|
||||
"""audit_file_headers.py — 文件头机器闸(code-conventions §1.4.1)。
|
||||
|
||||
文件头 = .gd 文件顶部连续注释区:连续的空行或 # 注释行,首个非注释代码行止。
|
||||
本脚本只实现机械可判子集(两类白名单的人判面见规范正文):
|
||||
本脚本只实现机械可判子集(2026-10-20 缩窄:行数上限 R1 与类别白名单已停用):
|
||||
R0 载体:头注非空时首行必须以 `##` 文档注释开头;
|
||||
R1 行数上限:头注注释行(非空)> 8 行;
|
||||
R2 日期:头注出现日期模式(2026-09-12 / 2026年9月 一类)——日期只属于 git log 与迭代报告;
|
||||
R3 变更叙事词:「迭代N / 本迭代」、批次叙事复合词(本批/拆件批/改造批……)、「迁移到/改名为」
|
||||
一类变更经过。清单从紧防误伤:「结算批」等玩法机制词不在黑名单;
|
||||
@@ -20,8 +19,6 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCAN_DIRS = ["game/core", "game/data", "game/presentation"]
|
||||
|
||||
MAX_HEADER_LINES = 8
|
||||
|
||||
DATE_PAT = re.compile(r"\d{4}\s*[-/.年]\s*\d{1,2}")
|
||||
NARRATIVE_PATS = [
|
||||
(re.compile(r"迭代\s*\d|本迭代"), "变更叙事「迭代N」"),
|
||||
@@ -52,9 +49,6 @@ def audit_path(path):
|
||||
rows = header_lines(text)
|
||||
if rows and not rows[0][1].lstrip().startswith("##"):
|
||||
hits.append((rows[0][0], "R0 头注必须以 ## 文档注释开头"))
|
||||
if len(rows) > MAX_HEADER_LINES:
|
||||
hits.append((rows[0][0], "R1 头注 %d 行 > 上限 %d 行(区间 %d-%d 行)"
|
||||
% (len(rows), MAX_HEADER_LINES, rows[0][0], rows[-1][0])))
|
||||
for lineno, line in rows:
|
||||
if DATE_PAT.search(line):
|
||||
hits.append((lineno, "R2 头注日期"))
|
||||
@@ -77,7 +71,7 @@ def main():
|
||||
if violations:
|
||||
print("[audit_file_headers] FAIL: 文件头存量 %d 处(存量按 code-conventions §1.4.1 白名单清偿)" % violations)
|
||||
return 1
|
||||
print("[audit_file_headers] ok: 头注 ≤8 行且零黑名单命中")
|
||||
print("[audit_file_headers] ok: 头注零黑名单命中(R0 载体/R2 日期/R3 变更叙事)")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""doc_ref_audit.py — 文档内代码引用可核检查(机检)
|
||||
|
||||
职责:现行文档里凡写「某文件:第几行」,就该指得动。本工具核两件事:
|
||||
①路径可解析——目标文件在 git 受控文件清单里存在;
|
||||
②行号在界内——引用的行号不超过目标文件的实际行数。
|
||||
动机:条目与规约常写 `board_view.gd:737` 这类指针,代码前进后行号漂移、文件迁移改名,
|
||||
账面就与现实脱节而无人知(2026-10-21 迭代 32 收尾时实测抓到两例)。
|
||||
|
||||
扫描面:`design/` 现行文档树 + 四份根文件(`待办清单.md`/`体验验证清单.md`/`roadmap.md`/`AGENTS.md`)。
|
||||
排除面:`reports/` 与 `design/research/`——两处是中间文档暂存地(doc-conventions 第 11 节),
|
||||
入内文件按「吸收即死」退役,历史件不回溯改,故不入本闸判据。
|
||||
|
||||
自检二道(防假绿):扫描文件数为零 ⇒ FAIL;抽取到的引用数为零 ⇒ FAIL。
|
||||
(正则被改写失手或路径写错时,检查会静默空转并报绿——与 static_scan 各段同规。)
|
||||
|
||||
判据分两档:
|
||||
红(判 FAIL)=路径不存在/行号超界;
|
||||
建议(只报告)=同名文件有多条路径需要人工定位/`--anchors` 的内容锚没在目标行附近找到。
|
||||
|
||||
用法:
|
||||
python tools/doc_ref_audit.py # 全扫,报告模式
|
||||
python tools/doc_ref_audit.py --strict # 有红项时 exit 1(供门禁)
|
||||
python tools/doc_ref_audit.py --anchors # 追加内容锚核对(只报告,不判红)
|
||||
python tools/doc_ref_audit.py <file.md> ... # 只扫指定文件
|
||||
exit:0=全绿 / 1=有红项(--strict)/ 2=扫描面异常(目录缺失)
|
||||
"""
|
||||
import argparse
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
|
||||
SCAN_DIRS = ["design"]
|
||||
SCAN_EXCLUDE_DIRS = ["design/research"]
|
||||
ROOT_FILES = ["待办清单.md", "体验验证清单.md", "roadmap.md", "AGENTS.md"]
|
||||
|
||||
# 引用形态:路径或文件名 + 扩展名 + 半/全角冒号 + 行号。反引号可包在中间或外面,
|
||||
# 因此先抓到扩展名为止,冒号允许紧跟或在反引号之后。
|
||||
REF_RE = re.compile(
|
||||
r"([\w./一-鿿-]+\.(?:gd|tres|sh|py|json|md|cfg|yml|yaml|txt|png|html))"
|
||||
r"[`”』」]?\s*[::]\s*(\d+)"
|
||||
)
|
||||
# 内容锚:同一行内的反引号片段或「」内文本,取长度 ≥4 的第一个非引用片段
|
||||
ANCHOR_RE = re.compile(r"`([^`\n]{4,60})`|「([^」\n]{4,60})」")
|
||||
ANCHOR_WINDOW = 12
|
||||
# 留档信号(与 doc_term_audit 同口径):同句带日期或退役/注销类说明时,指向已不存在的路径
|
||||
# 属历史叙述而非现行指针,降为建议级报出、不判红。
|
||||
ARCHIVE_RE = re.compile(
|
||||
r"\d{4}-\d{2}-\d{2}|退役|注销|已删|已废除|已废|废除|作废|历史|原名|改名|迁入|迁到|移到|迁至"
|
||||
)
|
||||
|
||||
|
||||
def tracked_files():
|
||||
out = subprocess.run(
|
||||
["git", "-c", "core.quotepath=false", "-C", str(REPO), "ls-files"],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="ignore",
|
||||
)
|
||||
if out.returncode != 0:
|
||||
return None
|
||||
return {l.strip().strip('"').replace("\\", "/") for l in out.stdout.splitlines() if l.strip()}
|
||||
|
||||
|
||||
def collect_docs(explicit):
|
||||
if explicit:
|
||||
return [p for p in explicit if Path(p).is_file()]
|
||||
paths = []
|
||||
for d in SCAN_DIRS:
|
||||
root = REPO / d
|
||||
if not root.is_dir():
|
||||
return None
|
||||
for p in sorted(root.rglob("*.md")):
|
||||
rel = p.relative_to(REPO).as_posix()
|
||||
if any(rel.startswith(x + "/") for x in SCAN_EXCLUDE_DIRS):
|
||||
continue
|
||||
paths.append(rel)
|
||||
for f in ROOT_FILES:
|
||||
if (REPO / f).is_file():
|
||||
paths.append(f)
|
||||
return paths
|
||||
|
||||
|
||||
def resolve(name, doc_rel, index, bybase):
|
||||
"""返回 (命中路径列表, 解析方式)。方式=exact / ambiguous / none。"""
|
||||
name = name.strip("./")
|
||||
if "/" in name:
|
||||
for cand in (
|
||||
os.path.normpath(os.path.join(os.path.dirname(doc_rel), name)).replace("\\", "/"),
|
||||
os.path.normpath(name).replace("\\", "/"),
|
||||
):
|
||||
if cand in index:
|
||||
return [cand], "exact"
|
||||
return [], "none"
|
||||
cands = sorted(bybase.get(name, []))
|
||||
if not cands:
|
||||
return [], "none"
|
||||
if len(cands) == 1:
|
||||
return cands, "exact"
|
||||
return cands, "ambiguous"
|
||||
|
||||
|
||||
def line_count(path, cache):
|
||||
if path not in cache:
|
||||
try:
|
||||
cache[path] = len(io.open(REPO / path, encoding="utf-8", errors="ignore").read().splitlines())
|
||||
except OSError:
|
||||
cache[path] = 0
|
||||
return cache[path]
|
||||
|
||||
|
||||
def anchor_of(line, name):
|
||||
"""取同句里可当内容锚的片段:反引号或「」内、长度 ≥4、且自身不是文件路径。"""
|
||||
for m in ANCHOR_RE.finditer(line):
|
||||
frag = (m.group(1) or m.group(2)).strip()
|
||||
if name.split("/")[-1] in frag:
|
||||
continue
|
||||
if re.search(r"\.(gd|tres|sh|py|json|md|yaml|yml|html|png)$", frag):
|
||||
continue
|
||||
if re.fullmatch(r"[\d:~\-]+", frag):
|
||||
continue
|
||||
return frag
|
||||
return None
|
||||
|
||||
|
||||
def check(doc_rel, index, bybase, cache, anchors=False):
|
||||
reds, advises = [], []
|
||||
text = io.open(REPO / doc_rel, encoding="utf-8", errors="ignore").read()
|
||||
for ln, line in enumerate(text.splitlines(), 1):
|
||||
for m in REF_RE.finditer(line):
|
||||
name = m.group(1).lstrip("①②③④⑤⑥⑦⑧⑨⑩〔[[((「」·、。,,;;")
|
||||
num = int(m.group(2))
|
||||
hits, how = resolve(name, doc_rel, index, bybase)
|
||||
if how == "none":
|
||||
if ARCHIVE_RE.search(line):
|
||||
advises.append("%s:%d 引用已退役件 指 `%s`——同句带日期或退役/注销类说明,按留档处理" % (doc_rel, ln, name))
|
||||
else:
|
||||
reds.append("%s:%d 引用可核/路径不存在 文档指 `%s`,git 受控文件清单里没有这件" % (doc_rel, ln, name))
|
||||
continue
|
||||
if how == "ambiguous":
|
||||
advises.append("%s:%d 同名多路径 指 `%s`——命中 %d 件(%s),需写全路径" % (
|
||||
doc_rel, ln, name, len(hits), ", ".join(hits[:3])))
|
||||
target = hits[0]
|
||||
total = line_count(target, cache)
|
||||
if num > total:
|
||||
if ARCHIVE_RE.search(line):
|
||||
advises.append("%s:%d 行号已越界(留档句) 指 `%s:%d`,目标现 %d 行——同句带退役/日期说明" % (
|
||||
doc_rel, ln, name, num, total))
|
||||
else:
|
||||
reds.append("%s:%d 引用可核/行号超界 指 `%s:%d`,目标文件现只有 %d 行" % (doc_rel, ln, name, num, total))
|
||||
continue
|
||||
if anchors:
|
||||
frag = anchor_of(line, name)
|
||||
if frag:
|
||||
lines = io.open(REPO / target, encoding="utf-8", errors="ignore").read().splitlines()
|
||||
lo, hi = max(0, num - 1 - ANCHOR_WINDOW), min(len(lines), num + ANCHOR_WINDOW)
|
||||
window = "\n".join(lines[lo:hi])
|
||||
if frag not in window:
|
||||
advises.append("%s:%d 内容锚未命中 指 `%s:%d`,同句片段「%s」不在该线 ±%d 行内" % (
|
||||
doc_rel, ln, os.path.basename(name), num, frag[:28], ANCHOR_WINDOW))
|
||||
return reds, advises
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="文档内代码引用可核检查")
|
||||
ap.add_argument("files", nargs="*", help="指定文件;缺省扫 design/ 现行树+四份根文件")
|
||||
ap.add_argument("--strict", action="store_true", help="有红项时 exit 1")
|
||||
ap.add_argument("--anchors", action="store_true", help="追加内容锚核对(只报告)")
|
||||
args = ap.parse_args()
|
||||
|
||||
index = tracked_files()
|
||||
if index is None:
|
||||
print("引用可核 FAIL:git ls-files 取不到受控文件清单(扫描面异常)")
|
||||
return 2
|
||||
docs = collect_docs(args.files)
|
||||
if docs is None:
|
||||
print("引用可核 FAIL:扫描目录缺失(design/ 不在位)")
|
||||
return 2
|
||||
if not docs:
|
||||
print("引用可核 FAIL:扫描面为空——路径写错导致的静默假绿")
|
||||
return 1
|
||||
|
||||
bybase = {}
|
||||
for p in index:
|
||||
bybase.setdefault(p.rsplit("/", 1)[-1], []).append(p)
|
||||
|
||||
cache, all_reds, all_adv = {}, [], []
|
||||
total_refs = 0
|
||||
for doc in docs:
|
||||
reds, adv = check(doc, index, bybase, cache, anchors=args.anchors)
|
||||
total_refs += doc_ref_count(doc)
|
||||
all_reds += reds
|
||||
all_adv += adv
|
||||
if total_refs == 0:
|
||||
print("引用可核 FAIL:抽取到的引用数为零——正则或扫描面失手,属假绿防线")
|
||||
return 1
|
||||
|
||||
for r in all_reds:
|
||||
print(r)
|
||||
print("── 引用可核:扫描 %d 件文档 · 抽取引用 %d 处 · 红项 %d · 建议 %d" % (
|
||||
len(docs), total_refs, len(all_reds), len(all_adv)))
|
||||
if all_adv:
|
||||
for a in all_adv[:25]:
|
||||
print(" (建议)" + a)
|
||||
if len(all_adv) > 25:
|
||||
print(" (建议)…另有 %d 条" % (len(all_adv) - 25))
|
||||
return 1 if (args.strict and all_reds) else 0
|
||||
|
||||
|
||||
def doc_ref_count(doc_rel):
|
||||
text = io.open(REPO / doc_rel, encoding="utf-8", errors="ignore").read()
|
||||
return sum(len(REF_RE.findall(l)) for l in text.splitlines())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+14
-4
@@ -14,8 +14,10 @@ r"""doc_term_audit.py — GDD 退役术语扫描(半自动)
|
||||
python tools/doc_term_audit.py <file.md> ... # 只扫指定文件
|
||||
python tools/doc_term_audit.py --json # 机器可读
|
||||
|
||||
口径:只扫 design/gdd/——design/research
|
||||
(历史提案)、docs/(样例)与 production/ 不在扫描域;词表扩充 = 改 RETIRED_TERMS。
|
||||
口径:扫 design/gdd/ 与 design/art/ 两处文档域 + 四份根文件(`待办清单.md`/`体验验证清单.md`/
|
||||
`roadmap.md`/`AGENTS.md`,2026-10-21 迭代 32 卡①反哺批扩面,用户裁;根文件同属现行文本)
|
||||
——design/research(历史提案)与 reports/(中间文档)不在扫描域;
|
||||
词表扩充 = 改 RETIRED_TERMS(受 doc-conventions §4 词表冻结约束:唯一入口=用户实读反馈并经用户裁决)。
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
@@ -46,6 +48,8 @@ RETIRED_TERMS = [
|
||||
("雾格", "2026-09-14", "(雾格移除)", "不做清单"),
|
||||
("兄弟精", "2026-09-26", "L2 熊地精(bugbear 复用)", "变体名未经批准退役批——用户质问查证后全仓清除"),
|
||||
# —— 用语规范(2026-10-15 用户令):权威=design/gdd/术语表.md「用语规范」A 表,机器闸吸收 ——
|
||||
# 词表冻结:本节禁再新增词条;扩表唯一入口=用户实读反馈并经用户裁决,由非迭代批写入,agent 禁自行增词
|
||||
# (doc-conventions §4「词表冻结」)。
|
||||
# 注意:「随批」「台账」两词本身是上方行级白名单信号(行内出现即归留档类),对其扫描会自豁免,
|
||||
# 故不列入;「收口」「在飞」等照常扫描。术语表「用语规范」节已加入节级豁免(词表自身不命中)。
|
||||
("对标", "2026-10-15", "参照", "用语规范 A 表(STC 1.7)"),
|
||||
@@ -64,9 +68,14 @@ RETIRED_TERMS = [
|
||||
# (词面误报率高,靠术语表对照行与人工判读),game/ 扫描域归代码批后另加。
|
||||
("金币", "2026-10-18", "GP(唯一正式命名)", "GP 命名统一批——「金币」全库禁用"),
|
||||
("插旗|标旗", "2026-10-18", "封印(正式动作名,全库统一)", "术语刷新批——「旗」概念退役"),
|
||||
("完美排雷", "2026-10-19", "完美封印(正式名见 design/gdd/术语表.md 退役词对照)", "迭代 32 卡①反哺批 2026-10-21 用户裁入表——代码注释面由 S11 承接,文档正文面由此条承接"),
|
||||
]
|
||||
|
||||
DEFAULT_DIRS = ["design/gdd"]
|
||||
DEFAULT_DIRS = ["design/gdd", "design/art"]
|
||||
# 根规则与流程文件同属现行文本,2026-10-21 迭代 32 反哺批复测后纳入扫描面:
|
||||
# 当日实测这批文件自身含 3 处用语表废止词(roadmap 的「对拍」「标旗」、AGENTS 的「收口」×2),
|
||||
# 随同批订正后判红归零才扩面——先改现实、再让闸盯住现实,避免带红扩面。
|
||||
DEFAULT_FILES = ["待办清单.md", "体验验证清单.md", "roadmap.md", "AGENTS.md"]
|
||||
|
||||
# 用语规范 A 表补充词专项补扫:随批/台账 两词本身是上方行级白名单信号(行内出现即归
|
||||
# 留档类),主扫描对它们自豁免;本通道只做节外残留的专项报告,供后续触碰批清偿。
|
||||
@@ -119,7 +128,7 @@ TERM_CONTEXT_EXEMPT = {
|
||||
# 敌人 id 只能读作历史留档(真正的现行规格在 敌人系统.md「目标顺位与意图线索」节)。
|
||||
"伏击": re.compile(r"ambusher|成文占位"),
|
||||
"heavy_multiplier|重击倍率": re.compile(r"ambusher|heavy_multiplier|成文占位"),
|
||||
# 拔旗(2026-09-19 退役「旗无移除手段」):06 待办清单「件派生动词白名单」条目说的是
|
||||
# 拔旗(2026-09-19 退役「旗无移除手段」):待办清单「件派生动词白名单」条目说的是
|
||||
# 「将来件池 ≥30 种后可能开放的第三盘面动作」,是待评估的设计意图而非在用机制。
|
||||
"拔旗|撤旗": re.compile(r"件派生动词白名单|第三盘面动作|非信息类"),
|
||||
}
|
||||
@@ -187,6 +196,7 @@ def main():
|
||||
for d in DEFAULT_DIRS:
|
||||
paths += sorted(Path(d).rglob("*.md"))
|
||||
paths += sorted(Path(d).rglob("*.yaml"))
|
||||
paths += [Path(f) for f in DEFAULT_FILES]
|
||||
paths = [p for p in paths if Path(p).exists()]
|
||||
|
||||
findings = scan(paths) + scan_supplement(paths)
|
||||
|
||||
+19
-55
@@ -9,14 +9,12 @@ r"""gdd_lint.py — GDD lean 格式门禁
|
||||
|
||||
规则(口径权威 = design/engineering/doc-conventions.md 第 5–6 节文档骨架与结构纪律):
|
||||
R1 散文段落 ≤ 240 字(代码块/表格/列表/引用不计)
|
||||
R2 表格字符占比(词条表分域豁免,见 R2_EXEMPT 注)
|
||||
R3 正文禁止修订注记:~~删除线~~、"作废"、括号内"日期+评审/修订/拍板/裁决/核销"
|
||||
(登记处/清单按 RULE_EXEMPT 分域豁免)
|
||||
R4 禁止变更记录节:出现「版本历史/修订记录/变更记录/修改记录/更新记录/更新日志」标题即红
|
||||
(沿革只进 git commit log;扫描面 = 全 design/ + roadmap.md,见 --changelog-only)
|
||||
R5 流程/状态类章节必须含代码块图(字符画或 Mermaid);清单节/词条节按 RULE_EXEMPT 分域豁免
|
||||
R5 流程/状态类章节必须含代码块图(字符画或 Mermaid);词条节按 RULE_EXEMPT 分域豁免
|
||||
R6 表格列数一致(含转义 \| 识别)
|
||||
R7 括号嵌套跨度 ≤ 80 字(长括号 = 审计注记挤进正文的信号;登记处/清单按 RULE_EXEMPT 分域豁免)
|
||||
R7 括号嵌套跨度 ≤ 80 字(长括号 = 审计注记挤进正文的信号)
|
||||
R8 核心规则表:规则列必须是"谁·何时·做什么"陈述句所在的可读表——说明/要点列
|
||||
禁止出现编号引用(AC-x/F-x/EC-x/E-x/OQ-x/Rule-x/义务),引用只允许在依据类列
|
||||
R9 一页纸节点:任一标题节点的直接内容(到下一标题为止)≤ 80 行——装不下就拆子节点
|
||||
@@ -31,42 +29,19 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
MAX_PARA = 240
|
||||
# 2026-09-14 放宽 60→30:lean 改革后的新模板(参考 analysis/design 两件套)允许
|
||||
# 短段落与编号人话规则——防铺开的职责改由 R1(段 ≤240 字)与 R9(节点 ≤80 行)承担,
|
||||
# R2 降为兜底下限(防纯散文文档)。
|
||||
MIN_TABLE_RATIO = 30
|
||||
# R2 表格占比判据已停用(2026-10-20 规则审阅批):lean 体系退役后其解读对象已不存在,
|
||||
# 分域豁免越加越多、只产存量噪音(af94ce97 指纹刷新为证),判据与 R2_EXEMPT 一并拆除。
|
||||
MAX_PAREN_SPAN = 80
|
||||
|
||||
# 规则分域(2026-10-03 用户裁「测试要快/要准」优化批):R1/R2 的体裁前提是「规范文」
|
||||
# (短段+表格化);登记处/清单/概念文的自然形态就是长条目与叙事散文,硬套=自 2026-09-20
|
||||
# 上线起 8/8 常红零过滤价值(test-perf-report-2026-10-02 §6.2 实证)。R3/R7 不豁免——
|
||||
# 「provenance 出正文」对一切体裁有效;R4 变更记录节禁令自 2026-10-19 起对一切体裁有效。
|
||||
R1_EXEMPT = {
|
||||
"06-待办清单.md", # 全项目待办登记处:现状/状态/方案长条目为体裁
|
||||
"07-体验验证清单.md", # 体验验证读数清单:同上
|
||||
}
|
||||
R2_EXEMPT = {
|
||||
"01-核心循环.md", # 循环概念文:叙事散文为体裁
|
||||
"03-内容规模.md", # 规模框架文:多维档位靠散文交代,结构表只有一张
|
||||
"04-美术音频方向.md", # 美术方向文:同上
|
||||
"05-不做清单.md", # 清单文:条目散文为体裁
|
||||
"06-待办清单.md",
|
||||
"07-体验验证清单.md",
|
||||
"术语表.md", # 词条表:表格为主体形态,表格占比高是体裁本身
|
||||
}
|
||||
# 规则分域(2026-10-03 用户裁「测试要快/要准」优化批):R1 的体裁前提是「规范文」
|
||||
# (短段+表格化)。「provenance 出正文」对一切体裁有效;R4 变更记录节禁令自 2026-10-19
|
||||
# 起对一切体裁有效。
|
||||
# 规则粒度分域豁免(2026-10-15 用户令「清单类文件与术语表不做 GDD 规范性检查」)。
|
||||
# 原则:**按规则豁免,不按文件拉黑**——体裁规则(R3/R5/R7)对登记处/清单/词条表
|
||||
# 不适用,但机械可判的完整性规则(R4/R6/R8/R9/R11)一律不豁免,真缺陷不得藏起来。
|
||||
# 各条理由:
|
||||
# R3 正文禁修订注记——登记处/清单的条目「带日期+裁决来源」是设计目的本身
|
||||
# (条目要能回答「这条谁在何时裁的」),禁它等于禁登记处的核心价值。
|
||||
# R5 流程/状态类节须含代码块图——节名含「流程/生命周期」即触发,但 07 的
|
||||
# 「㉖…全流程走查」「㉗…目检」是目检清单节、术语表的「开发流程」是词条节,
|
||||
# 两者都不是流程描述,属节名误匹配。
|
||||
# R7 括号跨度 ≤80 字——长括注在登记处/清单里是常态(现状+裁决+归属挤在一行)。
|
||||
# 原则:**按规则豁免,不按文件拉黑**——体裁规则对词条表不适用,但机械可判的完整性规则
|
||||
# (R4/R6/R8/R9/R11)一律不豁免,真缺陷不得藏起来。
|
||||
# R5 流程/状态类节须含代码块图——节名含「流程/生命周期」即触发,术语表的「开发流程」
|
||||
# 是词条节、不是流程描述,属节名误匹配。
|
||||
RULE_EXEMPT = {
|
||||
"06-待办清单.md": {"R3", "R7"},
|
||||
"07-体验验证清单.md": {"R3", "R5", "R7"},
|
||||
"术语表.md": {"R5"},
|
||||
}
|
||||
|
||||
@@ -193,27 +168,16 @@ def lint_file(path):
|
||||
issues.append(f"R9 节点「{node_name}」直接内容 {node_count} 行(>{MAX_NODE_LINES})——超一页,拆为子节点")
|
||||
infos.append(f"最大节点直接内容 {max(node_count, 0)} 行")
|
||||
|
||||
# R1 段落长度(登记处/清单文分域豁免,见 R1_EXEMPT 注)
|
||||
# R1 段落长度
|
||||
fname = Path(path).name
|
||||
ex = RULE_EXEMPT.get(fname, set()) # 规则粒度分域豁免(见 RULE_EXEMPT 注)
|
||||
if fname not in R1_EXEMPT:
|
||||
for p in paras:
|
||||
if len(p) > MAX_PARA:
|
||||
issues.append(f"R1 散文段 {len(p)} 字(>{MAX_PARA}): {p[:50]}…")
|
||||
for p in paras:
|
||||
if len(p) > MAX_PARA:
|
||||
issues.append(f"R1 散文段 {len(p)} 字(>{MAX_PARA}): {p[:50]}…")
|
||||
|
||||
# R2 表格占比(概念/方向/清单文分域豁免,见 R2_EXEMPT 注)
|
||||
code_chars = sum(len(l) for l in code_lines)
|
||||
table_chars = sum(len(l) for l in text.splitlines() if l.strip().startswith("|"))
|
||||
total = max(len(text) - code_chars, 1)
|
||||
ratio = int(100 * table_chars / total)
|
||||
if fname in R2_EXEMPT:
|
||||
infos.append(f"表格占比 {ratio}%(R2 分域豁免)")
|
||||
elif ratio < MIN_TABLE_RATIO:
|
||||
issues.append(f"R2 表格占比 {ratio}%(<{MIN_TABLE_RATIO}%)")
|
||||
else:
|
||||
infos.append(f"表格占比 {ratio}%")
|
||||
# R2 表格占比已停用(见文件头注释)。
|
||||
|
||||
# R3 修订注记(代码块豁免;登记处/清单按 RULE_EXEMPT 豁免——变更记录节已整体禁设,无豁免域)
|
||||
# R3 修订注记(代码块豁免;变更记录节已整体禁设,无豁免域)
|
||||
body_no_code = "\n".join(l for l in text.splitlines() if l not in code_lines)
|
||||
for m in (() if "R3" in ex else re.finditer(r"~~.+?~~", body_no_code)):
|
||||
issues.append(f"R3 删除线残留: …{m.group(0)[:40]}…")
|
||||
@@ -307,7 +271,7 @@ def lint_file(path):
|
||||
issues.append(f"R7 最长括号跨度 {longest_span} 字(>{MAX_PAREN_SPAN})——疑似审计注记挤进正文")
|
||||
infos.append(f"最长括号跨度 {longest_span} 字")
|
||||
|
||||
return {"file": str(path), "issues": issues, "infos": infos, "table_ratio": ratio}
|
||||
return {"file": str(path), "issues": issues, "infos": infos}
|
||||
|
||||
|
||||
def main():
|
||||
@@ -347,7 +311,7 @@ def main():
|
||||
else:
|
||||
for r in results:
|
||||
mark = "❌ FAIL" if r["issues"] else "✅ PASS"
|
||||
print(f"\n{mark} {r['file']} (表格占比 {r['table_ratio']}%)")
|
||||
print(f"\n{mark} {r['file']}")
|
||||
for i in r["infos"]:
|
||||
print(f" · {i}")
|
||||
for issue in r["issues"]:
|
||||
|
||||
@@ -166,7 +166,7 @@ class RunAcc:
|
||||
self.end_hp = int(e.get("hp", 0))
|
||||
|
||||
def on_action(self, e):
|
||||
# 刷新经济监控(07 清单⑦):只计受理成功的弃图刷新——被拒尝试不构成弃图。
|
||||
# 刷新经济监控(体验验证清单⑦):只计受理成功的弃图刷新——被拒尝试不构成弃图。
|
||||
if e.get("op") == "refresh" and e.get("accepted"):
|
||||
self.refresh_count += 1
|
||||
|
||||
@@ -514,7 +514,7 @@ def render(res_files, args, out):
|
||||
print("== 关卡级统计 ==", file=out)
|
||||
print(" 口径:局=run 行切分(无 run 行的旧日志整文件一局);通关=result=victory;", file=out)
|
||||
print(" 进BossHP=Boss首结算快照HP+首轮承伤−首轮治疗(精确反推);平均轮数=每局settlement数。", file=out)
|
||||
print(" 弃图率=每局受理刷新次数(07⑦ 基线读数,与 MC 刷新/局同口径);", file=out)
|
||||
print(" 弃图率=每局受理刷新次数(体验验证清单⑦ 基线读数,与 MC 刷新/局同口径);", file=out)
|
||||
print(" 每轮刷新率=刷新次数÷结算轮数(组内加权)。阈值数字归调参批,本表只出基线。", file=out)
|
||||
hdr = (f" {'关卡':>6} {'档位':>8} {'局数':>5} {'通关率':>7} {'平均轮数':>8} {'进BossHP均':>10} "
|
||||
f"{'通关剩余HP均':>12} {'整局承伤均':>10} {'弃图率':>7} {'每轮刷新率':>9}")
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""V12 同种子复放比对器(卡② 效果引擎无状态化批):解析两次 MC 采集的 RUNSUM 行按 seed 配对判读。
|
||||
两判据模式——parity:整条记录逐位相等(off 组,未受影响路径须透明);
|
||||
input-parity:输入面八字段 + 顶层字段集恒等、种子集与局数一致(formed/rules 组,输出面允许 RNG 级联位移)。
|
||||
退出码 0 绿 / 1 红 / 2 用法或解析错。字段集从基线文件推导,禁硬编码。"""
|
||||
import sys, json, argparse
|
||||
|
||||
INPUT_KEYS = ["policy", "seed", "atk_pct", "hp_pct", "route", "attune", "equipped", "feat_mode"]
|
||||
|
||||
|
||||
def load(path):
|
||||
recs = {}
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line.startswith("RUNSUM "):
|
||||
continue
|
||||
try:
|
||||
d = json.loads(line[len("RUNSUM "):])
|
||||
except json.JSONDecodeError:
|
||||
print("解析失败行:", line[:80])
|
||||
sys.exit(2)
|
||||
recs[d.get("seed")] = d
|
||||
return recs
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--mode", choices=["parity", "input-parity"], required=True)
|
||||
ap.add_argument("--base", required=True)
|
||||
ap.add_argument("--post", required=True)
|
||||
ap.add_argument("--expect", type=int, required=True)
|
||||
a = ap.parse_args()
|
||||
base, post = load(a.base), load(a.post)
|
||||
if len(base) != a.expect or len(post) != a.expect:
|
||||
print("局数不符:base=%d post=%d expect=%d" % (len(base), len(post), a.expect))
|
||||
return 1
|
||||
if set(base) != set(post):
|
||||
print("种子集不一致")
|
||||
return 1
|
||||
red = False
|
||||
if a.mode == "parity":
|
||||
diff = [s for s in base if base[s] != post[s]]
|
||||
if diff:
|
||||
red = True
|
||||
print("off 组逐位差异 %d 局: %s" % (len(diff), diff[:10]))
|
||||
for s in diff[:3]:
|
||||
k = [kk for kk in base[s] if base[s].get(kk) != post[s].get(kk)]
|
||||
print(" seed", s, "差异字段:", k)
|
||||
else:
|
||||
bkeys = set(next(iter(base.values())).keys())
|
||||
pkeys = set(next(iter(post.values())).keys())
|
||||
if bkeys != pkeys:
|
||||
red = True
|
||||
print("顶层字段集不一致 base-p=%s post-b=%s" % (bkeys - pkeys, pkeys - bkeys))
|
||||
for s in base:
|
||||
for kk in INPUT_KEYS:
|
||||
if base[s].get(kk) != post[s].get(kk):
|
||||
red = True
|
||||
print("输入面字段 %s 在 seed %s 不一致: %r vs %r" % (kk, s, base[s].get(kk), post[s].get(kk)))
|
||||
# levels_cleared 直方图仅报告(方向不作判据)
|
||||
lc = post[s].get("levels_cleared") if post else None
|
||||
print("判读:%s" % ("FAIL" if red else "PASS"))
|
||||
return 1 if red else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+169
-50
@@ -1,15 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""pipeline_gate.py — 工作流程流水线闸(06 条目状态机+roadmap 迭代总表+需求卡片口径+一次交叉核)。
|
||||
r"""pipeline_gate.py — 工作流程流水线闸(待办清单条目状态机+roadmap 迭代总表+需求卡片口径+一次交叉核)。
|
||||
|
||||
职责:机检 06 条目(标题元数据/属性齐备/状态词白名单/条件凭据/状态段累积制在场约束/特批/
|
||||
职责:机检待办清单条目(标题元数据/属性齐备/状态词白名单/条件凭据/状态段累积制在场约束/特批/
|
||||
零命中)与 roadmap 迭代总表(六列/状态四形态/卡片与报告存在性/编入时间与预计周期的
|
||||
历史行「—」豁免/周期≤2 周可特批豁免/单元格白名单/零命中);「已编迭代↔已写卡段↔迭代行
|
||||
卡片列」交叉核只此一处。2026-10-18 卡片冲突修订批补两条卡片机检:C1 关联文档同步面的
|
||||
文档义务口径(生效 2026-10-17)、C2 设计事实源声明(生效 2026-10-18)——两条都按文件名
|
||||
日期只追新规生效日之后的卡,旧卡属 git 历史留档不回头改。
|
||||
日期只追新规生效日之后的卡,旧卡属 git 历史留档不回头改。2026-10-21 验收闭合批补 C3:
|
||||
改动—验收对照表的结构检查(生效 2026-10-21)——只查节/列/行/必填格非空,不判断覆盖真假;
|
||||
覆盖真假由独立验收审计判。
|
||||
约束:每红项一行「文件:行号 判据名 一句白话说明」,exit 0=全绿 / 1=有红 / 2=输入件缺失;
|
||||
卡片与报告存在性相对 --repo-root 解析(缺省=本文件上两级);06 与 roadmap 均已为新格式
|
||||
卡片与报告存在性相对 --repo-root 解析(缺省=本文件上两级);待办清单与 roadmap 均已为新格式
|
||||
(2026-10-18 数据整改批),本闸已注册进 tools/run_all_gates.sh 文档域(08c 流水线闸)。
|
||||
用法:python tools/pipeline_gate.py [--todo PATH] [--roadmap PATH] [--repo-root PATH]
|
||||
"""
|
||||
@@ -18,7 +20,7 @@ import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_TODO = Path("design/gdd/06-待办清单.md")
|
||||
DEFAULT_TODO = Path("待办清单.md")
|
||||
DEFAULT_ROADMAP = Path("roadmap.md")
|
||||
|
||||
SOURCE_WORDS = ("playnote", "会话提及", "审计发现", "批次移交")
|
||||
@@ -47,6 +49,12 @@ SOURCE_MARKS = ("设计事实源", "唯一事实源") # 两种合格写法,
|
||||
SYNC_HEADING_RE = re.compile(r"^#{2,3}\s*\d*\.?\s*关联文档同步面")
|
||||
BANNED_DOC_MARKS = ("design/", "../gdd/", "../registry/", "../research/", "../qa/",
|
||||
"../engineering/", "../ux/", "../art/", "roadmap.md", "AGENTS.md")
|
||||
# C3 改动—验收对照表(2026-10-21 验收闭合批,生效 2026-10-21)
|
||||
LEDGER_RULE_DATE = (2026, 10, 21)
|
||||
LEDGER_HEADING_RE = re.compile(r"^#{2,4}\s*.*(验收对照表|改动[—\-]验收)")
|
||||
LEDGER_REQUIRED = ("需求点", "改动", "验收测试", "真实入口")
|
||||
LEDGER_PROOF_MARKS = ("撤销", "变异", "证明")
|
||||
AUDIT_MARK = "验收审计"
|
||||
|
||||
DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}")
|
||||
VER_RE = re.compile(r"[vV]\d+(?:\.\d+)*")
|
||||
@@ -119,8 +127,12 @@ def check_title(title, line, issues, where):
|
||||
return face
|
||||
|
||||
|
||||
def check_segment(word, creds, line, issues, where, root):
|
||||
"""判据 3 的单段校验。返回 (合法状态词|None, 已写卡路径|None, 已编迭代号|None)。"""
|
||||
def check_segment(word, line, issues, where):
|
||||
"""判据 3 的单段校验。返回 (合法状态词|None, 已编迭代号|None)。
|
||||
|
||||
2026-10-20 状态栏纯化批:状态行只写状态词(§12.1),括号凭据已废止——
|
||||
闭合证据改到现状关联文档清单核对(check_todo 内),本函数只认状态词本身。
|
||||
"""
|
||||
def red(name, msg):
|
||||
issues.append((where, line, name, msg))
|
||||
|
||||
@@ -128,40 +140,24 @@ def check_segment(word, creds, line, issues, where, root):
|
||||
base = "已编迭代" if word.startswith("已编迭代") else word
|
||||
if im is None and base == "已编迭代":
|
||||
red("状态词非法", "「%s」缺迭代号(须「已编迭代 N」)" % word)
|
||||
return None, None, None
|
||||
return None, None
|
||||
if base not in STATUS_WORDS:
|
||||
red("状态词非法", "「%s」不在合法状态词白名单" % word)
|
||||
return None, None, None
|
||||
cred_text = ";".join(creds)
|
||||
if base in ("文档设计已闭合", "已写入"):
|
||||
if not (VER_RE.search(cred_text) and ("/" in cred_text or ".md" in cred_text)):
|
||||
red("凭据缺失", "「%s」须带括号落点凭据(文档路径+版本号形态)" % base)
|
||||
elif base == "美术设计已闭合":
|
||||
if not ("art-bible" in cred_text.lower() and VER_RE.search(cred_text)):
|
||||
red("凭据缺失", "「美术设计已闭合」凭据须含 art-bible 路径+版本号")
|
||||
elif base == "已写卡":
|
||||
rp = None
|
||||
for c in creds:
|
||||
m = RESEARCH_RE.search(c)
|
||||
if m:
|
||||
rp = m.group(0)
|
||||
break
|
||||
if rp is None:
|
||||
red("凭据缺失", "「已写卡」括号内须为 reports/ 下的需求卡片路径")
|
||||
elif not (root / rp).is_file():
|
||||
red("卡片路径不存在", "已写卡路径 %s 相对仓库根不存在" % rp)
|
||||
return base, rp, None
|
||||
red("状态词非法", "「%s」不在合法状态词白名单" % base)
|
||||
return None, None
|
||||
if im is not None:
|
||||
return base, None, int(im.group(1))
|
||||
return base, None, None
|
||||
return base, int(im.group(1))
|
||||
return base, None
|
||||
|
||||
|
||||
def check_status(raw, line, face, issues, where, root):
|
||||
"""判据 3+状态段累积制(§12.1)+特批/备选。返回 (状态词列表, 已写卡路径列表, 已编迭代号|None)。
|
||||
def check_status(raw, line, face, issues, where):
|
||||
"""判据 3+状态段累积制(§12.1)+特批/备选。返回 (状态词列表, 已编迭代号|None)。
|
||||
|
||||
累积制:按+切段后段数不限,逐段各自合法;再查两条结构约束——
|
||||
下游段(待写卡/已写卡/已编迭代)在场时上游闭合段必须在场(带日期特批可豁免);
|
||||
双轨设计阶段(无下游段)必须恰两段且一文档一美术。
|
||||
状态行只写状态词(§12.1):去掉特批标记与(备选)后缀后,残留任何括号即红——
|
||||
证据写现状关联文档清单,由 check_todo 到正文核对;此处点名病因后短路,
|
||||
不再产出碎片「状态词非法」误导红。
|
||||
"""
|
||||
content = raw.strip()
|
||||
m = re.match(r"^\*\*(.+)\*\*$", content)
|
||||
@@ -175,28 +171,28 @@ def check_status(raw, line, face, issues, where, root):
|
||||
issues.append((where, line, "特批标记", "〔用户特批〕缺日期(带 YYYY-MM-DD 才放行跳轨)"))
|
||||
content = APPROVAL_RE.sub("", content)
|
||||
content = content.replace("(备选)", "").replace("(备选)", "").strip()
|
||||
if re.search(r"[(())]", content):
|
||||
issues.append((where, line, "状态行含括号内容",
|
||||
"括号里的信息(路径/版本/说明)写进现状关联文档清单,状态行只留状态词(§12.1)"))
|
||||
return [], None
|
||||
segs = [s.strip() for s in content.split("+") if s.strip()]
|
||||
if not segs:
|
||||
issues.append((where, line, "状态词非法", "状态行内容为空"))
|
||||
return [], [], None
|
||||
words, cards, iter_no = [], [], None
|
||||
return [], None
|
||||
words, iter_no = [], None
|
||||
for seg in segs:
|
||||
creds = CRED_RE.findall(seg)
|
||||
word = CRED_RE.sub("", seg).strip()
|
||||
w, rp, it = check_segment(word, creds, line, issues, where, root)
|
||||
w, it = check_segment(seg, line, issues, where)
|
||||
if w:
|
||||
words.append(w)
|
||||
if rp:
|
||||
cards.append(rp)
|
||||
if it is not None:
|
||||
iter_no = it
|
||||
downstream = [w for w in words if w in DOWNSTREAM_WORDS]
|
||||
if downstream and face in PRESENCE_FACES and not dated_approval:
|
||||
need = []
|
||||
if face in ("文档轨", "双轨") and "文档设计已闭合" not in words:
|
||||
need.append("文档设计已闭合(凭据)")
|
||||
need.append("文档设计已闭合")
|
||||
if face in ("美术轨", "双轨") and "美术设计已闭合" not in words:
|
||||
need.append("美术设计已闭合(凭据)")
|
||||
need.append("美术设计已闭合")
|
||||
if need:
|
||||
issues.append((where, line, "在场约束",
|
||||
"下游段(%s)在场但缺上游闭合段 %s(带日期特批可豁免)"
|
||||
@@ -208,11 +204,16 @@ def check_status(raw, line, face, issues, where, root):
|
||||
issues.append((where, line, "双轨段数",
|
||||
"设计面=双轨设计阶段须恰两段且一文档一美术(现 %d 段:文档轨 %d、美术轨 %d)"
|
||||
% (len(segs), n_doc, n_art)))
|
||||
return words, cards, iter_no
|
||||
return words, iter_no
|
||||
|
||||
|
||||
def check_todo(text, issues, where, root):
|
||||
"""06 全部判据。返回交叉核链(已编迭代条目 → (行号, 迭代号, 卡片路径|None))。"""
|
||||
"""待办清单全部判据。返回交叉核链(已编迭代条目 → (行号, 迭代号, 卡片路径|None))。
|
||||
|
||||
2026-10-20 状态栏纯化批:闭合类状态词的证据核对从状态栏括号改到现状关联文档
|
||||
清单(§12.1「状态行只写状态词」)——已写卡对 reports/ 卡片路径+磁盘存在性,
|
||||
文档轨闭合/已写入对 design/ 路径+版本号,美术轨闭合对 art-bible+版本号。
|
||||
"""
|
||||
entries = parse_todo(text)
|
||||
links = []
|
||||
if not entries:
|
||||
@@ -233,8 +234,45 @@ def check_todo(text, issues, where, root):
|
||||
issues.append((where, line, "属性缺失", "条目缺「**%s**」属性" % req))
|
||||
if "状态" not in attrs:
|
||||
continue
|
||||
words, cards, iter_no = check_status(
|
||||
attrs["状态"], attr_line["状态"], face, issues, where, root)
|
||||
words, iter_no = check_status(
|
||||
attrs["状态"], attr_line["状态"], face, issues, where)
|
||||
# 现状属性全文(含清单续行):证据核对只看现状,其他属性不算数
|
||||
xian_lines, in_xian = [], False
|
||||
for ln, raw in e["body"]:
|
||||
m = ATTR_LINE_RE.match(raw.strip())
|
||||
if m:
|
||||
in_xian = m.group(1) == "现状"
|
||||
if in_xian:
|
||||
xian_lines.append(m.group(2))
|
||||
elif in_xian:
|
||||
xian_lines.append(raw)
|
||||
xian = "\n".join(xian_lines)
|
||||
xline = attr_line.get("现状", line)
|
||||
# 证据核对一:已写卡 → 现状清单须有卡片路径,逐条验磁盘存在
|
||||
cards = []
|
||||
if "已写卡" in words:
|
||||
cards = RESEARCH_RE.findall(xian)
|
||||
if not cards:
|
||||
issues.append((where, xline, "正文缺卡片路径",
|
||||
"状态段「已写卡」在场,但现状关联文档清单里没有 "
|
||||
"reports/需求卡片-*.md 路径(§12.1:证据写正文)"))
|
||||
else:
|
||||
for rp in cards:
|
||||
if not (root / rp).is_file():
|
||||
issues.append((where, line, "卡片路径不存在",
|
||||
"现状清单里的卡片路径 %s 相对仓库根不存在" % rp))
|
||||
# 证据核对二:文档轨闭合/反哺已写入 → 现状清单须有文档路径+版本号
|
||||
# (待办清单正文里的指针是相对路径,形如 systems/… 或 design/…,只认 .md 收尾+版本号)
|
||||
if "文档设计已闭合" in words or "已写入" in words:
|
||||
if not (re.search(r"[^\s ,,()()]+\.md", xian) and VER_RE.search(xian)):
|
||||
issues.append((where, xline, "正文缺设计定稿指针",
|
||||
"状态段含文档轨闭合词,但现状清单里没有带版本号的文档路径(§12.1:证据写正文)"))
|
||||
# 证据核对三:美术轨闭合 → 现状清单须有 art-bible 版本凭据
|
||||
if "美术设计已闭合" in words:
|
||||
if not ("art-bible" in xian.lower() and VER_RE.search(xian)):
|
||||
issues.append((where, xline, "正文缺美术凭据",
|
||||
"状态段「美术设计已闭合」在场,但现状清单里没有 "
|
||||
"art-bible 版本凭据(§12.1:证据写正文)"))
|
||||
need_gap = any(w in GAP_REQUIRED for w in words)
|
||||
if need_gap and "缺口" not in attrs:
|
||||
issues.append((where, line, "缺口属性",
|
||||
@@ -381,13 +419,88 @@ def card_date(path):
|
||||
return tuple(int(x) for x in found[-1].split("-"))
|
||||
|
||||
|
||||
def check_ledger(text, path, issues):
|
||||
"""C3 改动—验收对照表结构检查(生效 2026-10-21)。
|
||||
|
||||
只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空。
|
||||
不判断测试是否真的覆盖改动——那是独立验收审计的职责,机器闸不猜语义。
|
||||
"""
|
||||
lines = text.splitlines()
|
||||
start = None
|
||||
for i, raw in enumerate(lines):
|
||||
if LEDGER_HEADING_RE.match(raw.strip()):
|
||||
start = i
|
||||
break
|
||||
if start is None:
|
||||
issues.append((str(path), 1, "验收对照表缺失",
|
||||
"须含「改动—验收对照表」节(2026-10-21 起,req-card §8)"))
|
||||
return
|
||||
|
||||
header = None
|
||||
header_line = 0
|
||||
for i in range(start + 1, len(lines)):
|
||||
s = lines[i].strip()
|
||||
if s.startswith("#") and not LEDGER_HEADING_RE.match(s):
|
||||
break
|
||||
if s.startswith("|") and not SEP_ROW_RE.match(s):
|
||||
header = row_cells(s)
|
||||
header_line = i + 1
|
||||
break
|
||||
if header is None:
|
||||
issues.append((str(path), start + 1, "验收对照表缺表",
|
||||
"节内未找到表格"))
|
||||
return
|
||||
|
||||
idx = {}
|
||||
for name in LEDGER_REQUIRED:
|
||||
hit = next((k for k, cell in enumerate(header) if name in cell), None)
|
||||
if hit is None:
|
||||
issues.append((str(path), header_line, "验收对照表缺列",
|
||||
"缺「%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
|
||||
|
||||
rows = 0
|
||||
for i in range(header_line, len(lines)):
|
||||
s = lines[i].strip()
|
||||
if not s:
|
||||
continue
|
||||
if not s.startswith("|"):
|
||||
break
|
||||
if SEP_ROW_RE.match(s):
|
||||
continue
|
||||
cells = row_cells(s)
|
||||
if len(cells) < len(header):
|
||||
issues.append((str(path), i + 1, "验收对照表缺格",
|
||||
"列数少于表头(%d<%d)" % (len(cells), len(header))))
|
||||
continue
|
||||
rows += 1
|
||||
for name, k in idx.items():
|
||||
if k >= len(cells) or not cells[k].strip():
|
||||
issues.append((str(path), i + 1, "验收对照表空格",
|
||||
"「%s」列为空" % name))
|
||||
if rows == 0:
|
||||
issues.append((str(path), header_line, "验收对照表零行",
|
||||
"表内没有数据行"))
|
||||
|
||||
|
||||
def check_cards(root, issues):
|
||||
"""C1 文档义务口径(生效 2026-10-17)+C2 设计事实源声明(生效 2026-10-18)。
|
||||
"""C1 文档义务口径(生效 2026-10-17)+C2 设计事实源声明(生效 2026-10-18)
|
||||
+C3 改动—验收对照表结构(生效 2026-10-21)。
|
||||
|
||||
只检文件名日期达生效日的卡——更早的卡属已实现批次的历史留档,不回头改。
|
||||
C1 判据:「关联文档同步面」表内,目标格指向 design/ 或根规则文件、而义务格仍写
|
||||
「随批」者红(2026-10-17 req-card §6 改口径后,这类点只能列 reports/ 反哺点清单)。
|
||||
C2 判据:全文既无「设计事实源」也无「唯一事实源」者红(两种写法都与库内实态对齐)。
|
||||
C3 判据:有「改动—验收对照表」节;表头含需求点/改动/验收测试/真实入口/证明列;
|
||||
至少一行;必填格非空;全文含「验收审计」声明。语义覆盖不在此判。
|
||||
"""
|
||||
dirn = root / CARD_DIR
|
||||
if not dirn.is_dir():
|
||||
@@ -404,6 +517,12 @@ def check_cards(root, issues):
|
||||
issues.append((str(path), 1, "设计事实源缺失",
|
||||
"须含「设计事实源」或「唯一事实源」声明句(req-card §2 三选一,"
|
||||
"GDD 节+版本号/样张+art-bible 版本/工程直入证据指认)"))
|
||||
if when >= LEDGER_RULE_DATE:
|
||||
check_ledger(text, path, issues)
|
||||
if AUDIT_MARK not in text:
|
||||
issues.append((str(path), 1, "独立验收审计未声明",
|
||||
"须在卡内写明独立验收审计入口与报告文件(2026-10-21 起,"
|
||||
"test-conventions §9.7)"))
|
||||
if when < SYNC_RULE_DATE:
|
||||
continue
|
||||
in_sec = False
|
||||
@@ -429,8 +548,8 @@ def check_cards(root, issues):
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description="工作流程流水线闸:06 条目状态机+roadmap 迭代总表+一次交叉核")
|
||||
ap.add_argument("--todo", help="06-待办清单路径(缺省=<仓库根>/design/gdd/06-待办清单.md)")
|
||||
description="工作流程流水线闸:待办清单条目状态机+roadmap 迭代总表+一次交叉核")
|
||||
ap.add_argument("--todo", help="待办清单路径(缺省=<仓库根>/待办清单.md)")
|
||||
ap.add_argument("--roadmap", help="roadmap 路径(缺省=<仓库根>/roadmap.md)")
|
||||
ap.add_argument("--repo-root", help="仓库根(卡片/报告存在性的相对基准;缺省=本文件上两级)")
|
||||
args = ap.parse_args()
|
||||
@@ -454,7 +573,7 @@ def main():
|
||||
if issues:
|
||||
print("流水线闸 FAIL:红项 %d" % len(issues))
|
||||
return 1
|
||||
print("流水线闸 PASS:06 条目 %d · 总表数据行 %d · 交叉核 %d 链 · 卡片口径 %d 张全过"
|
||||
print("流水线闸 PASS:待办清单条目 %d · 总表数据行 %d · 交叉核 %d 链 · 卡片口径 %d 张全过"
|
||||
% (len(parse_todo(todo_text)), len(index), len(links), n_cards))
|
||||
return 0
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# 段:
|
||||
# ① shot_probe 七屏窗口化截图(menu/settings/career/camp/level/battle/status)
|
||||
# → tools/shot_checker.py A-D 语义断言自动判定(A 屏级骨架全屏/B 文字对比度/C 明度三档带/
|
||||
# D 文字禁压纹理;E 焦点预算=语义面仍归人闸不机检)。首跑存量 FAIL 按 S1 停机点全量呈报登记 06。
|
||||
# D 文字禁压纹理;E 焦点预算=语义面仍归人闸不机检)。首跑存量 FAIL 按 S1 停机点全量呈报登记待办清单。
|
||||
# ② 取证段(Q7:超时/零字节/文件缺失任一即 FAIL——「渲染真实发生」必须成立,不做像素判定):
|
||||
# fx_shot_probe 三时刻抓帧(行到达/结算排播/轮末连帧)+ board_shot_probe L1-L3 棋盘抓帧。
|
||||
# ③ 棋盘 golden 逐像素:board 整屏帧按 board_meta.json 取样坐标裁剪棋盘区
|
||||
@@ -64,7 +64,7 @@ fi
|
||||
if [ "$_rc" -eq 0 ]; then
|
||||
python tools/shot_checker.py "$SHOT" shot_menu.png shot_settings.png shot_career.png shot_camp.png shot_level.png shot_battle.png shot_status.png | sed 's/^/[render_gate] /'
|
||||
_rc="${PIPESTATUS[0]}"
|
||||
[ "$_rc" -ne 0 ] && echo "[render_gate] FAIL — shot_checker A-D 断言存在红(S1 停机点:首跑存量 FAIL 全量呈报登记 06)"
|
||||
[ "$_rc" -ne 0 ] && echo "[render_gate] FAIL — shot_checker A-D 断言存在红(S1 停机点:首跑存量 FAIL 全量呈报登记待办清单)"
|
||||
fi
|
||||
|
||||
echo "======== fx_shot_probe 三时刻取证(Q7) ========"
|
||||
|
||||
@@ -27,7 +27,8 @@ ROOT = Path(__file__).resolve().parent.parent
|
||||
# 清单的家——迭代批禁写 design/,只准往这里写,design/ 全目录对它只读。只放 .md,不放图(图像白名单不含此目录)。
|
||||
TOP_DIRS = {"archive", "artwork", "design", "game", "sim-data", "tools", "reports"}
|
||||
# 2) 根目录文件白名单
|
||||
ROOT_FILES = {"AGENTS.md", "README.md", "roadmap.md", "stage.txt", "review-mode.txt", "playnote.md",
|
||||
ROOT_FILES = {"AGENTS.md", "README.md", "roadmap.md", "待办清单.md", "体验验证清单.md",
|
||||
"stage.txt", "review-mode.txt", "playnote.md",
|
||||
".gitignore", ".zcodeignore"}
|
||||
# 3) 图像允许目录(相对路径前缀;archive/ 为归档 zip 不扫)
|
||||
# game/tests/golden/(棋盘 golden 基线=机检测试资产非美术素材,素材齐盘态生成入库【基线换代权限 Q6】)
|
||||
|
||||
@@ -49,7 +49,7 @@ for _a in "$@"; do
|
||||
done
|
||||
|
||||
# 控制台编码:本仓库闸件输出中文与 ✅/✗ 符号。Windows GBK 控制台下 python 打印这些字符会抛
|
||||
# UnicodeEncodeError——06/10 两闸直接崩(假红)、09 的 FAIL 行被写成乱码字节(指纹漂移,
|
||||
# UnicodeEncodeError——待办清单/10 两闸直接崩(假红)、09 的 FAIL 行被写成乱码字节(指纹漂移,
|
||||
# 与基线比必判「红项清单变化」假红)、末尾汇总自身崩掉打不出结论。
|
||||
export PYTHONIOENCODING=utf-8
|
||||
|
||||
@@ -183,8 +183,11 @@ gate "06 gdd_lint" "gdd_lint.log" "$PY" "$REPO/tools/gdd_lint.py"
|
||||
gate "06b 变更记录节" "changelog.log" "$PY" "$REPO/tools/gdd_lint.py" --changelog-only
|
||||
gate "07 doc_link" "doc_link.log" "$PY" "$REPO/tools/doc_link_audit.py" --strict
|
||||
gate "08 doc_term" "doc_term.log" "$PY" "$REPO/tools/doc_term_audit.py" --strict
|
||||
# ── 8c. 流水线闸(06 条目状态机+roadmap 迭代总表+一次交叉核,判据=doc-conventions §12)──
|
||||
# ── 8c. 流水线闸(待办清单条目状态机+roadmap 迭代总表+一次交叉核,判据=doc-conventions §12)──
|
||||
gate "08c 流水线闸" "pipeline.log" "$PY" "$REPO/tools/pipeline_gate.py"
|
||||
# 8d 引用可核(tools/doc_ref_audit.py,2026-10-21 交付)暂不注册:新闸件无基线记录必判「闸况变化」拦回执,
|
||||
# 而建档要改 game/tests/.gates/baseline.json——触 game/ 即要求代码域全量测试(process-conventions §3.10 推域口径),
|
||||
# 文档域批按测试两入口铁规零跑游戏测试。注册与建档随下一个触 game/ 的批次一并做(待办清单在册)。
|
||||
fi
|
||||
# ── 6c. 回放确定性对拍(条件闸件;属代码域)──
|
||||
if in_domain code; then
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""技能池出招节奏审计(消费 playtest_*.jsonl 的 enemy_action 行)。
|
||||
|
||||
对应 06 待办「单拍行为环的体验验证(改型:技能池节奏体验验证)」的机器半边。
|
||||
对应待办清单「单拍行为环的体验验证(改型:技能池节奏体验验证)」的机器半边。
|
||||
判读带:
|
||||
· 过涝 = 敌群连续出特殊技、无普攻基底——读数 = 单敌群同场战斗内最长连续特殊技连击
|
||||
(连击 ≥3 记过涝信号,连击阈值 3 为代拍板默认:保底 M=4 与冷却 2-4 语义下 2 连属正常波动)
|
||||
|
||||
Reference in new issue
Block a user