Files
gplan-2/tools/extract_checks.py
DKChan 9c36c7a237 工程(工程清理批·用户令): 六件直改合批——①palette_check基线建档+双门②机检化: golden/palette_baseline两PNG(token92色∪素材快照1274色=并集1346色)+gen_palette_baseline.py建档刷新件+闸脚本05b色板闸(code域,quick/影响面SKIP)+头注基线纪律, 门①语义检查与48计数上限不承载(口径归art-bible owner,呈报); ②extract_checks补check_quiet静默通道: CALL_RE/DEF_RE扩双通道+collect_call按匹配长偏移+quiet列记channel:check_quiet, 自证三重(双计等式2155==2155/新旧对拍1719→2133=+414入账/往返--diff PASS零差异), test-conventions§5.2局限备案句随批换代; ③样张S1b四色墨×PANEL_DATA垫牌≥4.5:1色值对表断言(读数4.97–5.62与审计复算逐位一致, 样张31/31全绿; V10基线对拍图artwork环境件--save-baseline重建); ④main.gd死preload清理10条(登记8条复核多出2条, 7具名消费面已迁main_ui_build拆件自持, 逐条grep复核零代码使用), presentation 17套件全绿+冒烟15项0失败兜底; ⑤注释换代: ui_kit TEX_HERO_PORTRAIT「48px档」→原稿分辨率+显示档读PORTRAIT_DISPLAY_H, path_check头注删日期批注记; ⑥CAMP_HEADER_BAND退役评估: 4处消费面核过(关卡屏2/商店1/KeystoneBadge工厂1), 退役删除不成立, 处置=改名中性化归ui_kit令牌批顺带(零值变化); ⑦run_all_gates 02传参核查=d32ff342已完成漏删零改动; 待办清单7条目整条删除(状态机出口), pipeline_gate PASS(33条目/49总表行); 产出物=reports/工程改动报告-工程清理批-2026-11-02.md
2026-11-02 06:29:27 +08:00

