feat(测试迁移): 批 B 波 0 基建——断言静态提取器+验证集清单冻结+迁移账本

tools/extract_checks.py:smoke_all.gd 静态 check( 调用点提取器(括号配平归集可跨行调用,
码位掩码排字符串/注释内字样)——--extract 生成清单(已存在拒写防误覆)、--diff 逐条对账
(消失/新增/成对字面变更三类报告,批 B 出口机检唯一工具)、--list-funcs 编账辅助。
基点实测调用点 1065;行级 grep=1067 系另一口径(多 2 行=func check 定义行+一处注释行),
清单头注已写明。对刚生成清单 --diff 自测零差异;释义/处置列回填后复跑仍零差异。

game/tests/验证集清单.md(冻结参照物·常驻):77 章 1065 条,标签原文与 cond 摘要逐字
保真(cond 跨行折叠单行口径),每条人工白话释义+处置列(去向)已回填;章节含行段/条数/
去向/施工组。过 static_scan S11(205 文件零命中,唯一退役词字面标签与「去重闸」同行被
既有豁免覆盖,未新增豁免条款)。

reports/批B-断言迁移账本-2026-10-22.md:77 套分类账(系统 6 套 49 条/集成 1 套 14 条/
单元 70 套 1002 条)+12 施工组+套级变异策略+宿主落位计划(11 个新宿主预定路径与
run_all.gd manifest/system_test.gd 挂载方式,本波零建文件)+歧义套 8 项留主会话裁决
+引用普查附录(合同在案必改点逐条核实+合同外新发现 18 类,含产品码
tune_sim_controller.gd preload smoke 库件一处)。

smoke_all.gd 零改动(git diff 空);三个去向既有测试文件零改动;本波零游戏测试。
This commit is contained in:
DKChan committed 2026-10-22 23:18:20 +08:00
1 parent 1aea93972b
commit 771f871527
3 files changed
+2175

No files matched your search

+494
View File
@@ -0,0 +1,494 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""extract_checks.py — smoke_all.gd 断言静态提取器(验证集清单生成与出口对账)
机器提取 game/smoke/smoke_all.gd 的全部静态 check( 调用点(每个调用点记录:所在函数、
行号、标签字符串字面量原文、cond 表达式原文),提供两种模式:
用法:
python tools/extract_checks.py --extract [--src <源文件>] [--out <清单文件>] [--force]
生成验证集清单(已存在时拒写,除非 --force——冻结参照物防误覆)
python tools/extract_checks.py --diff <清单文件> [--src <源文件>]
对当前源码再提取,与清单逐条比对——批 B 出口对账唯一工具
python tools/extract_checks.py --list-funcs [--src <源文件>]
只打印函数清单与各函数条数(编账本辅助,不写文件)
提取口径:
- 静态 check( 调用点。与行级 grep(grep -c "check(")的差:grep 数含 func check 定义行
与一处注释行(「禁恒真 check(true)」),行级 grep=1067、真实调用点=1065(2026-10-22 基点实测)。
- 调用点可跨行:从 `check(` 起括号配平归集完整实参文本(() [] {} 三种括号都计深度);
字符串字面量内的括号/逗号/井号、行尾注释、转义序列均不参与结构判定(码位掩码单趟预扫)。
- cond 摘要=cond 原文跨行折叠为单行(连续空白并一空格);标签原文=第二个实参里的字符串
字面量逐字保真(含引号与转义序列原样,禁改写);标签实参若带格式化/拼接后缀
(`"..." % [...]`),后缀折叠后以「↩」接排在标签列。
- md 表格单元格内 `|` 转义为 `\|`、`\` 转义为 `\\`(源码串内无这两序列的歧义形态,读回可还原)。
--diff 对账规则:只比对「标签+cond+计数」三要素(合同 §五出口机检);行号随迁移漂移、
序号重排、白话释义/处置列由维护回填,均不参与比对。对不上的条目分三类报告:
消失(清单有现码无)、新增(现码有清单无)、成对字面变更(cond 同标签异=标签变更;
标签同 cond 异=cond 变更——一条「消失」与一条「新增」同因一字之改时按成对呈现)。
退出码:0=全对齐;1=有差异;2=用法错或文件不可读。
"""
import argparse
import bisect
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_SRC = ROOT / "game" / "smoke" / "smoke_all.gd"
DEFAULT_OUT = ROOT / "game" / "tests" / "验证集清单.md"
CALL_RE = re.compile(r"check\(")
DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check\s*\(")
FUNC_RE = re.compile(r"^(static\s+)?func\s+([A-Za-z_][A-Za-z0-9_]*)")
LITERAL_RE = re.compile(r'"((?:[^"\\]|\\.)*)"')
OPENERS = "([{"
CLOSERS = ")]}"
CLOSER_OF = {")": "(", "]": "[", "}": "{"}
def fold(text: str) -> str:
"""跨行折叠:连续空白(含换行/制表/回车)并为一空格。对账归一口径。"""
return re.sub(r"\s+", " ", text).strip()
def esc_cell(text: str) -> str:
return text.replace("\\", "\\\\").replace("|", "\\|")
def unesc_cell(text: str) -> str:
"""esc_cell 的逆。源码串内无 `\\|`/`\\\\` 歧义形态(提取口径节),还原无歧义。"""
out = []
i = 0
while i < len(text):
if text[i] == "\\" and i + 1 < len(text):
out.append(text[i + 1])
i += 2
else:
out.append(text[i])
i += 1
return "".join(out)
def code_mask(joined: str) -> bytearray:
"""单趟预扫:1=码位(非字符串内部、非注释)。字符串与行尾注释位置置 0。"""
mask = bytearray(len(joined))
in_str = False
i = 0
n = len(joined)
while i < n:
ch = joined[i]
if in_str:
mask[i] = 0
if ch == "\\" and i + 1 < n:
mask[i + 1] = 0
i += 2
continue
if ch == '"':
in_str = False
i += 1
continue
if ch == '"':
in_str = True
mask[i] = 0
i += 1
continue
if ch == "#":
nl = joined.find("\n", i)
end = n if nl == -1 else nl
for j in range(i, end):
mask[j] = 0
i = end
continue
mask[i] = 1
i += 1
return mask
def func_table(lines):
"""顶层函数表:[(name, start_line_1based)]。lambda 归宿主函数。"""
funcs = []
for i, line in enumerate(lines, 1):
m = FUNC_RE.match(line)
if m:
funcs.append((m.group(2), i))
return funcs
def func_at(funcs, lineno):
name = "<module>"
for fn, start in funcs:
if start <= lineno:
name = fn
else:
break
return name
def split_args(body):
"""按括号配平拆实参(顶层逗号分隔)。body=check( 与最外层 ) 之间的文本(换行已并空格)。"""
args = []
depth = {"(": 0, "[": 0, "{": 0}
cur = []
in_str = False
i = 0
while i < len(body):
ch = body[i]
if in_str:
if ch == "\\" and i + 1 < len(body):
cur.append(body[i])
cur.append(body[i + 1]) # 转义序列原样入账
i += 2
continue
if ch == '"':
in_str = False
cur.append(ch)
elif ch == '"':
in_str = True
cur.append(ch)
elif ch in depth:
depth[ch] += 1
cur.append(ch)
elif ch in CLOSERS:
depth[CLOSER_OF[ch]] -= 1
cur.append(ch)
elif ch == "," and depth["("] == 0 and depth["["] == 0 and depth["{"] == 0:
args.append("".join(cur))
cur = []
else:
cur.append(ch)
i += 1
args.append("".join(cur))
return args
def collect_call(joined, start):
"""从 check( 的 start 起括号配平归集调用。返回 (body, end_pos);不配平返回 None。"""
i = start + len("check(")
depth = 1
in_str = False
parts = []
n = len(joined)
while i < n:
ch = joined[i]
if in_str:
if ch == "\\":
parts.append(joined[i:i + 2])
i += 2
continue
if ch == '"':
in_str = False
parts.append(ch)
elif ch == '"':
in_str = True
parts.append(ch)
elif ch in OPENERS:
depth += 1
parts.append(ch)
elif ch in CLOSERS:
depth -= 1
if depth == 0:
return "".join(parts), i
parts.append(ch)
elif ch == "#":
nl = joined.find("\n", i)
i = n if nl == -1 else nl # 行尾注释不进 body(并一空格)
parts.append(" ")
continue
else:
parts.append(ch if ch != "\n" else " ")
i += 1
return None, i
def extract(src_path: Path):
"""提取全部静态 check( 调用点。返回(条目列表, 函数表),条目保持源码顺序。"""
src = src_path.read_text(encoding="utf-8")
lines = src.split("\n")
funcs = func_table(lines)
offsets = []
total = 0
for line in lines:
offsets.append(total)
total += len(line) + 1
joined = "\n".join(lines)
mask = code_mask(joined)
def line_of(pos):
i = bisect.bisect_right(offsets, pos) - 1
return i + 1
entries = []
for m in CALL_RE.finditer(joined):
if not mask[m.start()]:
continue # 字符串字面量内 / 注释里的 check( 字样
ln = line_of(m.start())
if DEF_RE.match(lines[ln - 1]):
continue # func check 定义行
body, end_pos = collect_call(joined, m.start())
if body is None:
raise SystemExit("[extract_checks] FAIL:行 %d 起的 check( 括号不配平到文件尾——先查解析缺口" % ln)
args = split_args(body)
if len(args) not in (2, 3):
raise SystemExit(
"[extract_checks] FAIL:行 %d 的 check( 拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑"
% (ln, len(args)))
cond = args[0]
label_expr = args[1]
quiet = args[2] if len(args) == 3 else ""
lm = LITERAL_RE.search(label_expr)
if lm is None:
raise SystemExit(
"[extract_checks] FAIL:行 %d 的标签实参不含字符串字面量——解析缺口,先查再跑" % ln)
entries.append({
"line": ln,
"end_line": line_of(min(end_pos, len(joined) - 1)),
"func": func_at(funcs, ln),
"label_literal": lm.group(0), # 含引号,原文逐字
"label_suffix": label_expr[lm.end():], # 格式化/拼接后缀(通常为空或 % ...)
"cond": cond,
"quiet": quiet,
})
return entries, funcs
def label_cell(e):
"""标签列:字面量 ↩ 后缀(后缀空则只有字面量)。"""
sfx = fold(e["label_suffix"])
return e["label_literal"] + (" ↩ " + sfx if sfx else "")
def entry_key(e):
"""对账指纹:cond 折叠 + 标签字面量(逐字)+ 后缀折叠 + quiet。"""
return (fold(e["cond"]), e["label_literal"], fold(e["label_suffix"]), fold(e["quiet"]))
def pad3(n):
return "S%02d" % n if n < 100 else "S%03d" % n
def base_commit():
try:
r = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(ROOT),
capture_output=True, text=True, encoding="utf-8")
if r.returncode == 0:
return r.stdout.strip()
except OSError:
pass
return "<git 不可用,未取得基点 commit>"
def render_list(entries, funcs, src_rel: str) -> str:
"""生成验证集清单骨架。白话释义/处置两列留待回填(回填不动冻结列)。"""
out = []
out.append("# 验证集清单(迁移参照物·冻结)")
out.append("")
out.append("> 生成日期:2026-10-22 · 基点 commit:`%s` · 源:`%s`" % (base_commit(), src_rel))
out.append("> 提取口径:静态 `check(` 调用点,括号配平归集(可跨行)。基点实测调用点 1065 处;")
out.append("> 行级 `grep -c \"check(\"` = 1067 是另一口径——多出的 2 行为 `func check(` 定义行与")
out.append("> 一处注释行(「禁恒真 check(true)」),非调用点。运行时计数大于静态计数=循环展开条目,")
out.append("> 两口径各自浮动,禁互相换算(tests/README「断言计数双口径」)。")
out.append("> 生成命令:`python tools/extract_checks.py --extract`(可复现;重新生成须 --force)。")
out.append("> 冻结纪律:每条的「标签原文」「cond 摘要」「quiet」三列为机器提取逐字保真,禁任何改写;")
out.append("> cond 摘要=原文跨行折叠单行(连续空白并一空格),单元格内 `|`→`\\|`、`\\`→`\\\\` 转义,读回还原。")
out.append("> 「白话释义」「处置」两列由维护回填,不属冻结面。")
out.append("> 出口对账:`python tools/extract_checks.py --diff game/tests/验证集清单.md`")
out.append("> ——标签/cond/计数三对齐才算迁移完(合同 §五出口机检)。")
out.append("> 维护义务:此后断言新增/删除/改判必须同步本清单(test-conventions §9.3 锚)。")
out.append("")
seq = 0
by_func = []
for e in entries:
if by_func and by_func[-1][0] == e["func"]:
by_func[-1][1].append(e)
else:
by_func.append((e["func"], [e]))
for idx, (fn, es) in enumerate(by_func, 1):
start, end = es[0]["line"], es[-1]["end_line"]
out.append("## %s %s(行 %d–%d · %d 条 · 去向:待分类回填)" % (pad3(idx), fn, start, end, len(es)))
out.append("")
out.append("| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet |")
out.append("|---|---|---|---|---|---|---|")
for e in es:
seq += 1
out.append("| %d | %d | %s | %s | (待填) | | %s |" % (
seq, e["line"], esc_cell(label_cell(e)), esc_cell(fold(e["cond"])), esc_cell(fold(e["quiet"]))))
out.append("")
out.append("---")
out.append("全源共 %d 章、%d 条(静态调用点口径)。" % (len(by_func), seq))
return "\n".join(out) + "\n"
CELL_SPLIT_RE = re.compile(r"(?<!\\)\|")
def parse_list(path: Path):
"""读回清单:返回条目列表(键与 entry_key 同构;section 供报告归组)。"""
entries = []
cur_section = None
for i, line in enumerate(path.read_text(encoding="utf-8").split("\n"), 1):
if line.startswith("## "):
m = re.match(r"## \S+ (\S+)(行", line)
cur_section = m.group(1) if m else line[3:].strip()
continue
if not line.startswith("|"):
continue
cells = [c.strip() for c in CELL_SPLIT_RE.split(line)[1:-1]]
if len(cells) != 7 or not cells[0].isdigit() or cells[0] == "序号":
continue # 表头/分隔行
label_raw = unesc_cell(cells[2])
cond = unesc_cell(cells[3])
quiet = unesc_cell(cells[6])
lit_m = LITERAL_RE.match(label_raw)
literal = lit_m.group(0) if lit_m else label_raw
suffix = label_raw[lit_m.end():].strip() if lit_m else ""
suffix = suffix[1:].strip() if suffix.startswith("↩") else suffix
entries.append({
"md_line": i,
"section": cur_section,
"label_literal": literal,
"label_suffix": suffix,
"cond": cond,
"quiet": quiet,
})
return entries
def _short(s, n=80):
s = s.replace("\n", " ")
return s if len(s) <= n else s[:n] + "…"
def diff_report(list_path: Path, src_path: Path) -> int:
cur, _funcs = extract(src_path)
frozen = parse_list(list_path)
cur_keys = {}
for e in cur:
cur_keys.setdefault(entry_key(e), []).append(e)
frozen_keys = {}
for e in frozen:
frozen_keys.setdefault(entry_key(e), []).append(e)
missing = [k for k in frozen_keys if k not in cur_keys]
added = [k for k in cur_keys if k not in frozen_keys]
# 成对归类:cond 同→标签变更;标签同→cond 变更
label_changes, cond_changes = [], []
used_added = set()
rest_missing = []
for k in missing:
cond, lit, sfx, _q = k
pair = next((ka for ka in added if ka not in used_added and ka[0] == cond), None)
if pair is not None:
used_added.add(pair)
label_changes.append((k, pair))
continue
pair = next((ka for ka in added if ka not in used_added and ka[1] == lit and ka[2] == sfx), None)
if pair is not None:
used_added.add(pair)
cond_changes.append((k, pair))
continue
rest_missing.append(k)
rest_added = [k for k in added if k not in used_added]
ok = True
print("[extract_checks] 对账:%s vs 现提取 %s" % (list_path.name, src_path.name))
print(" 清单条数=%d 现提取条数=%d %s" % (
len(frozen), len(cur), "(计数对齐)" if len(frozen) == len(cur) else "(计数不齐)"))
def show(k, tag):
cond, lit, sfx, q = k
s = " %s cond=%s label=%s" % (tag, _short(cond), _short(lit))
if sfx:
s += " suffix=%s" % _short(fold(sfx))
if q:
s += " quiet=%s" % fold(q)
print(s)
if label_changes:
ok = False
print(" 标签字面变更 %d 对(cond 同、标签异):" % len(label_changes))
for old, new in label_changes:
show(old, "-")
show(new, "+")
if cond_changes:
ok = False
print(" cond 字面变更 %d 对(标签同、cond 异):" % len(cond_changes))
for old, new in cond_changes:
show(old, "-")
show(new, "+")
if rest_missing:
ok = False
print(" 消失 %d 条(清单有、现码无):" % len(rest_missing))
for k in rest_missing:
print(" - [%s]" % frozen_keys[k][0].get("section", "?"), end="")
show(k, "")
if rest_added:
ok = False
print(" 新增 %d 条(现码有、清单无):" % len(rest_added))
for k in rest_added:
es = cur_keys[k][0]
print(" + [func %s @%d]" % (es["func"], es["line"]), end="")
show(k, "")
if ok:
print("[extract_checks] PASS——标签/cond/计数三对齐,零差异")
return 0
print("[extract_checks] FAIL——见上差异清单")
return 1
def list_funcs(entries, funcs):
by_func = []
for e in entries:
if by_func and by_func[-1][0] == e["func"]:
by_func[-1][1] = by_func[-1][1] + 1
else:
by_func.append([e["func"], 1])
checked = {f for f, _ in by_func}
for fn, n in by_func:
print("%s\t%d" % (fn, n))
print("--- 无断言函数(helper,随套迁移)---")
for fn, ln in funcs:
if fn not in checked:
print("%s\t@%d" % (fn, ln))
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="smoke_all.gd 断言静态提取器(清单生成/对账)")
ap.add_argument("--extract", action="store_true", help="生成验证集清单")
ap.add_argument("--diff", metavar="清单文件", help="与清单逐条对账(批 B 出口机检)")
ap.add_argument("--list-funcs", action="store_true", help="打印函数清单与条数(编账本辅助)")
ap.add_argument("--src", default=str(DEFAULT_SRC), help="断言源文件")
ap.add_argument("--out", default=str(DEFAULT_OUT), help="--extract 输出路径")
ap.add_argument("--force", action="store_true", help="输出文件已存在时允许覆盖")
args = ap.parse_args()
if sum(bool(x) for x in (args.extract, args.diff, args.list_funcs)) != 1:
print("[extract_checks] 用法:--extract / --diff <清单文件> / --list-funcs 三选一")
return 2
src_path = Path(args.src)
if not src_path.is_file():
print("[extract_checks] FAIL:源文件不可读:%s" % src_path)
return 2
if args.extract:
out = Path(args.out)
if out.exists() and not args.force:
print("[extract_checks] FAIL:清单已存在(%s)——冻结参照物防误覆,重生成须加 --force" % out)
return 2
entries, _funcs = extract(src_path)
out.write_text(render_list(entries, _funcs, src_path.name), encoding="utf-8", newline="\n")
print("[extract_checks] 已生成 %s(%d 个调用点)" % (out, len(entries)))
return 0
if args.diff:
lp = Path(args.diff)
if not lp.is_file():
print("[extract_checks] FAIL:清单不可读:%s" % lp)
return 2
return diff_report(lp, src_path)
entries, funcs = extract(src_path)
list_funcs(entries, funcs)
return 0
if __name__ == "__main__":
sys.exit(main())