607 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""extract_checks.py — 断言静态提取器(验证集清单生成与出口对账)
机器提取宿主脚本的全部静态断言调用点(每个调用点记录:所在文件、所在函数、
行号、标签字符串字面量原文、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 <源文件>...]
只打印函数清单与各函数条数(编账本辅助,不写文件)
断言源(--src 可多个;缺省=三宿主遍历):
game/tests/unit/**/*.gd + game/tests/integration/**/*.gd + game/tests/system_test.gd
批 B 出口对账用显式模式(--src 列全本批宿主文件)——缺省遍历含既有套件的 Kit.check
断言,对账时以「新增」出现(非迁移面),出口判定以显式模式为准。
提取口径:
- 静态断言调用点,三通道都认:smoke/系统宿主的 check(cond, label)(cond 在前)、
单元/集成宿主的 Kit.check("label", cond)(点前缀=标签在前)、静默收集通道
Kit.check_quiet(cond, label)(cond 在前,条目 quiet 列记 channel:check_quiet);
`_check(` 等同名前缀调用非标准通道,不取。
- 调用点可跨行:从调用名起括号配平归集完整实参文本(() [] {} 三种括号都计深度);
字符串字面量内的括号/逗号/井号、行尾注释、转义序列均不参与结构判定(码位掩码单趟预扫)。
- cond 摘要=cond 原文跨行折叠为单行(连续空白并一空格);标签原文=标签实参里的字符串
字面量逐字保真(含引号与转义序列原样,禁改写);标签实参若带格式化/拼接后缀
(`"..." % [...]`),后缀折叠后以「↩」接排在标签列。
- md 表格单元格内 `|` 转义为 `\|`、`\` 转义为 `\\`(源码串内无这两序列的歧义形态,读回可还原)。
--diff 对账规则:只比对「标签+cond+计数」三要素(合同 §五出口机检);行号随迁移漂移、
序号重排、白话释义/处置列由维护回填,均不参与比对。对不上的条目分三类报告:
消失(清单有现码无)、新增(现码有清单无)、成对字面变更(cond 同标签异=标签变更;
标签同 cond 异=cond 变更——一条「消失」与一条「新增」同因一字之改时按成对呈现)。
退出码:0=全对齐;1=有差异;2=用法错或文件不可读。
"""
import argparse
import bisect
import re
import subprocess
import sys
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# 断言源缺省=三宿主遍历:unit/**+integration/** 两目录递归 + 系统宿主单文件。
# 遍历面含既有套件的 Kit.check 断言(非本批迁移面,对账呈「新增」)——出口对账用 --src 显式列宿主。
DEFAULT_TREES = (
ROOT / "game" / "tests" / "unit",
ROOT / "game" / "tests" / "integration",
ROOT / "game" / "tests" / "system_test.gd",
)
DEFAULT_OUT = ROOT / "game" / "tests" / "验证集清单.md"
def default_sources():
"""三宿主遍历:两目录递归 *.gd + 系统宿主单文件,路径排序定序。"""
files = []
for p in DEFAULT_TREES:
if p.is_file():
files.append(p)
elif p.is_dir():
files.extend(sorted(p.rglob("*.gd")))
return sorted(files)
CALL_RE = re.compile(r"check(?:_quiet)?\(")
DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check(?:_quiet)?\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, call_len):
"""从调用名起点起括号配平归集调用。返回 (body, end_pos);不配平返回 None。"""
i = start + call_len
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/check_quiet)。返回(条目列表, 函数表),条目保持源码顺序。"""
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 # 字符串字面量内 / 注释里的调用名字样
ln = line_of(m.start())
if DEF_RE.match(lines[ln - 1]):
continue # func check / func check_quiet 定义行
prev = joined[m.start() - 1] if m.start() > 0 else ""
if prev and (prev.isalnum() or prev == "_"):
continue # `_check(` 等同名前缀——非标准通道
is_quiet = m.group(0).startswith("check_quiet")
if is_quiet:
label_first = False # Kit.check_quiet(cond, label)——cond 在前
else:
label_first = prev == "." # Kit.check("label", cond)——点前缀=标签在前
body, end_pos = collect_call(joined, m.start(), len(m.group(0)))
if body is None:
raise SystemExit("[extract_checks] FAIL:%s 行 %d 起的断言调用括号不配平到文件尾——先查解析缺口"
% (src_path.name, ln))
args = split_args(body)
if len(args) not in (2, 3):
raise SystemExit(
"[extract_checks] FAIL:%s 行 %d 的断言调用拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑"
% (src_path.name, ln, len(args)))
if label_first:
label_expr, cond = args[0], args[1]
else:
cond, label_expr = args[0], args[1]
quiet = "channel:check_quiet" if is_quiet else (args[2] if len(args) == 3 else "")
lm = LITERAL_RE.search(label_expr)
if lm is None:
continue # 标签实参无字面量(wrapper 直传变量——既有套件先例 reward_overlay_test _req)
# 不取:无可指纹化面;冻结面对账不受影响(字面量损坏会以「消失」呈现)
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 extract_all(src_paths):
"""多文件提取合并。返回(条目列表, {相对路径: 函数表});条目附加 src=相对路径。"""
entries = []
funcs_by_src = {}
for p in src_paths:
es, funcs = extract(p)
rel = p.relative_to(ROOT).as_posix() if p.is_relative_to(ROOT) else p.as_posix()
for e in es:
e["src"] = rel
entries.extend(es)
funcs_by_src[rel] = funcs
return entries, funcs_by_src
def label_cell(e):
"""标签列:字面量 ↩ 后缀(后缀空则只有字面量)。"""
sfx = fold(e["label_suffix"])
return e["label_literal"] + (" ↩ " + sfx if sfx else "")
def entry_key(e):
"""对账指纹=合同 §五三要素:cond 折叠 + 标签字面量(逐字)+ 后缀折叠。
quiet 不入指纹——它是 smoke 通道的打印档位(只影响 PASS 行打印,计数/红绿同口径),
Kit 通道无此参,迁移时按接线注记落通道转换;quiet 列保留展示,不参与对账。"""
return (fold(e["cond"]), e["label_literal"], fold(e["label_suffix"]))
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_by_src, src_names, carry=None) -> str:
"""生成验证集清单。冻结列=机器提取;「白话释义/处置」两列:承接命中行用旧清单回填,
其余落(待填)由维护回填(回填不动冻结列)。多源按(源文件, 函数)分章——跨文件同名
函数不并章;章头行号=该源文件内行号。头注全部由当次数据生成,禁批次专名与写死计数。"""
carry = carry or {}
out = []
out.append("# 验证集清单(断言快照·冻结)")
out.append("")
out.append("> 生成日期:%s · 基点 commit:`%s` · 源:%s" % (date.today().isoformat(), base_commit(), "、".join(src_names)))
out.append("> 提取口径:静态 `check(`/`check_quiet(` 调用点,括号配平归集(可跨行);缺省源=三宿主遍历")
out.append("> (unit/**+integration/**+system_test.gd)。本基点实测 %d 个静态调用点;行级" % len(entries))
out.append("> `grep -c \"check(\"` 是另一口径(含 `func check(` 定义行与注释行),两口径各自浮动,")
out.append("> 禁互相换算(tests/README「断言计数双口径」)。运行时计数大于静态计数=循环展开条目。")
out.append("> 生成命令:`python tools/extract_checks.py --extract`(可复现;重新生成须 --force;")
out.append("> 承接旧清单回填列须 --carry-from <旧清单>)。")
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 §8.3 维护锚)。")
out.append("")
seq = 0
by_gf = []
for e in entries:
key = (e["src"], e["func"])
if by_gf and by_gf[-1][0] == key:
by_gf[-1][1].append(e)
else:
by_gf.append((key, [e]))
for idx, ((src, fn), es) in enumerate(by_gf, 1):
start, end = es[0]["line"], es[-1]["end_line"]
out.append("## %s %s(行 %d–%d · %d 条 · 源 %s · 去向:待分类回填)" % (pad3(idx), fn, start, end, len(es), src))
out.append("")
out.append("| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet |")
out.append("|---|---|---|---|---|---|---|")
for e in es:
seq += 1
hit = carry.get((fold(e["cond"]), e["label_literal"], fold(e["label_suffix"])))
disp = esc_cell(hit[0]) if hit else "(待填)"
act = esc_cell(hit[1]) if hit else ""
out.append("| %d | %d | %s | %s | %s | %s | %s |" % (
seq, e["line"], esc_cell(label_cell(e)), esc_cell(fold(e["cond"])), disp, act,
esc_cell(fold(e["quiet"]))))
out.append("")
out.append("---")
out.append("全源共 %d 章、%d 条(静态调用点口径)。" % (len(by_gf), 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])
disp = unesc_cell(cells[4])
action = unesc_cell(cells[5])
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,
"disp": disp,
"action": action,
})
return entries
CARRY_RENAME = ("node_map_templates_test.gd", "node_map_determinism_test.gd")
def carry_cells(frozen_rows):
"""旧清单行 → 承接映射:指纹(与 entry_key 同构)→(白话释义, 处置)。
承接文本内的旧宿主文件名一并映射为新名(唯一在册更名对象)。"""
m = {}
for r in frozen_rows:
key = (fold(r["cond"]), r["label_literal"], fold(r["label_suffix"]))
m[key] = (r.get("disp", "").replace(*CARRY_RENAME), r.get("action", "").replace(*CARRY_RENAME))
return m
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_paths) -> int:
cur, _funcs = extract_all(src_paths)
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 = 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 现提取 %d 个源文件" % (list_path.name, len(src_paths)))
print(" 清单条数=%d 现提取条数=%d %s" % (
len(frozen), len(cur), "(计数对齐)" if len(frozen) == len(cur) else "(计数不齐)"))
def show(k, tag, q=""):
cond, lit, sfx = 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, "-", frozen_keys[old][0].get("quiet", ""))
show(new, "+", cur_keys[new][0].get("quiet", ""))
if cond_changes:
ok = False
print(" cond 字面变更 %d 对(标签同、cond 异):" % len(cond_changes))
for old, new in cond_changes:
show(old, "-", frozen_keys[old][0].get("quiet", ""))
show(new, "+", cur_keys[new][0].get("quiet", ""))
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, "", frozen_keys[k][0].get("quiet", ""))
if rest_added:
ok = False
print(" 新增 %d 条(现码有、清单无):" % len(rest_added))
for k in rest_added:
es = cur_keys[k][0]
print(" + [src %s func %s @%d]" % (es["src"], es["func"], es["line"]), end="")
show(k, "", es.get("quiet", ""))
if ok:
print("[extract_checks] PASS——标签/cond/计数三对齐,零差异")
return 0
print("[extract_checks] FAIL——见上差异清单")
return 1
def list_funcs(entries, funcs_by_src):
for src, funcs in funcs_by_src.items():
by_func = []
for e in entries:
if e["src"] != src:
continue
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])
print("=== %s ===" % src)
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 resolve_sources(src_args):
"""--src 列表 → Path 列表(相对路径按仓库根解析);缺省=三宿主遍历。任一文件不可读即 None。"""
if src_args:
paths = [Path(s) if Path(s).is_absolute() else ROOT / s for s in src_args]
else:
paths = default_sources()
if not paths:
print("[extract_checks] FAIL:断言源为空(三宿主遍历无命中?)")
return None
for p in paths:
if not p.is_file():
print("[extract_checks] FAIL:源文件不可读:%s" % p)
return None
return paths
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="断言静态提取器(清单生成/对账)")
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", nargs="+", default=None,
help="断言源文件(可多个;缺省=三宿主遍历 unit/**+integration/**+system_test.gd)")
ap.add_argument("--out", default=str(DEFAULT_OUT), help="--extract 输出路径")
ap.add_argument("--force", action="store_true", help="输出文件已存在时允许覆盖")
ap.add_argument("--carry-from", default=None, metavar="旧清单",
help="--extract 时按指纹承接旧清单「白话释义/处置」两列(旧宿主文件名映射为新名)")
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_paths = resolve_sources(args.src)
if src_paths is None:
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
carry = {}
if args.carry_from:
cf = Path(args.carry_from)
if not cf.is_file():
print("[extract_checks] FAIL:--carry-from 旧清单不可读:%s" % cf)
return 2
old_rows = parse_list(cf)
carry = carry_cells(old_rows)
print("[extract_checks] 旧清单 %d 行 → 承接指纹 %d 个" % (len(old_rows), len(carry)))
entries, funcs_by_src = extract_all(src_paths)
out.write_text(render_list(entries, funcs_by_src, [p.name for p in src_paths], carry), encoding="utf-8", newline="\n")
hitn = sum(1 for e in entries
if (fold(e["cond"]), e["label_literal"], fold(e["label_suffix"])) in carry)
print("[extract_checks] 已生成 %s(%d 个源文件、%d 个调用点、承接回填 %d 行)"
% (out, len(src_paths), len(entries), hitn))
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_paths)
entries, funcs_by_src = extract_all(src_paths)
list_funcs(entries, funcs_by_src)
return 0
if __name__ == "__main__":
sys.exit(main())