diff --git a/game/tests/.gates/mut_snapshots/seal_polarity_test.gd b/game/tests/.gates/mut_snapshots/seal_polarity_test.gd new file mode 100644 index 00000000..01846251 --- /dev/null +++ b/game/tests/.gates/mut_snapshots/seal_polarity_test.gd @@ -0,0 +1,74 @@ +# tests/unit/battle_settlement/seal_polarity_test.gd — 封印与极性单元测试宿主 +# 断言面 = 八套(批 B 断言迁移自 smoke_all.gd,G5 施工组;八套按源编排序分八笔原子提交迁入): +# 末点 AP 封印轮末结算(playtest 缺陷②回归——action_economy 末点花费与轮终结) +# 封印不可逆四情况(禁撤封印 + 挖封印格兑付改判——minesweeper_rules 拒绝面) +# 清图奖励双份兑付链(真实流完美封印——orchestrator grant_clear 身份兑付) +# 清图奖励弱形式分支(零兑付 / 高光守卫 / 第二份拒——直调编排私有面构造) +# 轮内刷新 provenance 守卫(刷新图清图不发清图奖励) +# 极性路由 + 封印溢价 125%(挖安全格正向 / 挖危险格负向 / 标中标空兑付极性) +# 负向字典逐型直验(效果行 / heal 直伤 / blast 警觉 / action 行动点排队) +# 格面资源资格面(揭示事件 resource 标记 + 封锁格身份查询) +# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload, +# ConfigDB 显式实例化构造注入(各套自建 RngService——源套掷点隔离形态,宿主不持统一流); +# 表加载失败不中止(沿被迁断言面的源语义——诊断在此展开,表值变异的红由具体断言落,承 G2/G3/G4 同款)。 +# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/seal_polarity_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const ConfigDBScript := preload("res://core/config_db.gd") +const AutoPlayer := preload("res://smoke/auto_player.gd") # 自动玩家策略单一实现(迁移期按现路径 preload——账本 §五夹具前提) + +var _db + + +func _init() -> void: + _db = ConfigDBScript.new() + # 表加载失败不中止(承 G2/G3/G4 同款:表损坏的红落在具体断言上—— + # 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评) + var load_ok: bool = _db.load_tables("res://data/tables", true) + if not load_ok: + push_error("[seal_polarity_test] 表加载返回失败——诊断如下(红由具体断言落)") + for d in _db.diagnostics(): + if d.severity == "error" or d.severity == "fatal": + push_error(" [%s] %s: %s" % [d.severity, d.category, d.message]) + # 套执行序沿源编排序 + _smoke_seal_ap_exhausted() + quit(Kit.finish("seal_polarity_test")) + + +# ---- 末点 AP 封印轮末结算(playtest 缺陷②回归:末点 AP 花在封印必须终结轮次进入下一轮)---- + +func _smoke_seal_ap_exhausted() -> void: + print("--- 排雷玩法·封印动作耗尽末点 AP 回归 ---") + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(20260911) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) + var rounds: Array = [] + settlement.round_started.connect(func(ri): rounds.append(ri)) + Kit.check("回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)", + settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"})) + var minesweeper = settlement.minesweeper + # 恰花 (AP-1) 点挖掘,留末点给封印(表驱动——AP 从表读不写死) + var ap_total: int = int(_db.action_economy.ap_per_round) + for i in range(ap_total - 1): + if settlement.action_economy.phase != "Active" or settlement.action_economy.ap == 0: + break + var plan = AutoPlayer.choose_dig(minesweeper) + if plan.is_empty(): + break + if plan.hero_cell != minesweeper.hero_pos(): + minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) + minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) + Kit.check("%d 次挖掘后恰剩 1 AP(phase=%s ap=%d)" % [ap_total - 1, str(settlement.action_economy.phase), settlement.action_economy.ap], + settlement.action_economy.phase == "Active" and settlement.action_economy.ap == 1) + # 末点 AP 花在封印——轮次必须终结并结算进入第 2 轮 + # 封印目标 = 挖掘策略的下一目标格(构造保证:未开、与小人邻接——先把小人移到其邻接已开格) + var plan2 = AutoPlayer.choose_dig(minesweeper) + Kit.check("仍存在可挖目标(封印目标来源)", not plan2.is_empty()) + var out: Array = [] + if not plan2.is_empty(): + if plan2.hero_cell != minesweeper.hero_pos(): + minesweeper.settle_batch([{"action": "move", "cell": plan2.hero_cell}]) + out = minesweeper.settle_batch([{"action": "seal_place", "cell": plan2.dig_cell}]) + Kit.check("末点 AP 封印被受理", not out.is_empty() and bool(out[0].accepted)) + Kit.check("封印动作耗尽 AP 触发轮末结算并进入第 2 轮(rounds=%s)" % str(rounds), rounds.size() >= 2) diff --git a/game/tests/mutation/mutation_engine.gd b/game/tests/mutation/mutation_engine.gd new file mode 100644 index 00000000..19c5bdc4 --- /dev/null +++ b/game/tests/mutation/mutation_engine.gd @@ -0,0 +1,2173 @@ +# tests/mutation/mutation_engine.gd — 按条变异验证引擎(单进程·内存内变异·零触盘) +# 干什么:游戏环境启动一次,对每条断言执行「内存里改坏 → 只重跑该断言所在套函数 → +# 确认这条断言报警(FAIL)→ 撤销改坏 → 记录结果」,一条接一条;全程不改任何文件。 +# +# 三件武器(按套配置适配,见 mutation_plan.gd): +# table 表/资源改写——改已加载 Resource 的内存字段,跑完按原值回写; +# inject 注入编译——被测生产脚本源码字符串在内存做最小变换(数值±1/比较符翻转/ +# 布尔翻转/键错位/置空等形态),GDScript.reload() 原地重编译(零触盘), +# 套函数内 load()/preload() 到的类即变异体,跑完全量还原源码再 reload; +# state 状态篡改——直接改运行中对象的内存状态:宿主长活成员(_db 等)、 +# 生产脚本静态变量与成员初值、autoload 实例成员与场景树节点名。 +# 改坏点自动生成:按计划里该套守的生产模块枚举全部候选改坏,逐个试跑该套函数, +# 扫到能让目标断言翻 FAIL 的改坏即记(摊销扫描:一次试跑同时判定套内全部未杀条目); +# 随后逐条复验(该断言的改坏点单独跑一轮确认红)出逐条台账记录。 +# +# 用法(工作目录 = game/): +# godot --headless --path . --script res://tests/mutation/mutation_engine.gd -- --all +# godot ... -- --host=board_gen_reward # 指定宿主全部套 +# godot ... -- --suite=seal_polarity:_smoke_seal_ap_exhausted +# godot ... -- --snap=seal_polarity=<快照绝对或 res 路径> # 物化迁移中宿主的提交快照 +# godot ... -- --ledger --out=res://reports/批B-变异统一台账.md +# 可选:--resume(跳过已有结果 JSON 的套)· --budget=N · --cap-ms=N +# 退出码:0=全部条目验红(打不红的条全部进异常清单)· 1=存在未验红且不在异常清单 +# · 2=零条目(含无参数用法打印)。 +extends SceneTree + +const Plan := preload("res://tests/mutation/mutation_plan.gd") + +const _USER_GUARD_FILES := ["save.json", "save_slot_1.json", "save_slot_2.json", "save_slot_3.json"] +const _RESULT_DIR := "user://mutation_work/results" + +var _args := {} +var _jobs: Array = [] # [[host_id, suite_name], ...] +var _adhoc_hosts := {} # id -> cfg(--adhoc 零配置宿主:套/表/模块从宿主源码自动推导) +var _orig_src := {} # 路径 -> 原始源码(宿主与模块件,FileAccess 原文) +var _orig_scripts := {} # 路径 -> 原件 GDScript 对象(进程内首捕;终态夺回路径用) +var _orig_variant := {} # 路径 -> 原件源码变体(无 class_name,编译一次反复接管——还原通道) +var _mod_cand_cache := {} # 模块路径 -> Array[候选](含 func 归属,未排序) +var _host_states := {} # host_id -> {cfg, scan, gds, inst} +var _user_backup := {} # user 档备份(文件名 -> 字节或 null) +var _mut_proxy: Object = null # 活树代理(root 引流+套创建节点记录) +var _mut_tops: Array = [] # 当前 trial 经代理入树的顶层节点(顺序) +var _mut_pending: Array = [] # 待施加树形模板(trial 内节点入树时按路径匹配施加) +var _mut_tree_templates: Array = [] # 基线轮挖出的树形状态模板(候选来源) +var _mut_recording := false # 基线轮记录开关 +var _started := false +var _done := false +var _t0_ms := 0 + + +# ============================ 主循环骨架 ============================ + +func _process(_delta: float) -> bool: + if not _started: + _started = true + _t0_ms = Time.get_ticks_msec() + _main() + return false + if _done: + return true + if Time.get_ticks_msec() - _t0_ms > Plan.GLOBAL_WATCHDOG_MS: + push_error("[mut] 全局看门狗到点,强制收束(已完成套的结果已落盘 %s)" % _RESULT_DIR) + quit(1) + return true + return false + + +func _main() -> void: + _parse_args() + if _args.has("help") or (_args.is_empty() and not _args.has("ledger-only")): + _print_usage() + quit(2) + _done = true + return + DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(_RESULT_DIR)) + var ledger_only: bool = _args.has("ledger-only") + if not ledger_only: + _build_adhoc_hosts() + _parse_jobs() + _apply_snaps() + _backup_user_save() + var n := _jobs.size() + var done := 0 + for job in _jobs: + done += 1 + print("[mut] ===== 套 %d/%d %s:%s =====" % [done, n, job[0], job[1]]) + await _run_job(String(job[0]), String(job[1])) + _restore_user_save() + var integrity := _verify_scripts_restored() + print("[mut] 全部套完成:jobs=%d · 生产/宿主源码还原核验=%s" % [n, "全还原" if integrity == "" else "异常:" + integrity]) + var code := 2 + if _args.has("ledger") or ledger_only: + code = _build_ledger(_ledger_out_path()) + else: + code = _exit_code_from_results() + quit(code) + _done = true + + +func _parse_args() -> void: + for a in OS.get_cmdline_user_args(): + if not a.begins_with("--"): + continue + var body := a.substr(2) + var eq := body.find("=") + var k := body if eq < 0 else body.substr(0, eq) + var v := "true" if eq < 0 else body.substr(eq + 1) + if k == "suite": + if not _args.has("suites"): + _args["suites"] = [] + _args["suites"].append(v) + elif k == "snap": + if not _args.has("snaps"): + _args["snaps"] = [] + _args["snaps"].append(v) + elif k == "adhoc": + if not _args.has("adhocs"): + _args["adhocs"] = [] + _args["adhocs"].append(v) + elif k == "excl-func": + if not _args.has("excl-funcs"): + _args["excl-funcs"] = [] + _args["excl-funcs"].append(v) + elif k == "excl-line": + if not _args.has("excl-lines"): + _args["excl-lines"] = [] + _args["excl-lines"].append(v) + else: + _args[k] = v + + +# --adhoc==<宿主 res 路径>:零配置宿主(主会话统一验证跑计划外宿主用)。 +# 套 = 有断言点且零必填参的函数(_init/_initialize 除外);表 = 源码 _db.<名> 访问词; +# 模块 = 源码 load/preload 的 res://*.gd;通道 Kit.check 在场=kit 否则 self;含 await=异步。 +# 武器三件全开(table 候选按推导表、inject 按推导模块;细化归计划条目——候选招数不全的 +# 套按台账异常清单补计划配置后重跑)。 +func _build_adhoc_hosts() -> void: + for spec in _args.get("adhocs", []): + var parts := String(spec).split("=", true, 1) + if parts.size() != 2: + push_error("[mut] --adhoc 规格非法(要 id=res://路径):%s" % String(spec)) + continue + var hid := String(parts[0]) + var src_path := String(parts[1]) + var src := FileAccess.get_file_as_string(src_path) + if src == "": + push_error("[mut] adhoc 宿主源码为空/缺失:%s" % src_path) + continue + var scan := _scan_source(src) + var re_table := RegEx.create_from_string("_db\\.([a-z][a-z_0-9]*)") + var tables := {} + for m in re_table.search_all(src): + tables[String(m.get_string(1))] = true + var re_mod := RegEx.create_from_string("(?:load|preload)\\(\\s*\"(res://[^\"]+\\.gd)\"") + var modules := {} + for m2 in re_mod.search_all(src): + var mp := String(m2.get_string(1)) + if mp != src_path: + modules[mp] = true + var by_func := {} + for s in scan["sites"]: + var fn := String(s["func"]) + if fn in ["_init", "_initialize", ""]: + continue + by_func[fn] = int(by_func.get(fn, 0)) + 1 + var suites := {} + for fn2 in by_func.keys(): + if not scan["func_ranges"].has(fn2) or _func_required_args(scan, fn2) > 0: + continue + suites[fn2] = {"count": int(by_func[fn2]), "weapons": ["table", "inject", "state"], + "tables": tables.keys(), "modules": modules.keys()} + if suites.is_empty(): + push_error("[mut] adhoc 宿主无可运行套(断言点全在有参 helper 里?):%s" % src_path) + continue + _adhoc_hosts[hid] = {"id": hid, "src": src_path, + "channel": "kit" if src.find("Kit.check") >= 0 else "self", + "async": src.find("await ") >= 0, "budget": 800, + "cap_ms": Plan.SUITE_CAP_MS_DEFAULT, "state_autoloads": [], "suites": suites} + var n := 0 + for s2 in suites.values(): + n += int(s2["count"]) + print("[mut] adhoc 宿主 %s(%d 套 %d 点 · 表 %d · 模块 %d)" % [hid, suites.size(), n, tables.size(), modules.size()]) + + +# 函数必填参数个数(带默认值的不计——零必填参才可作套函数直呼) +func _func_required_args(scan: Dictionary, fname: String) -> int: + var r: Dictionary = scan["func_ranges"][fname] + var head := String(scan["lines"][int(r["start"])]) + var open_i := head.find("(") + var close_i := head.rfind(")") + if open_i < 0 or close_i <= open_i: + return 0 + var n := 0 + for seg in head.substr(open_i + 1, close_i - open_i - 1).split(","): + var s := String(seg).strip_edges() + if s != "" and s.find("=") < 0: + n += 1 + return n + + +func _parse_jobs() -> void: + _jobs.clear() + if _args.has("all"): + for h in Plan.hosts(): + for s in h["suites"].keys(): + _jobs.append([h["id"], s]) + elif _args.has("host"): + var h := _plan_cfg(String(_args["host"])) + if h.is_empty(): + push_error("[mut] 未知宿主:%s" % String(_args["host"])) + else: + for s in h["suites"].keys(): + _jobs.append([h["id"], s]) + elif _args.has("suites"): + for spec in _args["suites"]: + var parts := String(spec).split(":", true, 1) + if parts.size() != 2 or _plan_cfg(parts[0]).is_empty(): + push_error("[mut] --suite 规格非法(要 宿主:套名):%s" % String(spec)) + continue + _jobs.append([parts[0], parts[1]]) + # --adhoc 宿主整host入队(仅在无 --all/--host/--suite 显式选面时;显式 --suite 仍可单挑其套) + var explicit := _args.has("all") or _args.has("host") or _args.has("suites") + if not explicit: + for hid in _adhoc_hosts.keys(): + for s in _adhoc_hosts[hid]["suites"].keys(): + _jobs.append([String(hid), String(s)]) + if String(_args.get("resume", "false")) == "true": + var kept: Array = [] + for job in _jobs: + if FileAccess.file_exists(_result_path(String(job[0]), String(job[1]))): + print("[mut] resume 跳过已有结果:%s:%s" % [job[0], job[1]]) + else: + kept.append(job) + _jobs = kept + + +func _apply_snaps() -> void: + for spec in _args.get("snaps", []): + var parts := String(spec).split("=", true, 1) + if parts.size() != 2: + continue + var h := _plan_cfg(parts[0]) + if h.is_empty(): + continue + var content := FileAccess.get_file_as_string(parts[1]) + if content == "": + push_error("[mut] 快照源为空:%s" % parts[1]) + continue + var dst: String = h["src"] + DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(dst).get_base_dir()) + var f := FileAccess.open(dst, FileAccess.WRITE) + f.store_string(content) + f.close() + print("[mut] 已物化宿主快照 %s <- %s(%d 行)" % [dst, parts[1], content.count("\n") + 1]) + + +func _print_usage() -> void: + print("按条变异验证引擎——用法:") + print(" godot --headless --path game --script res://tests/mutation/mutation_engine.gd -- --all") + print(" 可选:--host= · --suite=:<套名>(可多次)· --snap=<宿主id>=<快照路径>") + print(" --adhoc==<宿主 res:// 路径>(零配置宿主:套/表/模块自动推导,可多次)") + print(" --excl-func=<文件名>:<函数名>(函数级候选排除——挂死/崩溃型毒候选隔离阀,可多次)") + print(" --excl-line=<文件名>:<1基行号>(行级毒候选定点排除,可多次)") + print(" --resume · --budget=N · --cap-ms=N · --ledger --out= · --ledger-only") + print("计划内宿主×套×静态断言数(合计 %d):" % Plan.total_static_checks()) + for h in Plan.hosts(): + for s in h["suites"].keys(): + print(" %s : %s = %d 条" % [h["id"], s, h["suites"][s]["count"]]) + + +# ============================ 源码扫描 ============================ + +# 宿主配置查找:--adhoc 注册的零配置宿主优先,其次计划条目 +func _plan_cfg(host_id: String) -> Dictionary: + if _adhoc_hosts.has(host_id): + return _adhoc_hosts[host_id] + return Plan.host_by_id(host_id) + + +func _host_state(host_id: String) -> Dictionary: + if _host_states.has(host_id): + return _host_states[host_id] + var cfg := _plan_cfg(host_id) + if cfg.is_empty(): + return {} + var src := FileAccess.get_file_as_string(String(cfg["src"])) + if src == "": + push_error("[mut] 宿主源码为空/缺失:%s(迁移中宿主需先 --snap 物化)" % String(cfg["src"])) + return {} + _orig_src[String(cfg["src"])] = src + var scan := _scan_source(src) + var st := {"cfg": cfg, "src": src, "scan": scan, "gds": null, "inst": null} + _host_states[host_id] = st + return st + + +func _scan_source(src: String) -> Dictionary: + var lines := src.split("\n") + var codes: Array[String] = [] + var in_tqs := false + for raw in lines: + var tq := raw.count("\"\"\"") + if in_tqs: + if tq % 2 == 1: + in_tqs = false + codes.append("") + continue + if tq % 2 == 1: + in_tqs = true + codes.append(_strip_comment(raw)) + var func_ranges := {} # 函数名 -> {start, end} + var func_of_line := {} + var cur := "" + for i in lines.size(): + var s := codes[i].strip_edges() + if s.begins_with("func ") or s.begins_with("static func "): + var head := s.substr(s.find("func ") + 5) + var nm := head.substr(0, head.find("(")).strip_edges() + if cur != "": + func_ranges[cur]["end"] = i + cur = nm + func_ranges[cur] = {"start": i, "end": lines.size() - 1} + func_of_line[i] = cur + var sites := [] + var idx := 0 + for i in lines.size(): + var is_def := codes[i].strip_edges().begins_with("func ") or codes[i].strip_edges().begins_with("static func ") + var col := 0 + while true: + var hit := _find_check_call(codes[i], col) + if hit["col"] < 0: + break + col = int(hit["col"]) + int(hit["len"]) + if is_def and hit["kind"] == "self": + continue + sites.append({"idx": idx, "line": i, "col": int(hit["col"]), "len": int(hit["len"]), + "kind": hit["kind"], "func": func_of_line[i]}) + idx += 1 + return {"lines": lines, "codes": codes, "func_ranges": func_ranges, + "func_of_line": func_of_line, "sites": sites} + + +func _find_check_call(code: String, from: int) -> Dictionary: + var found := {"col": -1, "len": 0, "kind": ""} + var probes := [["Kit.check_quiet(", "kit_q", 1], ["Kit.check(", "kit", 0], ["check(", "self", 1]] + for p in probes: + var token: String = p[0] + var c := code.find(token, from) + while c >= 0: + var prev := " " if c == 0 else code[c - 1] + var ok_site := true + if p[1] == "self": + if prev == "." or prev == "_": + ok_site = false + if c >= 5 and code.substr(c - 5, 5) == "func ": + ok_site = false + else: + if not (prev in [" ", "\t", "(", "[", "=", ",", "+"]): + ok_site = false + if ok_site: + if found["col"] < 0 or c < int(found["col"]): + found = {"col": c, "len": token.length(), "kind": p[1], "arg": p[2]} + break + c = code.find(token, c + 1) + return found + + +func _extract_label(scan: Dictionary, site: Dictionary) -> String: + var lines: Array = scan["lines"] + var start_line: int = site["line"] + var start_col: int = int(site["col"]) + int(site["len"]) + var depth := 0 + var args: Array[String] = [] + var cur_arg := "" + var in_str := false + for li in range(start_line, lines.size()): + var raw: String = lines[li] + var ci := 0 + if li == start_line: + ci = start_col + while ci < raw.length(): + var ch := raw[ci] + if in_str: + cur_arg += ch + if ch == "\\" and ci + 1 < raw.length(): + ci += 1 + cur_arg += raw[ci] + elif ch == "\"": + in_str = false + ci += 1 + continue + if ch == "\"": + in_str = true + cur_arg += ch + elif ch == "(" or ch == "[" or ch == "{": + depth += 1 + cur_arg += ch + elif ch == ")" or ch == "]" or ch == "}": + depth -= 1 + if depth < 0: + args.append(cur_arg) + return _pick_label(args, int(site.get("arg", 0))) + cur_arg += ch + elif ch == "," and depth == 0: + args.append(cur_arg) + cur_arg = "" + else: + cur_arg += ch + ci += 1 + cur_arg += " " + return _pick_label(args, int(site.get("arg", 0))) + + +func _pick_label(args: Array[String], want: int) -> String: + if want >= args.size(): + return "<取参失败>" + var a := args[want].strip_edges().replace("\t", " ") + while a.find(" ") >= 0: + a = a.replace(" ", " ") + return a + + +func _strip_comment(line: String) -> String: + var out := "" + var in_str := false + var i := 0 + while i < line.length(): + var c := line[i] + if in_str: + out += c + if c == "\\" and i + 1 < line.length(): + out += line[i + 1] + i += 2 + continue + if c == "\"": + in_str = false + i += 1 + else: + if c == "#": + break + if c == "\"": + in_str = true + out += c + i += 1 + if in_str: + return "" # 引号不闭合——保守跳过该行 + return out + + +# ============================ 宿主源码内存变换与编译 ============================ + +func _compile_variant(host_id: String, run_fn: String, strip_siblings: bool) -> Dictionary: + var st := _host_state(host_id) + if st.is_empty(): + return {} + var scan: Dictionary = st["scan"] + var lines: Array = scan["lines"] + var new_lines: Array = [] + for l in lines: + new_lines.append(String(l)) + # ① 断言点改写 _mut_check(idx, ...)(自后向前替换,列号不漂移) + var sites: Array = scan["sites"] + for si in range(sites.size() - 1, -1, -1): + var s: Dictionary = sites[si] + var token := "Kit.check_quiet(" if s["kind"] == "kit_q" else ("Kit.check(" if s["kind"] == "kit" else "check(") + var raw := String(new_lines[int(s["line"])]) + new_lines[int(s["line"])] = raw.substr(0, int(s["col"])) + "_mut_check(%d, " % int(s["idx"]) + raw.substr(int(s["col"]) + token.length()) + # ② 中和 _init/_initialize(保夹具,删套调用与 quit);_split_tail 形态另删兄弟套调用 + var suite_names: Array = st["cfg"]["suites"].keys() + for fname in ["_init", "_initialize"]: + if not scan["func_ranges"].has(fname): + continue + var r: Dictionary = scan["func_ranges"][fname] + for i in range(int(r["start"]) + 1, int(r["end"]) + 1): + var code := String(scan["codes"][i]).strip_edges() + if code == "": + continue + var m := _simple_call_name(code) + if m != "" and scan["func_ranges"].has(m) and m != fname: + new_lines[i] = "\tpass # [mut 变换] 套调用摘除" + elif code.begins_with("quit("): + new_lines[i] = "\tpass # [mut 变换] quit 摘除" + if strip_siblings and scan["func_ranges"].has(run_fn): + var r2: Dictionary = scan["func_ranges"][run_fn] + for i in range(int(r2["start"]) + 1, int(r2["end"]) + 1): + var code2 := String(scan["codes"][i]).strip_edges() + var m2 := _simple_call_name(code2) + if code2.begins_with("await ") and m2 != "" and m2 in suite_names and m2 != run_fn: + new_lines[i] = "\t\tpass # [mut 变换] 兄弟套调用摘除" + elif code2.begins_with("quit("): + new_lines[i] = "\tpass # [mut 变换] quit 摘除" + # ③ 活树/帧信号改道(root/get_root/process_frame → 引擎注入方法;按变换后行重取代码面前缀) + for i in new_lines.size(): + var raw2 := String(new_lines[i]) + var cpart := _strip_comment(raw2) + if cpart == "": + continue + var r1 := _replace_word(raw2, cpart, "get_root()", "_mut_root()") + var r2b := _replace_word(r1["line"], r1["scan"], "process_frame", "_mut_pf()") + var r3 := _replace_word(r2b["line"], r2b["scan"], "root", "_mut_root()") + new_lines[i] = r3["line"] + # ④ 注入预置(断言记点 / 活树改道) + var prelude := "\n\n# ---- [mut 变换] 引擎注入预置 ----\n" + prelude += "var _mut_engine = null\nvar _mut_sites = {}\n" + prelude += "func _mut_check(idx, a, b) -> void:\n" + prelude += "\tif not _mut_sites.has(idx):\n\t\t_mut_sites[idx] = []\n" + prelude += "\t_mut_sites[idx].append(bool(b) if (b is bool) else bool(a))\n" + prelude += "func _mut_root():\n\treturn _mut_engine.mut_root()\n" + prelude += "func _mut_pf():\n\treturn _mut_engine.process_frame\n" + var gds := GDScript.new() + gds.source_code = "\n".join(new_lines) + prelude + if gds.reload() != OK: + push_error("[mut] 宿主变换编译失败:%s run_fn=%s" % [host_id, run_fn]) + return {} + return {"gds": gds} + + +func _simple_call_name(code: String) -> String: + var c := code + if c.begins_with("await "): + c = c.substr(6).strip_edges() + if not c.ends_with("()"): + return "" + var nm := c.substr(0, c.length() - 2) + if nm == "" or nm.find("(") >= 0 or nm.find(" ") >= 0 or not nm.is_valid_identifier(): + return "" + return nm + + +func _replace_word(raw: String, scan: String, token: String, repl: String) -> Dictionary: + var out := raw + var s := scan + var pos := s.find(token) + while pos >= 0: + var before_ok := pos == 0 or not _is_word_char(s[pos - 1]) + var after := pos + token.length() + var after_ok := after >= s.length() or not _is_word_char(s[after]) + var dotted := pos > 0 and s[pos - 1] == "." + if before_ok and after_ok and not dotted and not _inside_string_span(s, pos): + out = out.substr(0, pos) + repl + out.substr(pos + token.length()) + s = s.substr(0, pos) + repl + s.substr(pos + token.length()) + pos = s.find(token, pos + repl.length()) + else: + pos = s.find(token, pos + token.length()) + return {"line": out, "scan": s} + + +func _is_word_char(c: String) -> bool: + if c == "_": + return true + return (c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or (c >= "0" and c <= "9") + + +# ============================ 套执行 ============================ + +func _run_job(host_id: String, suite_name: String) -> void: + var st := _host_state(host_id) + if st.is_empty(): + _write_result(host_id, suite_name, {"host": host_id, "suite": suite_name, "complete": true, + "rows": [], "anomalies": [{"scope": "suite", "reason": "宿主源码缺失"}], "meta": {}}) + return + var senv: Dictionary = st["cfg"]["suites"][suite_name] + var run_fn := String(senv.get("run_fn", suite_name)) + var variant := _compile_variant(host_id, run_fn, bool(senv.get("strip_siblings", false))) + if variant.is_empty(): + _write_result(host_id, suite_name, {"host": host_id, "suite": suite_name, "complete": true, + "rows": [], "anomalies": [{"scope": "suite", "reason": "宿主变换编译失败"}], "meta": {}}) + return + var gds: GDScript = variant["gds"] + var inst: Object = gds.new() + inst.set("_mut_engine", self) + st["gds"] = gds + st["inst"] = inst + var scan: Dictionary = st["scan"] + var targets: Array = [] + var label_of := {} + for s in scan["sites"]: + if s["func"] == suite_name: + var s2 := {"idx": s["idx"], "line": s["line"], "col": s["col"], "len": s["len"], + "kind": s["kind"], "arg": s.get("arg", 0), "func": s["func"]} + targets.append(s2) + label_of[int(s["idx"])] = _extract_label(scan, s2) + var anomalies: Array = [] + var expect := int(senv["count"]) + if targets.size() != expect: + anomalies.append({"scope": "suite", "reason": "静态断言点账实不符:计划 %d 实扫 %d(以实扫为准)" % [expect, targets.size()]}) + var budget := int(_args.get("budget", st["cfg"]["budget"])) + var cap_ms := int(_args.get("cap-ms", st["cfg"].get("cap_ms", Plan.SUITE_CAP_MS_DEFAULT))) + # ---- 基线(未执行 = 条件分支未走到,视为可杀形态;执行了必须全真) ---- + _mut_tree_templates = [] + _mut_recording = true + var base = await _run_suite_fn(st, run_fn) + _mut_recording = false + var base_ok := {} + for t in targets: + var idx := int(t["idx"]) + if base["sites"].has(idx): + var any_false := false + for v in base["sites"][idx]: + if not v: + any_false = true + break + if any_false: + anomalies.append({"scope": "assert", "idx": idx, "label": label_of[idx], + "reason": "基线即红——未变异已失败(死断言候选或环境污染)"}) + base_ok[idx] = false + else: + base_ok[idx] = true + else: + base_ok[idx] = true + var pending := 0 + for t in targets: + if base_ok[int(t["idx"])]: + pending += 1 + # ---- 候选改坏点(按武器优先序;摊销扫描) ---- + var weapon_cands := _build_candidates(st, senv, suite_name) + if String(_args.get("debug", "false")) == "true": + for w3 in ["table", "inject", "state"]: + print("[mut-debug] %s 候选数=%d" % [w3, weapon_cands[w3].size()]) + for di in range(0, mini(12, weapon_cands["inject"].size())): + print("[mut-debug] inject#%d %s" % [di, String(weapon_cands["inject"][di]["desc"])]) + var killers := {} + for t in targets: + killers[int(t["idx"])] = [] + var tried := 0 + var suite_t0 := Time.get_ticks_msec() + for w in senv["weapons"]: + for c in weapon_cands[String(w)]: + if pending <= 0 or tried >= budget or Time.get_ticks_msec() - suite_t0 > cap_ms: + break + var _trace := String(_args.get("trace", "false")) == "true" + if _trace: + print("[mut-trace] try #%d %s" % [tried + 1, String(c.get("desc", ""))]) + if not _apply_candidate(c): + _restore_candidate(c) # 施加失败也还原——失败施加可能已动了一半内存态 + tried += 1 + if _trace: + print("[mut-trace] #%d apply失败 %s" % [tried, String(c.get("desc", ""))]) + continue + var r = await _run_suite_fn(st, run_fn) + _restore_candidate(c) + tried += 1 + if _trace: + var nred := 0 + for t9 in targets: + var i9 := int(t9["idx"]) + if r["sites"].has(i9): + for v9 in r["sites"][i9]: + if not v9: + nred += 1 + break + print("[mut-trace] #%d %s 红数=%d" % [tried, String(c.get("desc", "")), nred]) + for t in targets: + var idx2 := int(t["idx"]) + if not base_ok[idx2] or not killers[idx2].is_empty(): + continue + if r["sites"].has(idx2): + var bad := false + for v in r["sites"][idx2]: + if not v: + bad = true + break + if bad: + killers[idx2].append(c) + pending -= 1 + if pending <= 0 or tried >= budget or Time.get_ticks_msec() - suite_t0 > cap_ms: + break + # ---- 逐条复验(改坏 → 跑该断言所在函数 → 验 FAIL → 撤销 → 记录) ---- + var rows: Array = [] + for t in targets: + var idx3 := int(t["idx"]) + if not base_ok[idx3]: + rows.append({"idx": idx3, "label": label_of[idx3], "status": "anomaly", + "reason": _anomaly_reason(anomalies, idx3), "weapon": "", "cand": "", + "cand_file": "", "cand_line": "", "cand_kind": "", "ms": 0, "collateral": -1}) + continue + var verified := false + for c in killers[idx3]: + if not _apply_candidate(c): + continue + var rv = await _run_suite_fn(st, run_fn) + _restore_candidate(c) + if rv["sites"].has(idx3): + var red := false + for v in rv["sites"][idx3]: + if not v: + red = true + break + if red: + var coll := 0 + for other_idx in rv["sites"].keys(): + if int(other_idx) == idx3: + continue + for v2 in rv["sites"][other_idx]: + if not v2: + coll += 1 + break + rows.append({"idx": idx3, "label": label_of[idx3], "status": "verified", + "weapon": String(c["weapon"]), "cand": String(c["desc"]), + "cand_file": String(c.get("file", "")), "cand_line": str(int(c.get("line", -1))), + "cand_kind": String(c.get("kind", "")), "ms": int(rv["us"] / 1000.0), + "collateral": coll, "killers_available": killers[idx3].size()}) + verified = true + break + if not verified: + var reason := "" + if killers[idx3].is_empty(): + reason = ("预算内未找到能翻红的改坏(试跑 %d 轮,武器=%s)——死断言候选或改坏面缺形" % [tried, ", ".join(PackedStringArray(senv["weapons"]))]) if tried > 0 else "零轮试跑(预算/上限异常)" + reason += _env_read_hint(scan, t) + if String(label_of[idx3]).find("幂等") >= 0: + reason += ";标签含幂等语义——确定性快照的内容等值在单行确定性变异下难破,疑似结构性打不红,须人工研判改坏面" + else: + reason = "复验未红(改坏候选 %d 个均不复现——疑似非确定断言)" % killers[idx3].size() + anomalies.append({"scope": "assert", "idx": idx3, "label": label_of[idx3], "reason": reason}) + rows.append({"idx": idx3, "label": label_of[idx3], "status": "anomaly", "reason": reason, + "weapon": "", "cand": "", "cand_file": "", "cand_line": "", "cand_kind": "", + "ms": 0, "collateral": -1}) + # ---- 收尾:跑后基线复绿(零残留证明) ---- + var rend = await _run_suite_fn(st, run_fn) + var end_green := true + for t in targets: + var idx4 := int(t["idx"]) + if rend["sites"].has(idx4): + for v in rend["sites"][idx4]: + if not v: + end_green = false + break + if not end_green: + anomalies.append({"scope": "suite", "reason": "跑后基线未复绿——存在跨试跑状态残留,须人工排查"}) + var verify_ms_total := 0 + for r in rows: + verify_ms_total += int(r.get("ms", 0)) + _write_result(host_id, suite_name, {"host": host_id, "suite": suite_name, "complete": true, + "run_fn": run_fn, "budget": budget, "tried": tried, "targets": targets.size(), + "static_plan": expect, "rows": rows, "anomalies": anomalies, + "end_baseline_green": end_green, + "meta": {"verify_ms_total": verify_ms_total}}) + inst.free() + st["inst"] = null + st["gds"] = null + + +func _anomaly_reason(anomalies: Array, idx: int) -> String: + for a in anomalies: + if int(a.get("idx", -1)) == idx: + return String(a["reason"]) + return "基线异常" + + +# 断言表达式环境面识别(断言行连读三行——多行 cond 的续行):命中 FileAccess/DirAccess +# (磁盘读取)或「大写成员常量读取」(schema 词表 const 经宿主 preload 编译期绑定原件—— +# 路径接管不可达且 const 深冻结)的打不红条目注明归因——台账异常清单用(禁触盘/禁 reload +# 原件是引擎红线,此类条目只能如实进异常清单,不为验红改文件) +func _env_read_hint(scan: Dictionary, t: Dictionary) -> String: + var lines: Array = scan["lines"] + var re_const := RegEx.create_from_string("\\.[A-Z][A-Z0-9_]{2,}\\b") + for li in range(int(t["line"]), mini(int(t["line"]) + 3, lines.size())): + var s := _strip_comment(String(lines[li])) + if s.find("FileAccess") >= 0 or s.find("DirAccess") >= 0: + return ";断言表达式含磁盘读取(FileAccess/DirAccess)——内存内改坏不可达,归环境面死断言候选" + if re_const != null and re_const.search(s) != null: + return ";断言表达式读 schema 常量(大写成员,宿主 preload 编译期绑定原件)——路径接管不可达且 const 深冻结,归结构性打不红候选" + return "" + + +func _run_suite_fn(st: Dictionary, run_fn: String) -> Dictionary: + var inst: Object = st["inst"] + inst.set("_mut_sites", {}) + _mut_tops = [] + var t0 := Time.get_ticks_usec() + if bool(st["cfg"]["async"]): + await inst.call(run_fn) + else: + inst.call(run_fn) + var us := Time.get_ticks_usec() - t0 + var out := {} + var sites: Dictionary = inst.get("_mut_sites") + for k in sites.keys(): + out[int(k)] = sites[k].duplicate() + return {"sites": out, "us": us} + + +# ============================ 候选改坏点生成 ============================ + +func _build_candidates(st: Dictionary, senv: Dictionary, suite_name: String) -> Dictionary: + var by_weapon := {"table": [], "inject": [], "state": []} + var tokens := _suite_tokens(st, String(senv.get("run_fn", suite_name))) + for w in senv["weapons"]: + match String(w): + "table": + by_weapon["table"] = _table_candidates(st, senv.get("tables", []), + _suite_literals(st, String(senv.get("run_fn", suite_name)))) + "inject": + for mp in senv.get("modules", []): + for c in _module_candidates(String(mp)): + c["weapon"] = "inject" + by_weapon["inject"].append(c) + # 相关性置前:套函数体内出现过的标识符(被测入口/协作者名)命中的候选优先试跑—— + # 大模块按行序扫会把深处守卫排在预算外,套源码词面+计划 funcs 提示是仅有的自动定位信号 + var scored: Array = [] + var hinted_all: Dictionary = senv.get("funcs", {}) + for ci in by_weapon["inject"].size(): + var c2: Dictionary = by_weapon["inject"][ci] + var sc := 0 + var mp_file := String(String(c2.get("file", "")).get_file()) + var hinted_names: Array = hinted_all.get(mp_file, []) + if hinted_names.has("*") or hinted_names.has(String(c2.get("func", ""))): + sc += 4 + if tokens.has(String(c2.get("func", ""))): + sc += 2 + for t in _word_tokens(String(c2.get("line_text", ""))): + if t.length() >= 4 and tokens.has(t): + sc += 1 + break + scored.append(sc) + var order := range(by_weapon["inject"].size()) + order.sort_custom(func(a: int, b: int) -> bool: + if scored[a] != scored[b]: + return int(scored[a]) > int(scored[b]) + return a < b) + var reordered: Array = [] + for oi in order: + reordered.append(by_weapon["inject"][oi]) + by_weapon["inject"] = reordered + "state": + by_weapon["state"] = _state_candidates(st, senv) + for tc in _tree_state_candidates(): + by_weapon["state"].append(tc) + # 跨武器去重(同文件同行同形态只留先到者;注入/初值候选无 set_key——以行号+形态区分; + # 按套的武器优先序迭代——先到武器保留该行候选的武器归属) + var seen := {} + var worder: Array = [] + for w4 in senv["weapons"]: + worder.append(String(w4)) + for w5 in ["table", "inject", "state"]: + if not worder.has(w5): + worder.append(w5) + for w2 in worder: + var kept: Array = [] + for c in by_weapon[w2]: + var key := "%s|%s|%s|%s" % [c.get("file", ""), c.get("kind", ""), c.get("set_key", ""), str(c.get("line", -1))] + if seen.has(key): + continue + seen[key] = true + kept.append(c) + by_weapon[w2] = kept + return by_weapon + + +# 词法切分(标识符提取用——4.7 RegEx 无 split,手扫字符) +func _word_tokens(code: String) -> PackedStringArray: + var out := PackedStringArray() + var cur := "" + for i in code.length(): + var ch := code[i] + if _is_word_char(ch): + cur += ch + elif cur != "": + out.append(cur) + cur = "" + if cur != "": + out.append(cur) + return out + + +# 套函数体标识符集合(注释剔除后取词;run_fn 本体及其调用的套内私有 helper 一并纳入—— +# 供注入候选按「被套源码词面命中」排序) +func _suite_tokens(st: Dictionary, run_fn: String) -> Dictionary: + var scan: Dictionary = st["scan"] + var tokens := {} + var want: Array = [run_fn] + var seen_fns := {} + while not want.is_empty(): + var fn := String(want.pop_back()) + if fn == "" or seen_fns.has(fn) or not scan["func_ranges"].has(fn): + continue + seen_fns[fn] = true + var r: Dictionary = scan["func_ranges"][fn] + for i in range(int(r["start"]), int(r["end"]) + 1): + var code := String(scan["codes"][i]) + if code.strip_edges() == "": + continue + for t in _word_tokens(code): + if t.length() >= 4: + tokens[t] = true + var m := _simple_call_name(code.strip_edges()) + if m != "" and scan["func_ranges"].has(m): + want.append(m) + return tokens + + +func _table_candidates(st: Dictionary, table_names: Array, lits: Array) -> Array: + var cands: Array = [] + var inst: Object = st["inst"] + if inst == null or inst.get("_db") == null: + return cands + var db = inst.get("_db") + for tname in table_names: + var t = db.get(String(tname)) + if t == null or not (t is Resource): + continue + var before := cands.size() + _walk_table(t, String(tname), cands, 0, lits) + if String(_args.get("debug", "false")) == "true": + print("[mut-debug] 表 %s 候选 %d(累计 %d)" % [String(tname), cands.size() - before, cands.size()]) + return cands + + +# 套函数体字符串字面量(注释剔除后引号内文本;套内私有 helper 一并纳入)。 +# 供表武器「字面量键位插行/加键」候选——断言点名的业务键(如 defense_up)就是套源码 +# 字面量,账本 §九 G2 旧法「补立已退役行/加键」形态的自动取材通道。 +func _suite_literals(st: Dictionary, run_fn: String) -> Array: + var scan: Dictionary = st["scan"] + var out: Array = [] + var seen := {} + var want: Array = [run_fn] + var seen_fns := {} + while not want.is_empty(): + var fn := String(want.pop_back()) + if fn == "" or seen_fns.has(fn) or not scan["func_ranges"].has(fn): + continue + seen_fns[fn] = true + var r: Dictionary = scan["func_ranges"][fn] + for i in range(int(r["start"]), int(r["end"]) + 1): + var code := String(scan["codes"][i]) + if code.strip_edges() == "": + continue + var ci := 0 + while ci < code.length(): + if code[ci] == "\"": + var cj := ci + 1 + var buf := "" + while cj < code.length() and code[cj] != "\"": + if code[cj] == "\\" and cj + 1 < code.length(): + cj += 2 + continue + buf += code[cj] + cj += 1 + if cj >= code.length(): + break + var lit := buf + if lit.length() >= 3 and lit.length() <= 40 and not seen.has(lit) \ + and _is_ident_like(lit): + seen[lit] = true + out.append(lit) + ci = cj + 1 + continue + ci += 1 + var m := _simple_call_name(code.strip_edges()) + if m != "" and scan["func_ranges"].has(m): + want.append(m) + if out.size() >= 24: + break + return out + + +func _is_ident_like(s: String) -> bool: + if s == "" or (s[0] >= "0" and s[0] <= "9"): + return false + for i in s.length(): + if not _is_word_char(s[i]): + return false + return true + + +func _walk_table(res: Resource, label: String, cands: Array, depth: int, lits: Array) -> void: + var n := 0 + for p in res.get_property_list(): + if n >= 96 or cands.size() >= 2600: + return + var pname := String(p["name"]) + if pname.begins_with("resource_") or pname in ["script", "metadata", "resource_name", "reference"]: + continue + var v = res.get(pname) + match typeof(v): + TYPE_OBJECT: + if v is Resource and depth < 2: + _walk_table(v, label + "." + pname, cands, depth + 1, lits) + TYPE_INT: + for nv in [v + 1, v - 1, 0, v * 10]: + if nv != v and nv >= 0: + cands.append(_val_cand(res, label + "." + pname, pname, nv)) + n += 1 + TYPE_FLOAT: + for fv in [v + 1.0, v * 10.0]: + cands.append(_val_cand(res, label + "." + pname, pname, fv)) + n += 1 + TYPE_BOOL: + cands.append(_val_cand(res, label + "." + pname, pname, not v)) + n += 1 + TYPE_STRING: + if v != "": + cands.append(_val_cand(res, label + "." + pname, pname, v + "_x")) + cands.append(_val_cand(res, label + "." + pname, pname, "")) + n += 2 + TYPE_DICTIONARY: + # 字典值属性(如 settlement.effect_pool_map):现键值改写+套字面量加键—— + # has/键查通道的击穿面(「无 X 键」类断言须造出同名键才翻红) + if depth < 2: + _dict_prop_candidates(v, label + "." + pname, cands, lits, depth) + TYPE_ARRAY: + if depth < 2: + _array_prop_candidates(v, label + "." + pname, cands, lits, depth) + n += 1 + + +# 数组值属性(行集为主):结构性改坏置前(账本 §九 G2 旧法主力形态:复插/删行/加键/删键) +# +套字面量键位插行(已退役行/清单规模类断言)+空表插行+逐键值改写(逐键上限,防 +# 前排字符串键吃光配额把纵深数值键排到预算外) +func _array_prop_candidates(arr: Array, label: String, cands: Array, lits: Array, depth: int) -> void: + var row_cap := mini(arr.size(), 16) + if String(_args.get("debug", "false")) == "true": + print("[mut-debug] 数组 %s 行数=%d row_cap=%d lits=%d" % [label, arr.size(), row_cap, lits.size()]) + for ei in row_cap: + var el0 = arr[ei] + if el0 is Dictionary: + cands.append(_dict_addkey_cand(el0, "%s[%d]" % [label, ei])) + var kk := 0 + for key0 in el0.keys(): + if kk >= 2: + break + cands.append(_dict_erase_cand(el0, "%s[%d].%s" % [label, ei, str(key0)], key0)) + kk += 1 + if cands.size() >= 2600: + return + if arr.size() > 0: + cands.append(_row_dup_cand(arr, label)) + cands.append(_row_dup_cand(arr, label, mini(2, arr.size() - 1))) + cands.append(_row_del_cand(arr, label, 0)) + if arr.size() > 2: + cands.append(_row_del_cand(arr, label, arr.size() - 1)) + # 字面量键位插行:键名取行首键,值=套源码字面量、其余字段抄行0(append 表尾—— + # 规模/成员/退役行查通道全翻红;还原按持引用 erase,跑后基线复绿闸兜底残留) + var el_first = arr[0] + var idk = null + if el_first is Dictionary and el_first.size() > 0: + idk = el_first.keys()[0] + var idv = el_first.get(idk) if idk != null else null + var lit_n := 0 + for lit in lits: + if lit_n >= 8 or cands.size() >= 2600: + break + # 键值非字符串不插(int 键位插字符串值必畸形行);键值==字面量无新信息不插 + if idk == null or typeof(idv) != TYPE_STRING or str(idv) == lit: + continue + cands.append(_row_lit_insert_cand(arr, label, idk, lit)) + lit_n += 1 + # 新键值插行(每行集一挺——「十型封闭/成员集合」类断言要一个词表外的键值才翻红; + # 键值型非字符串不生成——typed 行插异型必 apply 失败白轮) + if idk != null and typeof(idv) == TYPE_STRING: + cands.append(_row_lit_insert_cand(arr, label, idk, "mut_seed")) + elif arr.is_empty(): + cands.append(_row_synth_insert_cand(arr, label)) + for ei in row_cap: + var el = arr[ei] + if el is Dictionary: + # 每键候选各自成列再轮转并取——宽行(如 enemy 行 15 键、armor_class 殿后) + # 不被前排键的累计配额挤掉:先让每个键拿到第一形态,再谈第二三形态 + var per_key: Array = [] + for key in el.keys(): + var ev = el[key] + var tag := "%s[%d].%s" % [label, ei, str(key)] + var l: Array = [] + _dict_value_candidates(el, tag, key, ev, l, depth, lits) + per_key.append(l) + var added := 0 + var round_i := 0 + while round_i < 4 and added < 48 and cands.size() < 2600: + for l2 in per_key: + if round_i < l2.size(): + cands.append(l2[round_i]) + added += 1 + if added >= 48 or cands.size() >= 2600: + break + round_i += 1 + elif el is Resource: + _walk_table(el, "%s[%d]" % [label, ei], cands, depth + 1, lits) + else: + match typeof(el): + TYPE_INT: + for nv3 in [el + 1, el - 1, 0]: + if nv3 != el and nv3 >= 0: + cands.append(_arr_elem_cand(arr, label, ei, nv3)) + TYPE_FLOAT: + cands.append(_arr_elem_cand(arr, label, ei, el + 1.0)) + TYPE_BOOL: + cands.append(_arr_elem_cand(arr, label, ei, not el)) + TYPE_STRING: + if el != "": + cands.append(_arr_elem_cand(arr, label, ei, el + "_x")) + + +# 字典值属性候选:现键值改写(逐键上限)+套字面量加键 +func _dict_prop_candidates(dd: Dictionary, label: String, cands: Array, lits: Array, depth: int) -> void: + var kc := 0 + for key in dd.keys(): + if kc >= 6 or cands.size() >= 2600: + break + var ev = dd[key] + kc += _dict_value_candidates(dd, "%s.%s" % [label, str(key)], key, ev, cands, depth, lits) + var v0 = null + for k0 in dd.keys(): + v0 = dd[k0] + break + if v0 == null: + v0 = true + for lit in lits: + if cands.size() >= 2600: + break + if dd.has(lit): + continue + cands.append(_dict_lit_addkey_cand(dd, label, lit, v0)) + + +# 单键值改写候选(每键上限 2——纵深键不再被前排键的配额挤掉;返回追加数) +func _dict_value_candidates(dict: Dictionary, tag: String, key, ev, cands: Array, depth: int, lits: Array) -> int: + var kc := 0 + match typeof(ev): + TYPE_INT: + for nv2 in [ev - 1, ev + 1, 0]: + if kc >= 3: + break + if nv2 != ev and nv2 >= 0: + cands.append(_dict_cand(dict, tag, key, nv2)) + kc += 1 + TYPE_FLOAT: + for fv2 in [ev + 1.0, ev * 10.0]: + if kc >= 2: + break + cands.append(_dict_cand(dict, tag, key, fv2)) + kc += 1 + TYPE_STRING: + if ev != "": + cands.append(_dict_cand(dict, tag, key, ev + "_x")) + kc += 1 + TYPE_BOOL: + cands.append(_dict_cand(dict, tag, key, not ev)) + kc += 1 + TYPE_OBJECT: + if ev is Resource and depth < 1: + _walk_table(ev, tag, cands, depth + 1, lits) + return kc + + +var _rm_slot = null # 删行候选的暂存行(施加→还原窗口内持有) + + +func _dict_addkey_cand(dict: Dictionary, tag: String) -> Dictionary: + var apply_f := func() -> bool: + if dict.has("mut_seed"): + return false + dict["mut_seed"] = true + return true + var restore_f := func() -> void: + dict.erase("mut_seed") + return {"weapon": "table", "file": tag, "line": -1, "kind": "table 加键", + "set_key": tag + ".mut_seed", "desc": "表行加键 %s: mut_seed=true" % tag, + "apply": apply_f, "restore": restore_f} + + +func _dict_erase_cand(dict: Dictionary, tag: String, key) -> Dictionary: + var has_old: bool = dict.has(key) + var old = dict.get(key) + var apply_f := func() -> bool: + if not dict.has(key): + return false + dict.erase(key) + return true + var restore_f := func() -> void: + if has_old: + dict[key] = old + return {"weapon": "table", "file": tag, "line": -1, "kind": "table 删键", + "set_key": tag + ".erase", "desc": "表行删键 %s" % tag, + "apply": apply_f, "restore": restore_f} + + +func _row_dup_cand(arr: Array, label: String, ei: int = 0) -> Dictionary: + var apply_f := func() -> bool: + if ei >= arr.size(): + return false + var ins = arr[ei].duplicate(true) + arr.insert(0, ins) + _rm_slot = ins + return true + var restore_f := func() -> void: + # erase 按内容等价删首个命中——副本与原行同内容,删哪个对断言面等价; + # apply 失败时 _rm_slot 恒 null,restore 空操作(不误删真行) + if _rm_slot != null: + arr.erase(_rm_slot) + _rm_slot = null + return {"weapon": "table", "file": label, "line": -1, "kind": "table 复插行", + "set_key": label + ".dup" + str(ei), "desc": "表复插行 %s[%d] → 表首" % [label, ei], + "apply": apply_f, "restore": restore_f} + + +func _row_del_cand(arr: Array, label: String, ei: int) -> Dictionary: + var apply_f := func() -> bool: + if ei >= arr.size(): + return false + _rm_slot = arr[ei] + arr.remove_at(ei) + return true + var restore_f := func() -> void: + if _rm_slot != null: + arr.insert(mini(ei, arr.size()), _rm_slot) + _rm_slot = null + return {"weapon": "table", "file": label, "line": -1, "kind": "table 删行", + "set_key": label + ".del" + str(ei), "desc": "表删行 %s[%d]" % [label, ei], + "apply": apply_f, "restore": restore_f} + + +# 字面量键位插行(行首键=套源码字面量、其余字段抄行0)——「已退役行不存在/清单规模/ +# 成员封闭」类断言的击穿面 +func _row_lit_insert_cand(arr: Array, label: String, idk, lit: String) -> Dictionary: + var proto: Dictionary = arr[0] + var row: Dictionary = proto.duplicate(true) + row[idk] = lit + var apply_f := func() -> bool: + if arr.has(row): + return false + arr.append(row) + return true + var restore_f := func() -> void: + arr.erase(row) + return {"weapon": "table", "file": "%s.%s=%s" % [label, str(idk), lit], "line": -1, + "kind": "table 字面量插行", "set_key": "%s.%s=%s" % [label, str(idk), lit], + "desc": "表插行 %s:%s=%s(其余字段抄行0)" % [label, str(idk), lit], + "apply": apply_f, "restore": restore_f} + + +# 空表插行(空行集的「空表加载/零行为」类断言击穿面;值型按数组元素型适配, +# 不认识的元素型不生成——apply 必失败的白轮) +func _row_synth_insert_cand(arr: Array, label: String) -> Dictionary: + var row = {"mut_seed": true} + var typed_ok := true + if arr.is_typed(): + match arr.get_typed_builtin(): + TYPE_STRING: + row = "mut_seed" + TYPE_INT, TYPE_FLOAT: + row = 1 + TYPE_BOOL: + row = true + TYPE_DICTIONARY, TYPE_NIL, TYPE_OBJECT: + pass + _: + typed_ok = false + var apply_f := func() -> bool: + if not typed_ok: + return false + arr.append(row) + return true + var restore_f := func() -> void: + arr.erase(row) + return {"weapon": "table", "file": label, "line": -1, "kind": "table 空表插行", + "set_key": label + ".synth", "desc": "空表插行 %s(合成行灭 is_empty)" % label, + "apply": apply_f, "restore": restore_f} + + +# 字典字面量加键(断言点名的键名——「无 X 键/has 通道」类断言的击穿面;值抄现键值型) +func _dict_lit_addkey_cand(dd: Dictionary, label: String, lit: String, v0) -> Dictionary: + var apply_f := func() -> bool: + if dd.has(lit): + return false + dd[lit] = v0 + return true + var restore_f := func() -> void: + dd.erase(lit) + return {"weapon": "table", "file": "%s.%s" % [label, lit], "line": -1, + "kind": "table 字典加键", "set_key": "%s.%s" % [label, lit], + "desc": "字典加键 %s:%s(值型抄现键)" % [label, lit], + "apply": apply_f, "restore": restore_f} + + +func _arr_elem_cand(arr: Array, label: String, ei: int, nv) -> Dictionary: + var old = arr[ei] + var apply_f := func() -> bool: + arr[ei] = nv + return true + var restore_f := func() -> void: + arr[ei] = old + return {"weapon": "table", "file": "%s[%d]" % [label, ei], "line": -1, "kind": "table_elem", + "set_key": "%s[%d]=%s" % [label, ei, _fmt_val(nv)], "desc": "表元素改写 %s[%d]: %s → %s" % [label, ei, _fmt_val(old), _fmt_val(nv)], + "apply": apply_f, "restore": restore_f} + + +func _val_cand(res: Resource, path_label: String, pname: String, nv) -> Dictionary: + var old = res.get(pname) + var apply_f := func() -> bool: + res.set(pname, nv) + return true + var restore_f := func() -> void: + res.set(pname, old) + return {"weapon": "table", "file": path_label, "line": -1, "kind": "table_set", + "set_key": path_label + "=" + _fmt_val(nv), "desc": "表改写 %s: %s → %s" % [path_label, _fmt_val(old), _fmt_val(nv)], + "apply": apply_f, "restore": restore_f} + + +func _dict_cand(dict: Dictionary, path_label: String, key, nv) -> Dictionary: + var old = dict.get(key) + var apply_f := func() -> bool: + dict[key] = nv + return true + var restore_f := func() -> void: + dict[key] = old + return {"weapon": "table", "file": path_label, "line": -1, "kind": "table_set", + "set_key": path_label + "=" + _fmt_val(nv), "desc": "表行改写 %s: %s → %s" % [path_label, _fmt_val(old), _fmt_val(nv)], + "apply": apply_f, "restore": restore_f} + + +func _fmt_val(v) -> String: + var s := str(v) + return s.substr(0, 40) + + +# 模块源码行级候选(注释/字符串感知;loop 行跳过防挂死;含 func 归属供套级提示排序) +func _module_candidates(path: String) -> Array: + if _mod_cand_cache.has(path): + return _mod_cand_cache[path] + var src := FileAccess.get_file_as_string(path) + if src == "": + var s = load(path) + src = String(s.source_code) if s != null else "" + if src == "": + push_error("[mut] 模块源码缺失:%s" % path) + _mod_cand_cache[path] = [] + return [] + _orig_src[path] = src + if not _orig_scripts.has(path): + var so = load(path) + if so != null: + _orig_scripts[path] = so # 原件对象捕获(还原通道+终态核验基准) + var lines := src.split("\n") + var codes: Array[String] = [] + var in_tqs := false + for raw in lines: + var tq := raw.count("\"\"\"") + if in_tqs: + if tq % 2 == 1: + in_tqs = false + codes.append("") + continue + if tq % 2 == 1: + in_tqs = true + codes.append(_strip_comment(raw)) + var cur_func := "" + var all: Array = [] + for i in lines.size(): + var s := codes[i].strip_edges() + if s.begins_with("func ") or s.begins_with("static func "): + var head := s.substr(s.find("func ") + 5) + cur_func = head.substr(0, head.find("(")).strip_edges() + if s == "": + continue + if _func_excluded(path, cur_func): + continue + var scope := "body" if cur_func != "" else "init" + all.append_array(_line_candidates(codes[i], i, String(lines[i]), path, scope, cur_func)) + _mod_cand_cache[path] = all + return all + + +# 函数级候选排除(--excl-func=<文件名>:<函数名>,可多次):挂死/崩溃型变异体的隔离阀—— +# 有毒候选(如把带重试环的 generate 改成无限递归→栈溢出杀进程)按函数整段剔除, +# 同模块其余函数的候选照常生成 +func _func_excluded(path: String, fname: String) -> bool: + for spec in _args.get("excl-funcs", []): + var parts := String(spec).split(":", true, 1) + if parts.size() == 2 and String(path).get_file() == parts[0] and fname == parts[1]: + return true + return false + + +# 行级候选排除(--excl-line=<文件名>:<1 基行号>,可多次):单行毒候选(如 BFS 邻接条件 +# 取反→队列无界增长→OOM 杀进程)的精确定点阀,同函数其余行照常 +func _line_excluded(path: String, i: int) -> bool: + for spec in _args.get("excl-lines", []): + var parts := String(spec).split(":", true, 1) + if parts.size() == 2 and String(path).get_file() == parts[0] and int(parts[1]) == i + 1: + return true + return false + + +func _line_candidates(code: String, i: int, orig_line: String, path: String, scope: String, cur_func: String) -> Array: + var cands: Array = [] + var stripped := code.strip_edges() + if stripped == "" or stripped.begins_with("class_name") or stripped.begins_with("extends"): + return cands + var loop_risk := stripped.begins_with("while") or stripped.begins_with("for ") \ + or code.find(" range(") >= 0 or code.find("\tfor ") >= 0 + var fname := String(path).get_file() + var muts: Array = [] + if _line_excluded(path, i): + return cands + if not loop_risk: + muts.append_array(_op_muts(code)) + muts.append_array(_token_muts(code, 2)) + muts.append_array(_empty_coll_muts(code)) + muts.append_array(_num_muts(code, 5)) + if code.find(" :=") < 0: + muts.append_array(_assign_null_muts(code)) + muts.append_array(_negret_muts(code)) + muts.append_array(_return_swap_muts(code)) + muts.append_array(_if_negate_muts(code)) + muts.append_array(_string_muts(code, 2)) + muts.append_array(_const_coll_append_muts(code)) + for m in muts: + var new_line := String(m["line"]) + if new_line == orig_line: + continue + var apply_f := func() -> bool: + # 变体编译+路径接管(原件零 reload——GDScript.reload 在原件有循环引用实例时 + # 恒返 ERR_ALREADY_IN_USE,进程内原地重编译对战斗核心件不可行) + var src := String(_orig_src.get(path, "")) + if src == "": + return false + var ls := src.split("\n") + if i >= ls.size() or ls[i] != orig_line: + return false + ls[i] = new_line + return _take_over_variant(path, "\n".join(ls)) + var restore_f := func() -> void: + var orig_s: GDScript = _orig_scripts.get(path) + if orig_s != null: + orig_s.take_over_path(path) # 原件对象夺回路径——缓存映射回到开工态(零重编译) + cands.append({"weapon": "inject", "file": path, "line": i, "kind": String(m["kind"]), + "scope": scope, "func": cur_func, "line_text": code, + "desc": "%s:%d %s [%s]" % [fname, i + 1, String(m["kind"]), _snippet_pair(orig_line, new_line)], + "apply": apply_f, "restore": restore_f}) + return cands + + +func _snippet_pair(a: String, b: String) -> String: + var sa := a.strip_edges() + var sb := b.strip_edges() + if sa.length() > 46: + sa = sa.substr(0, 46) + "…" + if sb.length() > 46: + sb = sb.substr(0, 46) + "…" + return sa + " → " + sb + + +func _op_muts(code: String) -> Array: + var flips := [["==", "!="], ["!=", "=="], [">=", "<"], ["<=", ">"], [">", "<="], ["<", ">="]] + var done: Array = [] + var out: Array = [] + for f in flips: + var from: String = f[0] + var to: String = f[1] + var pos := code.find(from) + while pos >= 0: + var ok := true + var ch_before := " " if pos == 0 else code[pos - 1] + var after_i := pos + from.length() + var ch_after := " " if after_i >= code.length() else code[after_i] + if ch_before in ["=", "!", "<", ">", "+", "-", "*", "/", "%", "&", "|", ":"]: + ok = false + if ch_after in ["=", "<", ">"]: + ok = false + if ok and _inside_string_span(code, pos): + ok = false + if ok: + var nl := code.substr(0, pos) + to + code.substr(pos + from.length()) + if not done.has(nl): + done.append(nl) + out.append({"kind": "op %s→%s" % [from, to], "line": nl}) + pos = code.find(from, pos + 1) + return out + + +func _inside_string_span(code: String, pos: int) -> bool: + var in_str := false + var i := 0 + while i < pos: + if code[i] == "\\": + i += 2 + continue + if code[i] == "\"": + in_str = not in_str + i += 1 + return in_str + + +func _token_muts(code: String, cap: int) -> Array: + var out: Array = [] + for t in [["true", "false"], ["false", "true"]]: + var pos := code.find(t[0]) + while pos >= 0 and out.size() < cap: + var before_ok := pos == 0 or not _is_word_char(code[pos - 1]) + var after: int = pos + String(t[0]).length() + var after_ok := after >= code.length() or not _is_word_char(code[after]) + if before_ok and after_ok and not _inside_string_span(code, pos): + out.append({"kind": "bool %s→%s" % [t[0], t[1]], "line": code.substr(0, pos) + t[1] + code.substr(after)}) + pos = code.find(t[0], pos + 1) + return out + + +func _num_muts(code: String, cap: int) -> Array: + var out: Array = [] + var i := 0 + while i < code.length() and out.size() < cap: + var c := code[i] + if c >= "0" and c <= "9": + var before_ok := i == 0 or not (_is_word_char(code[i - 1]) or code[i - 1] == ".") + var hexlike := c == "0" and i + 1 < code.length() and code[i + 1] in ["x", "b", "X", "B"] + if before_ok and not hexlike: + var j := i + var dot := false + while j < code.length() and ((code[j] >= "0" and code[j] <= "9") or (code[j] == "." and not dot)): + if code[j] == ".": + dot = true + j += 1 + var lit := code.substr(i, j - i) + var head := code.substr(0, i) + var tail := code.substr(j) + if not dot: + var iv := lit.to_int() + for nv in [iv + 1, iv - 1]: + if nv >= 0: + out.append({"kind": "num %d→%d" % [iv, nv], "line": head + str(nv) + tail}) + # 折半——±1 够不着的钳制带/阈值断言(旧法 46→25 类大跳)的最小通用替代形态 + var hv := iv / 2 + if hv != iv and hv >= 0: + out.append({"kind": "num %d→%d(折半)" % [iv, hv], "line": head + str(hv) + tail}) + else: + var fv := lit.to_float() + out.append({"kind": "num %s→%s" % [lit, _fmt_val(fv + 1.0)], "line": head + _fmt_val(fv + 1.0) + tail}) + var hv2 := fv * 0.5 + out.append({"kind": "num %s→%s(折半)" % [lit, _fmt_val(hv2)], "line": head + _fmt_val(hv2) + tail}) + i = j + continue + i += 1 + return out + + +func _empty_coll_muts(code: String) -> Array: + var out: Array = [] + for pair in [["= {}", "{\"mut_seed\": true}", "coll {}→注脏"], ["= []", "[\"mut_seed\"]", "coll []→注脏"]]: + var pos := code.find(pair[0]) + if pos >= 0 and not _inside_string_span(code, pos): + if pos > 0 and code[pos - 1] == ":": + continue # :={} 推断赋值——跳过避免生成非法行 + # 带类型标注的声明(var x: T = …)换异形字面量必编译失败——只吃无标注的普通赋值 + if code.substr(0, pos).contains(":"): + continue + var nl: String = code.substr(0, pos) + String(pair[1]) + code.substr(pos + String(pair[0]).length()) + out.append({"kind": pair[2], "line": nl}) + return out + + +func _assign_null_muts(code: String) -> Array: + var out: Array = [] + var pos := code.find(" = ") + while pos >= 0 and out.size() < 1: + var ch_before := "" if pos == 0 else code[pos - 1] + if not (ch_before in ["=", "!", "<", ">", "+", "-", "*", "/", "%", ":"]): + # 类型标注(var x: T = …)与 const 置 null 必编译失败——只吃无标注赋值 + if not code.substr(0, pos).contains(":") and not code.strip_edges().begins_with("const"): + var rhs := code.substr(pos + 3).strip_edges() + if rhs != "" and not rhs.begins_with("null"): + out.append({"kind": "asgn 置null", "line": code.substr(0, pos) + " = null"}) + pos = code.find(" = ", pos + 1) + return out + + +func _negret_muts(code: String) -> Array: + var out: Array = [] + var stripped := code.strip_edges() + if stripped.begins_with("return ") and not stripped.begins_with("return not"): + var expr := stripped.substr(7).strip_edges() + if expr != "": + var cut := code.find("return") + var indent := code.substr(0, cut) + out.append({"kind": "return 取反", "line": indent + "return not (" + expr + ")"}) + var key := ": return " + var pos := code.find(key) + if pos >= 0: + var expr2 := code.substr(pos + key.length()).strip_edges() + if expr2 != "" and not expr2.begins_with("not"): + out.append({"kind": "lambda 取反", "line": code.substr(0, pos) + key + "not (" + expr2 + ")"}) + return out + + +# return 置换(守卫击穿的最小通用形态):非平凡 return 表达式换成常量假/空—— +# 与返回类型不合的形态 reload 即拒、apply 内自动回写原件(廉价废轮,不跑套) +func _return_swap_muts(code: String) -> Array: + var out: Array = [] + var stripped := code.strip_edges() + if not stripped.begins_with("return "): + return out + var expr := stripped.substr(7).strip_edges() + if expr == "" or expr.begins_with("not") or expr in ["false", "0"]: + return out + var cut := code.find("return") + var indent := code.substr(0, cut) + if expr == "null": + out.append({"kind": "return null→{}", "line": indent + "return {}"}) + return out + if expr == "{}": + # 空字典返回=「清单外查空」类断言的守卫面——非空化击穿(读取通道在位类) + out.append({"kind": "return {}→非空", "line": indent + "return {\"mut_seed\": true}"}) + return out + if expr == "[]": + out.append({"kind": "return []→非空", "line": indent + "return [\"mut_seed\"]"}) + return out + out.append({"kind": "return 置假", "line": indent + "return false"}) + out.append({"kind": "return 置空", "line": indent + "return []"}) + return out + + +# if/elif 条件击穿两面:正条件取反;not 条件去 not(not(not X) ≡ X——对 not 形态取反 +# 无义不生成)。守卫与幂等缓存命中的通用击穿形态(账本旧法「has 恒真/恒假」类的自动替身; +# 跨行条件不处理——行尾无冒号即跳过) +func _if_negate_muts(code: String) -> Array: + var out: Array = [] + var stripped := code.strip_edges() + var head := "" + if stripped.begins_with("if "): + head = "if " + elif stripped.begins_with("elif "): + head = "elif " + if head == "": + return out + var cond := stripped.substr(head.length()) + if not cond.ends_with(":"): + return out + var body := cond.substr(0, cond.length() - 1).strip_edges() + var ci := code.find(head) + var indent := code.substr(0, ci) + if body.begins_with("not "): + out.append({"kind": head.strip_edges() + " 去not", "line": indent + head + body.substr(4) + ":"}) + else: + out.append({"kind": head.strip_edges() + " 取反", "line": indent + head + "not (" + body + "):"}) + return out + + +# const 数组追加元素(词表封闭类断言的击穿面——CELL_TYPES 等词表加一个词表外成员, +# size 与 has 双面翻红;单行字面量数组限定,跨行不处理) +func _const_coll_append_muts(code: String) -> Array: + var out: Array = [] + var stripped := code.strip_edges() + if not stripped.begins_with("const "): + return out + var open_i := code.find("[") + var close_i := code.rfind("]") + if open_i < 0 or close_i <= open_i: + return out + var body := code.substr(open_i + 1, close_i - open_i - 1) + if body.find("[") >= 0 or body.find("]") >= 0: + return out + var nl := code.substr(0, close_i) + ", \"mut_seed\"" + code.substr(close_i) + out.append({"kind": "const 数组追加", "line": nl}) + return out + + +func _string_muts(code: String, cap: int) -> Array: + var out: Array = [] + var i := 0 + while i < code.length() and out.size() < cap: + if code[i] == "\"": + var j := i + 1 + while j < code.length() and code[j] != "\"": + if code[j] == "\\": + j += 1 + j += 1 + if j < code.length(): + var content := code.substr(i + 1, j - i - 1) + if content != "" and not content.contains("res://") and not content.contains("user://") \ + and not content.contains("%") and not content.contains("$"): + var nl := code.substr(0, i + 1) + content + "_x" + code.substr(j) + out.append({"kind": "key 错位:\"%s\"" % content.substr(0, 20), "line": nl}) + i = j + 1 + continue + i += 1 + return out + + +# 状态篡改候选:宿主长活成员 / 模块静态变量与成员初值 / autoload 实例与树状态 +func _state_candidates(st: Dictionary, senv: Dictionary) -> Array: + var cands: Array = [] + var inst: Object = st["inst"] + # (a) 宿主长活成员 + if inst != null and inst.get("_db") != null: + var db = inst.get("_db") + if typeof(db.get("_fatal_flag")) == TYPE_BOOL: + cands.append(_obj_prop_cand(db, "_fatal_flag", true, "宿主._db._fatal_flag")) + if typeof(db.get("is_ready")) == TYPE_BOOL: + cands.append(_obj_prop_cand(db, "is_ready", false, "宿主._db.is_ready")) + # (b) 模块成员初值行(对象出生状态)+ 静态变量运行态改写 + for mp in senv.get("modules", []): + var mps := String(mp) + for c in _module_candidates(mps): + if c.get("scope", "body") == "init": + var c2: Dictionary = c.duplicate(true) + c2["weapon"] = "state" + c2["desc"] = "[状态·初值] " + String(c["desc"]) + cands.append(c2) + var msrc := FileAccess.get_file_as_string(mps) + if msrc == "" and load(mps) != null: + msrc = String(load(mps).source_code) + if msrc == "": + continue + var script = load(mps) + if script == null: + continue + var mlines := msrc.split("\n") + var static_found := 0 + for mi in mlines.size(): + if static_found >= 6: + break + var s := _strip_comment(String(mlines[mi])).strip_edges() + if not s.begins_with("static var "): + continue + var decl := s.substr(11) + var sp := decl.find(" ") + if sp <= 0: + continue + var vname := decl.substr(0, sp) + var old = script.get(vname) + if old == null: + continue + var nv = null + if typeof(old) == TYPE_BOOL: + nv = not old + elif typeof(old) == TYPE_INT: + nv = int(old) + 1 + elif typeof(old) == TYPE_FLOAT: + nv = float(old) + 1.0 + elif typeof(old) == TYPE_STRING and String(old) != "": + nv = String(old) + "_x" + if nv == null: + continue + var mfile := mps.get_file() + var sname := vname + var ov = old + var apply_f := func() -> bool: + script.set(sname, nv) + return true + var restore_f := func() -> void: + script.set(sname, ov) + cands.append({"weapon": "state", "file": mps, "line": mi, "kind": "static_var", + "desc": "[状态·静态] %s:%d %s → %s" % [mfile, mi + 1, sname, _fmt_val(nv)], + "apply": apply_f, "restore": restore_f}) + static_found += 1 + # (c) autoload 实例与树状态 + for aname in st["cfg"].get("state_autoloads", []): + var nm := String(aname) + var node := get_root().get_node_or_null(NodePath("/root/" + nm)) + if node == null: + continue + var nobj: Object = node + var apply_r := func() -> bool: + nobj.set("name", nm + "__mut") + return true + var restore_r := func() -> void: + nobj.set("name", nm) + cands.append({"weapon": "state", "file": "/root/" + nm, "line": -1, "kind": "tree_rename", + "desc": "[状态·树] autoload 节点 /root/%s 改名(装配链就绪护栏击穿)" % nm, + "apply": apply_r, "restore": restore_r}) + var bound := 0 + for p in nobj.get_property_list(): + if bound >= 12: + break + var pname := String(p["name"]) + if pname.begins_with("resource_") or pname in ["script", "metadata", "name", "owner", + "process_mode", "process_priority", "physics_process_priority", + "scene_file_path", "unique_name_in_owner"]: + continue + if (int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE) == 0: + continue + var pv = nobj.get(pname) + if typeof(pv) == TYPE_INT: + var o1: int = pv + var p1 := pname + var apply_p1 := func() -> bool: + nobj.set(p1, o1 + 1) + return true + var restore_p1 := func() -> void: + nobj.set(p1, o1) + cands.append({"weapon": "state", "file": "/root/" + nm, "line": -1, "kind": "obj_prop", + "set_key": p1, "desc": "[状态·实例] %s.%s: %d → %d" % [nm, p1, o1, o1 + 1], + "apply": apply_p1, "restore": restore_p1}) + bound += 1 + elif typeof(pv) == TYPE_BOOL: + var o2: bool = pv + var p2 := pname + var apply_p2 := func() -> bool: + nobj.set(p2, not o2) + return true + var restore_p2 := func() -> void: + nobj.set(p2, o2) + cands.append({"weapon": "state", "file": "/root/" + nm, "line": -1, "kind": "obj_prop", + "set_key": p2, "desc": "[状态·实例] %s.%s: %s → %s" % [nm, p2, _fmt_val(o2), _fmt_val(not o2)], + "apply": apply_p2, "restore": restore_p2}) + bound += 1 + return cands + + +func _obj_prop_cand(obj: Object, pname: String, nv, label: String) -> Dictionary: + var old = obj.get(pname) + var apply_f := func() -> bool: + obj.set(pname, nv) + return true + var restore_f := func() -> void: + obj.set(pname, old) + return {"weapon": "state", "file": label, "line": -1, "kind": "obj_prop", "set_key": pname, + "desc": "[状态·实例] %s: %s → %s" % [label, _fmt_val(old), _fmt_val(nv)], + "apply": apply_f, "restore": restore_f} + + +# ============================ 改坏施加与还原 ============================ + +# 活树代理:套代码里 root/get_root() 经编译变换改道至此(_mut_root())。代理把 add_child +# 引流到真实 root 并记录入树顶层节点——供「树形状态模板」武器按基线树形在 trial 内改写 +# 场景承载脚本的节点属性(.tscn 经 uid 直引脚本对象+场景缓存,路径接管不可达——这是 +# 呈现类断言唯一可达的改坏通道)。套代码用到的 root 面仅 add_child/get_node_or_null/ +# get_children(system_test 七处 add_child+autoload 探针;其余方法按需在此扩)。 +func mut_root() -> Object: + if _mut_proxy == null: + _mut_proxy = _MutRootProxy.new(self) + return _mut_proxy + + +class _MutRootProxy: + var eng + func _init(e) -> void: + eng = e + func add_child(n: Node, legacy_argument: bool = false, legible: bool = false) -> void: + eng.root.add_child(n) + eng._on_mut_add(n) + func remove_child(n: Node) -> void: + eng.root.remove_child(n) + func get_children() -> Array: + return eng.root.get_children() + func get_child(i: int) -> Node: + return eng.root.get_child(i) + func get_node_or_null(p) -> Node: + return eng.root.get_node_or_null(p) + func has_node(p) -> bool: + return eng.root.has_node(p) + func _to_string() -> String: + return str(eng.root) + + +func _on_mut_add(top: Node) -> void: + _mut_tops.append(top) + var top_i := _mut_tops.size() - 1 + if _mut_recording: + _mine_tree_templates(top, top_i) + for p in _mut_pending: + if int(p["top"]) != top_i: + continue + var n: Node = top + var ok := true + for seg in p["path"]: + n = n.get_node_or_null(NodePath(String(seg))) + if n == null: + ok = false + break + if ok: + n.set(String(p["prop"]), p["val"]) + + +# 树形模板挖掘(基线轮、入树即时):子树展平至深 3,Control 取 visible 翻转+mouse_filter +# 轮转,全部节点取 name 加尾——场景承载断言的默认态/在位链改坏面。路径=自顶的名字链。 +func _mine_tree_templates(top: Node, top_i: int) -> void: + if _mut_tree_templates.size() >= 24: + return + var stack: Array = [[top, []]] + while not stack.is_empty() and _mut_tree_templates.size() < 24: + var e: Array = stack.pop_front() + var n: Node = e[0] + var path: Array = e[1] + if not path.is_empty(): + if n is Control and typeof(n.get("visible")) == TYPE_BOOL: + _mut_tree_templates.append({"top": top_i, "path": path, "prop": "visible", + "val": not bool(n.get("visible")), "desc": "树态 %s.visible → %s" % ["/".join(PackedStringArray(path)), str(not bool(n.get("visible")))]}) + if n is Control and typeof(n.get("mouse_filter")) == TYPE_INT: + var mf: int = int(n.get("mouse_filter")) + _mut_tree_templates.append({"top": top_i, "path": path, "prop": "mouse_filter", + "val": (mf + 1) % 3, "desc": "树态 %s.mouse_filter → %d" % ["/".join(PackedStringArray(path)), (mf + 1) % 3]}) + if n.get_parent() != null: + _mut_tree_templates.append({"top": top_i, "path": path, "prop": "name", + "val": String(n.name) + "__mut", "desc": "树态 %s.name 改名" % "/".join(PackedStringArray(path))}) + if path.size() < 3: + for c in n.get_children(): + var np: Array = path.duplicate() + np.append(String(c.name)) + stack.push_back([c, np]) + + +func _tree_state_candidates() -> Array: + var cands: Array = [] + for t in _mut_tree_templates: + var tmpl: Dictionary = t + var apply_f := func() -> bool: + _mut_pending = [tmpl] + return true + var restore_f := func() -> void: + _mut_pending = [] + cands.append({"weapon": "state", "file": "/root(树态)", "line": -1, "kind": "tree_prop", + "desc": "[状态·树] %s" % String(tmpl["desc"]), "apply": apply_f, "restore": restore_f}) + return cands + + +func _apply_candidate(c: Dictionary) -> bool: + var f: Callable = c["apply"] + return bool(f.call()) + + +func _restore_candidate(c: Dictionary) -> void: + var f: Callable = c["restore"] + f.call() + + +# 变体编译+路径接管:变体为无路径 GDScript(去掉 class_name 行——重名注册会 hides-a-global-class +# 编译失败),编译成功后 take_over_path 让后续 load(路径) 取到变体;原件对象零 reload 零触碰 +func _take_over_variant(path: String, src: String) -> bool: + var v := GDScript.new() + v.source_code = _strip_class_name_line(src) + if v.reload() != OK: + return false + v.take_over_path(path) + return true + + +func _strip_class_name_line(src: String) -> String: + var ls := src.split("\n") + for i in ls.size(): + if String(ls[i]).strip_edges().begins_with("class_name "): + ls.remove_at(i) + break + return "\n".join(ls) + + +func _verify_scripts_restored() -> String: + var bad: Array[String] = [] + for p in _orig_scripts.keys(): + _orig_scripts[p].take_over_path(String(p)) # 兜底夺回(正常已由逐候选还原完成) + for p in _orig_scripts.keys(): + if load(String(p)) != _orig_scripts[p]: + bad.append(String(p) + "(缓存映射非原件)") + for p in _orig_src.keys(): + if FileAccess.get_file_as_string(String(p)) != String(_orig_src[p]): + bad.append(String(p) + "(磁盘源码变动!)") + if bad.is_empty(): + return "" + return "、".join(bad) + + +# ============================ user:// 真实档守卫 ============================ + +func _backup_user_save() -> void: + _user_backup.clear() + for p in _USER_GUARD_FILES: + var path := "user://" + String(p) + if FileAccess.file_exists(path): + var f := FileAccess.open(path, FileAccess.READ) + _user_backup[p] = f.get_buffer(f.getLength()) + f.close() + else: + _user_backup[p] = null + var cnt := 0 + for p in _USER_GUARD_FILES: + if _user_backup.get(p, null) != null: + cnt += 1 + print("[mut] user:// 真实档已备份(在档 %d 个)——跑毕核对零触碰" % cnt) + + +func _restore_user_save() -> void: + var touched := 0 + for p in _USER_GUARD_FILES: + var path := "user://" + String(p) + var orig = _user_backup.get(p, null) + var cur_exists := FileAccess.file_exists(path) + if orig == null: + if cur_exists: + DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) + touched += 1 + else: + var cur := PackedByteArray() + if cur_exists: + var f := FileAccess.open(path, FileAccess.READ) + if f != null: + cur = f.get_buffer(f.getLength()) + f.close() + if cur != orig: + var w := FileAccess.open(path, FileAccess.WRITE) + w.store_buffer(orig) + w.close() + touched += 1 + if touched > 0: + push_error("[mut] 警告:user:// 真实档被 %d 个文件触碰,已按开工备份还原" % touched) + else: + print("[mut] user:// 真实档零触碰(跑毕与开工逐字节一致)") + + +# ============================ 结果落盘与台账 ============================ + +func _result_path(host_id: String, suite: String) -> String: + return _RESULT_DIR + "/" + host_id + "__" + suite + ".json" + + +func _write_result(host_id: String, suite: String, data: Dictionary) -> void: + data["generated_at"] = Time.get_datetime_string_from_system() + var f := FileAccess.open(_result_path(host_id, suite), FileAccess.WRITE) + if f == null: + push_error("[mut] 结果写盘失败:" + _result_path(host_id, suite)) + return + f.store_string(JSON.stringify(data, " ")) + f.close() + var nv := 0 + var na := 0 + for r in data["rows"]: + if r["status"] == "verified": + nv += 1 + else: + na += 1 + print("[mut] 结果已落盘 %s(%d 条:验红 %d / 异常 %d)" % [_result_path(host_id, suite), data["rows"].size(), nv, na]) + + +func _exit_code_from_results() -> int: + var files := _list_results() + if files.is_empty(): + return 2 + var unresolved := 0 + for fp in files: + var data := _read_json(String(fp)) + for r in data.get("rows", []): + if r["status"] != "verified" and String(r.get("reason", "")) == "": + unresolved += 1 + return 1 if unresolved > 0 else 0 + + +func _list_results() -> Array: + var out: Array = [] + var dir := DirAccess.open(_RESULT_DIR) + if dir == null: + return out + dir.list_dir_begin() + var f := dir.get_next() + while f != "": + if not dir.current_is_dir() and f.ends_with(".json"): + out.append(_RESULT_DIR + "/" + f) + f = dir.get_next() + dir.list_dir_end() + out.sort() + return out + + +func _read_json(path: String) -> Dictionary: + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + return {} + var data = JSON.parse_string(f.get_as_text()) + f.close() + return data if data is Dictionary else {} + + +# 台账落盘路径:缺省 = 仓库根 reports/(res:// 映射 game/ 切片,仓库根 reports/ 是其上级目录—— +# 直接写 res://reports/ 会在 game/ 下造新目录,触熵增闸红区);--out 显式给绝对路径时原样用。 +func _ledger_out_path() -> String: + var given := String(_args.get("out", "")) + if given != "" and not given.begins_with("res://"): + return given + if given.begins_with("res://reports/"): + var base := ProjectSettings.globalize_path("res://") + return base + given.substr("res://".length()) + return ProjectSettings.globalize_path("res://") + "../reports/批B-变异统一台账.md" + + +func _build_ledger(out_path: String) -> int: + var files := _list_results() + if files.is_empty(): + print("[mut] 台账中止:无结果 JSON(%s)" % _RESULT_DIR) + return 2 + var ordered: Array = [] + var ordered_keys := {} + for h in Plan.hosts(): + for s in h["suites"].keys(): + var p := _result_path(String(h["id"]), String(s)) + if FileAccess.file_exists(p): + ordered.append(_read_json(p)) + ordered_keys[p] = true + for fp in files: + if not ordered_keys.has(String(fp)): + ordered.append(_read_json(String(fp))) + var total := 0 + var verified := 0 + var anom := 0 + var unresolved := 0 + var md := "" + var cmd_disp := String(_args.get("label", "")) if _args.has("label") else " ".join(OS.get_cmdline_user_args()) + md += "# 批 B 变异统一台账(机器生成)\n\n" + md += "> 生成命令:`godot --headless --path game --script res://tests/mutation/mutation_engine.gd -- %s`\n" % cmd_disp + md += "> 生成时间:%s · 引擎:游戏环境启动一次,逐条执行「内存内改坏 → 重跑该断言所在套函数 → 验 FAIL → 撤销 → 记录」,全程不改任何文件。\n" % Time.get_datetime_string_from_system() + md += "> 三武器:table=已加载 Resource 内存字段改写 · inject=生产脚本源码内存变换+reload 原地重编译(零触盘)· state=对象状态篡改(长活成员/静态与初值/autoload 树状态)。\n" + md += "> 中间结果 JSON 在 `user://mutation_work/results/`(仓库外);本文件为唯一持久产出。\n\n" + md += "## 一、逐条记录\n\n" + md += "| 宿主 | 套 | # | 标签 | 武器 | 改坏点 | 结果 | 复验轮耗时 | 连带红 |\n" + md += "|---|---|---|---|---|---|---|---|---|\n" + for data in ordered: + if data.is_empty(): + continue + for r in data.get("rows", []): + total += 1 + var status := String(r["status"]) + if status == "verified": + verified += 1 + else: + anom += 1 + if String(r.get("reason", "")) == "": + unresolved += 1 + var cand_disp := String(r.get("cand", "")) + var cfile := String(r.get("cand_file", "")) + if cfile != "" and status == "verified": + cand_disp = "%s:%s %s | %s" % [cfile.get_file(), String(r.get("cand_line", "")), String(r.get("cand_kind", "")), cand_disp] + md += "| %s | %s | %d | %s | %s | %s | %s | %s | %s |\n" % [ + _md_cell(String(data["host"])), _md_cell(String(data["suite"]).lstrip("_")), int(r["idx"]), + _md_cell(String(r["label"])), _md_cell(String(r.get("weapon", ""))), _md_cell(cand_disp), + ("验红" if status == "verified" else "异常"), + ("%dms" % int(r.get("ms", 0)) ) if status == "verified" else "—", + str(int(r.get("collateral", 0))) if status == "verified" else "—"] + md += "\n## 二、异常清单(打不红的条+原因)\n\n" + if anom == 0: + md += "(无——全部条目验红)\n" + for data in ordered: + if data.is_empty(): + continue + for a in data.get("anomalies", []): + var idxs := int(a.get("idx", -1)) + var lbl := "" + for r in data.get("rows", []): + if int(r["idx"]) == idxs and String(r["status"]) == "anomaly": + lbl = String(r["label"]) + break + md += "- %s:%s #%s「%s」——%s\n" % [data["host"], String(data["suite"]).lstrip("_"), + (str(idxs) if idxs >= 0 else "—"), _md_cell(lbl), _md_cell(String(a["reason"]))] + for data in ordered: + if not data.is_empty() and not bool(data.get("end_baseline_green", true)): + md += "- %s:%s——跑后基线未复绿(存在跨试跑状态残留)\n" % [data.get("host", "?"), String(data.get("suite", "?")).lstrip("_")] + md += "\n## 三、汇总\n\n" + for data in ordered: + if data.is_empty(): + continue + var v := 0 + var a2 := 0 + for r in data["rows"]: + if r["status"] == "verified": + v += 1 + else: + a2 += 1 + md += "- %s:%s——%d 条(验红 %d / 异常 %d)· 改坏试跑 %d 轮 · 跑后基线%s\n" % [ + data["host"], String(data["suite"]).lstrip("_"), data["rows"].size(), v, a2, + int(data.get("tried", 0)), "复绿" if bool(data.get("end_baseline_green", true)) else "**未复绿**"] + md += "\n合计 %d 条:验红 %d / 异常 %d。\n" % [total, verified, anom] + var f := FileAccess.open(out_path, FileAccess.WRITE) + if f == null: + push_error("[mut] 台账写盘失败:" + out_path) + return 1 + f.store_string(md) + f.close() + print("[mut] 台账已生成 %s(%d 条:验红 %d / 异常 %d)" % [out_path, total, verified, anom]) + if total == 0: + return 2 + return 1 if unresolved > 0 else 0 + + +func _md_cell(s: String) -> String: + var out := s.replace("|", "/").replace("\n", " ").replace("\t", " ") + if out.length() > 96: + out = out.substr(0, 96) + "…" + return out diff --git a/game/tests/mutation/mutation_plan.gd b/game/tests/mutation/mutation_plan.gd new file mode 100644 index 00000000..3bced315 --- /dev/null +++ b/game/tests/mutation/mutation_plan.gd @@ -0,0 +1,159 @@ +# tests/mutation/mutation_plan.gd — 变异验证引擎的计划配置(数据件,无逻辑) +# 宿主×套×变异策略一览:套→守的生产模块(账本 §二末列)+武器优先序+运行参数。 +# src 指「被变换编译的宿主源码」:稳定宿主直接读工作区文件;迁移中的宿主(如 seal) +# 用驱动命令物化的快照(user:// 下,仓库外)——物化命令见套配置旁注与台账头部。 +# 武器代号:table=表/资源改写(内存字段写读还原)· inject=注入编译(生产源码内存变换 +# reload 原地替换·零触盘)· state=状态篡改(静态变量/成员初值/autoload 实例与树状态)。 +# budget=每套改坏点试跑上限(轮)· cap_ms=每套墙钟上限 · count=静态断言点数(对账用)。 +extends RefCounted + +const GLOBAL_WATCHDOG_MS := 14400000 # 引擎全局看门狗(防挂死占闸;外层 bash 超时为最终兜底——4h,慢套全程靠它) +const SUITE_CAP_MS_DEFAULT := 600000 # 单套墙钟上限缺省(宿主级 cap_ms 可覆盖) +const BUDGET_DEFAULT := 160 # 单套试跑轮数缺省 + + +static func hosts() -> Array: + return [ + { + "id": "system_test", + "src": "res://tests/system_test.gd", + "channel": "self", # 断言通道 = 宿主自有 check(cond, label) + "async": true, # 套函数为协程(await process_frame) + "budget": 240, + "cap_ms": 2400000, # 慢套墙钟上限(boot 链/活战斗驱动单轮 20–60s) + "state_autoloads": ["ConfigDB", "SaveManager"], + "suites": { + "_split_tail": {"count": 1, "weapons": ["state"], "modules": [], "tables": [], + "strip_siblings": true, "funcs": {}}, + "_smoke_scene_split": {"count": 11, "weapons": ["inject", "state"], "tables": [], + "modules": ["res://presentation/scenes/menu_screen.gd", "res://presentation/scenes/camp_screen.gd", + "res://presentation/scenes/adventure_screen.gd", "res://presentation/main.gd"], + "funcs": {"menu_screen.gd": ["_ready", "_build_ui"], "camp_screen.gd": ["_ready", "_build_ui"], + "adventure_screen.gd": ["_ready", "_build_ui"], "main.gd": ["_apply_layer_contract", "_build_ui", "_ready"]}}, + "_smoke_boot_chain": {"count": 15, "weapons": ["inject", "state"], "tables": [], + "modules": ["res://presentation/settings_store.gd", "res://core/save_manager.gd", + "res://presentation/main.gd", "res://presentation/views/ui_kit.gd", "res://core/config_db.gd"], + "funcs": {"settings_store.gd": ["*"], "save_manager.gd": ["active_slot", "keystone_balance", + "write_to_disk", "has_breakpoint", "set_breakpoint", "clear_breakpoint", "slot_path", "adventure_total", "has_write_target"], + "main.gd": ["_ready", "_build_ui", "_refresh_resume_button", "_begin_session"]}}, + "_smoke_run_lifecycle": {"count": 11, "weapons": ["inject", "state"], "tables": [], + "modules": ["res://presentation/run_scope.gd", "res://presentation/views/battle_end_overlay.gd", + "res://presentation/main.gd", "res://core/adventure_runner.gd"], + "funcs": {"run_scope.gd": ["*"], "battle_end_overlay.gd": ["is_open", "open", "close"], + "main.gd": ["_start_adventure_from_menu", "_begin_session", "_on_battle_end_continue", "_refresh_resume_button"], + "adventure_runner.gd": ["abandon", "clear_breakpoint"]}}, + "_smoke_lifecycle_matrix": {"count": 10, "weapons": ["inject", "state"], "tables": [], + "modules": ["res://presentation/scenes/menu_screen.gd", "res://core/battle_orchestrator.gd", + "res://presentation/views/battle_end_overlay.gd", "res://presentation/main.gd", "res://core/adventure_runner.gd"], + "funcs": {"menu_screen.gd": ["_build_ui"], "battle_orchestrator.gd": ["is_battle_live", "begin_battle"], + "battle_end_overlay.gd": ["is_open"], "main.gd": ["_begin_session", "_on_battle_end_continue", + "_resume_from_menu", "_open_settings", "_open_career", "_show_debug_overlay"], + "adventure_runner.gd": ["abandon", "clear_breakpoint"]}}, + "_stage_faces": {"count": 1, "run_fn": "_smoke_lifecycle_matrix", "weapons": ["inject", "state"], + "tables": [], "modules": ["res://presentation/modal_flow.gd", "res://presentation/main.gd"], + "funcs": {"modal_flow.gd": ["*"], "main.gd": ["_setup_modal_flow", "_build_ui"]}}, + }, + }, + { + "id": "table_structure_guard", + "src": "res://tests/unit/data_config/table_structure_guard_test.gd", + "channel": "kit", + "async": false, + "budget": 3000, + "cap_ms": 1200000, + "state_autoloads": [], + "suites": { + "_smoke_ac_swap_guard": {"count": 12, "weapons": ["table", "inject"], + "tables": ["enemy", "status_effects", "reward", "settlement", "hero"], + "modules": ["res://core/unit_container.gd", "res://core/config_db_resolver.gd"], "funcs": {}}, + "_smoke_enemy_bands": {"count": 4, "weapons": ["table", "inject"], + "tables": ["enemy"], "modules": ["res://core/config_db_resolver.gd"], "funcs": {}}, + "_smoke_variation_framework": {"count": 9, "weapons": ["table", "inject"], + "tables": ["variation"], "modules": ["res://data/schemas/variation_table.gd"], "funcs": {}}, + "_smoke_difficulty_table": {"count": 8, "weapons": ["table", "inject"], + "tables": ["difficulty"], "modules": ["res://core/unit_container.gd", "res://core/config_db_resolver.gd"], "funcs": {}}, + "_smoke_nine_types_gp_stream": {"count": 6, "weapons": ["table", "inject"], + "tables": ["reward"], "modules": ["res://data/schemas/reward_table.gd", "res://core/rng_service.gd"], "funcs": {}}, + "_smoke_adventure_table": {"count": 17, "weapons": ["table", "inject"], + "tables": ["adventure", "treasure_items", "meta"], + "modules": ["res://data/schemas/adventure_table.gd", "res://core/attunement.gd", + "res://data/schemas/treasure_item_table.gd", "res://core/battle_orchestrator.gd", "res://core/meta_progression.gd", + "res://core/config_db.gd"], "funcs": {}}, + "_smoke_meta": {"count": 4, "weapons": ["table", "inject"], + "tables": ["meta"], "modules": ["res://core/meta_progression.gd"], "funcs": {}}, + }, + }, + { + "id": "board_gen_reward", + "src": "res://tests/unit/core/board_gen_reward_test.gd", + "channel": "kit", + "async": false, + "budget": 800, + "state_autoloads": [], + "suites": { + "_smoke_layout_cell_formula": {"count": 2, "weapons": ["inject", "state"], "tables": [], + "modules": ["res://presentation/views/board_view.gd"], "funcs": {}}, + "_smoke_reward_identity_unit": {"count": 10, "weapons": ["inject", "state"], "tables": [], + "modules": ["res://core/reward_economy.gd"], "funcs": {}}, + "_smoke_board_generator": {"count": 22, "weapons": ["inject", "table"], + "tables": ["board_gen"], + "modules": ["res://core/board_generator.gd", "res://core/config_db_resolver.gd"], "funcs": {}}, + "_smoke_power_interval": {"count": 5, "weapons": ["table", "inject"], "tables": ["reward"], + "modules": ["res://core/reward_economy.gd"], "funcs": {}}, + }, + }, + { + "id": "roll_multiplier", + "src": "res://tests/unit/battle_settlement/roll_multiplier_test.gd", + "channel": "kit", + "async": false, + "budget": 400, + "cap_ms": 1800000, + "state_autoloads": [], + "suites": { + "_smoke_enemy_attack_type": {"count": 4, "weapons": ["table", "inject"], + "tables": ["enemy"], "modules": ["res://core/unit_container.gd", "res://core/damage_controller.gd"], "funcs": {}}, + "_smoke_armor_roll": {"count": 15, "weapons": ["table", "inject"], "tables": ["settlement"], + "modules": ["res://core/damage_math.gd", "res://core/damage_controller.gd", "res://core/status_container.gd"], "funcs": {}}, + "_smoke_pool_wiring": {"count": 4, "weapons": ["table", "inject"], "tables": ["status_effects"], + "modules": ["res://core/damage_controller.gd"], "funcs": {}}, + "_smoke_damage_types": {"count": 10, "weapons": ["table", "inject"], "tables": ["enemy", "settlement"], + "modules": ["res://core/settlement/grant_segment.gd", "res://core/damage_controller.gd"], "funcs": {}}, + "_smoke_attack_chain_fresh_pool": {"count": 3, "weapons": ["inject", "state"], "tables": [], + "modules": ["res://core/battle_orchestrator.gd", "res://core/damage_controller.gd"], "funcs": {}}, + }, + }, + { + "id": "seal_polarity", + # 迁移中宿主:读驱动命令物化的提交快照(5ed1ec99 版=套1 五条在册形态),不读 WIP 工作区 + "src": "user://mutation_work/seal_polarity_test.gd", + "channel": "kit", + "async": false, + "budget": 600, + "state_autoloads": [], + "suites": { + "_smoke_seal_ap_exhausted": {"count": 5, "weapons": ["state", "table", "inject"], + "tables": ["action_economy"], + "modules": ["res://core/battle_orchestrator.gd", "res://core/action_economy.gd", "res://core/minesweeper_rules.gd"], + # funcs 提示=账本 §九 G5 套1 旧法变异点所在函数(只作试跑排序,不作唯一改坏面) + "funcs": {"battle_orchestrator.gd": ["begin_battle"], + "minesweeper_rules.gd": ["revealed_cells", "settle_batch"], + "action_economy.gd": ["_cost", "try_spend", "_spend_cost_and_arbitrate", "_spend_and_arbitrate"]}}, + }, + }, + ] + + +static func host_by_id(id: String) -> Dictionary: + for h in hosts(): + if h["id"] == id: + return h + return {} + + +static func total_static_checks() -> int: + var n := 0 + for h in hosts(): + for s in h["suites"].values(): + n += int(s["count"]) + return n diff --git a/reports/批B-变异统一台账.md b/reports/批B-变异统一台账.md new file mode 100644 index 00000000..b256907b --- /dev/null +++ b/reports/批B-变异统一台账.md @@ -0,0 +1,291 @@ +# 批 B 变异统一台账(机器生成) + +> 生成命令:`godot --headless --path game --script res://tests/mutation/mutation_engine.gd -- --all --snap=seal_polarity=res://tests/.gates/mut_snapshots/seal_polarity_test.gd(23 套分批跑齐:system/TSG/board/roll/seal;board_generator 与 roll 尾套带 --excl-func/--excl-line 毒候选隔离与 --budget 收刀,各批命令见实现报告)` +> 生成时间:2026-10-23T13:33:47 · 引擎:游戏环境启动一次,逐条执行「内存内改坏 → 重跑该断言所在套函数 → 验 FAIL → 撤销 → 记录」,全程不改任何文件。 +> 三武器:table=已加载 Resource 内存字段改写 · inject=生产脚本源码内存变换+reload 原地重编译(零触盘)· state=对象状态篡改(长活成员/静态与初值/autoload 树状态)。 +> 中间结果 JSON 在 `user://mutation_work/results/`(仓库外);本文件为唯一持久产出。 + +## 一、逐条记录 + +| 宿主 | 套 | # | 标签 | 武器 | 改坏点 | 结果 | 复验轮耗时 | 连带红 | +|---|---|---|---|---|---|---|---|---| +| system_test | split_tail | 0 | false | state | ConfigDB:-1 tree_rename / [状态·树] autoload 节点 /root/ConfigDB 改名(装配链就绪护栏击穿) | 验红 | 13ms | 0 | +| system_test | smoke_scene_split | 1 | menu.start_overlay != null and menu.lvbtn != null and menu.resumebtn != null and menu.campbtn !=… | | | 异常 | — | — | +| system_test | smoke_scene_split | 2 | menu.start_overlay.visible | | | 异常 | — | — | +| system_test | smoke_scene_split | 3 | menu.mouse_filter == Control.MOUSE_FILTER_IGNORE | | | 异常 | — | — | +| system_test | smoke_scene_split | 4 | camp.camp_overlay != null | | | 异常 | — | — | +| system_test | smoke_scene_split | 5 | not camp.camp_overlay.visible | | | 异常 | — | — | +| system_test | smoke_scene_split | 6 | adv.battle_root != null and adv.stage != null and adv.board_zone != null and adv.board_view != n… | | | 异常 | — | — | +| system_test | smoke_scene_split | 7 | top == main._start_overlay | inject | main.gd:198 bool true→false / main.gd:199 bool true→false [_start_overlay.visible = true → _star… | 验红 | 221ms | 0 | +| system_test | smoke_scene_split | 8 | main._menu_screen.get_index() > main._adventure_screen.get_index() | inject | main.gd:467 num 1→0 / main.gd:468 num 1→0 [slot += 1 → slot += 0] | 验红 | 204ms | 2 | +| system_test | smoke_scene_split | 9 | chain_ok | inject | main.gd:222 asgn 置null / main.gd:223 asgn 置null [info_panel = _adventure_screen.info_panel → inf… | 验红 | 224ms | 0 | +| system_test | smoke_scene_split | 10 | main._start_overlay == main._menu_screen.start_overlay and main.camp_overlay == main._camp_scree… | inject | main.gd:218 asgn 置null / main.gd:219 asgn 置null [_start_overlay = _menu_screen.start_overlay → _… | 验红 | 221ms | 1 | +| system_test | smoke_scene_split | 11 | main.run_scope != null and not main.run_scope.is_adventure | inject | main.gd:196 bool false→true / main.gd:197 bool false→true [_begin_session(false) # 闲时会话(gp_led… | 验红 | 214ms | 0 | +| system_test | smoke_boot_chain | 12 | db.is_ready and db.hero != null | | | 异常 | — | — | +| system_test | smoke_boot_chain | 13 | AudioServer.is_bus_mute(master) == (ss.volume_linear <= 0.001) and is_equal_approx(AudioServer.g… | | | 异常 | — | — | +| system_test | smoke_boot_chain | 14 | ui_kit.motion_scale == (0.5 if ss.reduced_motion else 1.0) | | | 异常 | — | — | +| system_test | smoke_boot_chain | 15 | main._start_overlay.visible | inject | main.gd:198 bool true→false / main.gd:199 bool true→false [_start_overlay.visible = true → _star… | 验红 | 121ms | 2 | +| system_test | smoke_boot_chain | 16 | not sm.has_breakpoint() and not main._resume_btn.visible | | | 异常 | — | — | +| system_test | smoke_boot_chain | 17 | sm.active_slot() == 0 and not sm.has_write_target() | inject | save_manager.gd:358 bool false→true / save_manager.gd:359 bool false→true [return false → return… | 验红 | 133ms | 1 | +| system_test | smoke_boot_chain | 18 | sm.keystone_balance() == 0 and not sm.has_breakpoint() and sm.adventure_total() == 0 | | | 异常 | — | — | +| system_test | smoke_boot_chain | 19 | not sm.write_to_disk() | inject | save_manager.gd:358 bool false→true / save_manager.gd:359 bool false→true [return false → return… | 验红 | 131ms | 1 | +| system_test | smoke_boot_chain | 20 | not (FileAccess.file_exists("user://save.json") and not FileAccess.file_exists(sm.slot_path(1))) | | | 异常 | — | — | +| system_test | smoke_boot_chain | 21 | not sm.has_breakpoint() and not main._resume_btn.visible | | | 异常 | — | — | +| system_test | smoke_boot_chain | 22 | sm.has_breakpoint() and main._resume_btn.visible | inject | main.gd:533 op ==→!= / main.gd:534 op ==→!= [if _resume_btn == null: → if _resume_btn != null:] | 验红 | 122ms | 2 | +| system_test | smoke_boot_chain | 23 | not main._resume_btn.visible | | | 异常 | — | — | +| system_test | smoke_boot_chain | 24 | main.run_scope != null and not main.run_scope.is_adventure and main.run_scope.is_inside_tree() | inject | main.gd:171 op !=→== / main.gd:172 op !=→== [if run_scope != null: → if run_scope == null:] | 验红 | 138ms | 2 | +| system_test | smoke_boot_chain | 25 | not main.debug_overlay.visible | | | 异常 | — | — | +| system_test | smoke_boot_chain | 26 | main.debug_badge != null and main.debug_badge.visible | inject | main.gd:199 bool true→false / main.gd:200 bool true→false [_debug_entry = true # 调试入口门控写死=true… | 验红 | 122ms | 2 | +| system_test | smoke_run_lifecycle | 27 | main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null | inject | main.gd:492 if 取反 / main.gd:493 if 取反 [if save_manager.has_breakpoint(): → if not (save_manager.… | 验红 | 138ms | 0 | +| system_test | smoke_run_lifecycle | 28 | main.run_scope.hints_shown.is_empty() | | | 异常 | — | — | +| system_test | smoke_run_lifecycle | 29 | main.run_scope.adventure.abandon() | | | 异常 | — | — | +| system_test | smoke_run_lifecycle | 30 | finished.done | inject | main.gd:11 key 错位:"/root/RngService" / main.gd:12 key 错位:"/root/RngService" [const AUTOLOAD_PATH… | 验红 | 331ms | 3 | +| system_test | smoke_run_lifecycle | 31 | main._end_overlay.is_open() | inject | main.gd:11 key 错位:"/root/RngService" / main.gd:12 key 错位:"/root/RngService" [const AUTOLOAD_PATH… | 验红 | 333ms | 3 | +| system_test | smoke_run_lifecycle | 32 | not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner =… | inject | main.gd:726 bool false→true / main.gd:727 bool false→true [_begin_session(false) → _begin_sessio… | 验红 | 141ms | 1 | +| system_test | smoke_run_lifecycle | 33 | main.run_scope.hints_shown.is_empty() and not main.run_scope.boss_active and main.run_scope.last… | inject | main.gd:712 if 取反 / main.gd:713 if 取反 [if _bonus.resume_if_pending(): # 三选一停点在途恢复入口… → if not … | 验红 | 141ms | 2 | +| system_test | smoke_run_lifecycle | 34 | main._start_overlay.visible | inject | main.gd:728 bool true→false / main.gd:729 bool true→false [_start_overlay.visible = true → _star… | 验红 | 153ms | 1 | +| system_test | smoke_run_lifecycle | 35 | not sm.has_breakpoint() | | | 异常 | — | — | +| system_test | smoke_run_lifecycle | 36 | main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.… | inject | main.gd:174 asgn 置null / main.gd:175 asgn 置null [run_scope.is_adventure = adventure_session → ru… | 验红 | 212ms | 2 | +| system_test | smoke_run_lifecycle | 37 | not main._start_overlay.visible | inject | main.gd:498 bool false→true / main.gd:499 bool false→true [_start_overlay.visible = false → _sta… | 验红 | 141ms | 1 | +| system_test | smoke_lifecycle_matrix | 39 | main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live() | | | 异常 | — | — | +| system_test | smoke_lifecycle_matrix | 40 | main.run_scope.adventure.abandon() | | | 异常 | — | — | +| system_test | smoke_lifecycle_matrix | 41 | finished.done and main._end_overlay.is_open() | | | 异常 | — | — | +| system_test | smoke_lifecycle_matrix | 42 | main.run_scope != old_scope and main.run_scope.is_inside_tree() | inject | main.gd:716 bool false→true / main.gd:717 bool false→true [_settlement_stop_pending = false → _s… | 验红 | 6481ms | 2 | +| system_test | smoke_lifecycle_matrix | 43 | not is_instance_valid(old_scope) | inject | main.gd:716 bool false→true / main.gd:717 bool false→true [_settlement_stop_pending = false → _s… | 验红 | 6341ms | 2 | +| system_test | smoke_lifecycle_matrix | 44 | false | | | 异常 | — | — | +| system_test | smoke_lifecycle_matrix | 45 | main.run_scope.adventure != null and main.run_scope.adventure != first_adv | | | 异常 | — | — | +| system_test | smoke_lifecycle_matrix | 46 | main.run_scope.adventure.abandon() | | | 异常 | — | — | +| system_test | smoke_lifecycle_matrix | 47 | main._resume_btn.visible | inject | main.gd:533 op ==→!= / main.gd:534 op ==→!= [if _resume_btn == null: → if _resume_btn != null:] | 验红 | 4864ms | 0 | +| system_test | smoke_lifecycle_matrix | 48 | main.run_scope != idle_scope and main.run_scope.is_inside_tree() | inject | main.gd:544 if 去not / main.gd:545 if 去not [if not save_manager.has_breakpoint(): → if save_manag… | 验红 | 2963ms | 1 | +| system_test | stage_faces | 38 | bad.is_empty() | inject | main.gd:218 asgn 置null / main.gd:219 asgn 置null [_start_overlay = _menu_screen.start_overlay → _… | 验红 | 307ms | 0 | +| table_structure_guard | smoke_ac_swap_guard | 21 | "状态表 defense_up 行已退役(无双轨)" | table | status_effects.rows.effect_id=defense_up:-1 table 字面量插行 / 表插行 status_effects.rows:effect_id=defe… | 验红 | 5ms | 0 | +| table_structure_guard | smoke_ac_swap_guard | 22 | "ac_up 行在位(hero · +1 ac · 只续时不叠层——2026-09-20 主角增益续时批)" | table | status_effects.rows[1].effect_id:-1 table 删键 / 表行删键 status_effects.rows[1].effect_id | 验红 | 5ms | 0 | +| table_structure_guard | smoke_ac_swap_guard | 23 | "奖励表 defense 正向行 effect_id = ac_up(显示名盾牌加护,权重/tier 不变)" | table | reward.rows[1].cell_type:-1 table 删键 / 表行删键 reward.rows[1].cell_type | 验红 | 5ms | 0 | +| table_structure_guard | smoke_ac_swap_guard | 24 | "effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)" | table | settlement.effect_pool_map.defense_up:-1 table 字典加键 / 字典加键 settlement.effect_pool_map:defense_up… | 验红 | 5ms | 0 | +| table_structure_guard | smoke_ac_swap_guard | 25 | "hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [ wall_detail if wall_deta… | | | 异常 | — | — | +| table_structure_guard | smoke_ac_swap_guard | 26 | "主角 armor_class 在域内(%d——暂定基准随表走)" % int(_db.hero.armor_class) | table | hero.armor_class:-1 table_set / 表改写 hero.armor_class: 10 → 100 | 验红 | 5ms | 0 | +| table_structure_guard | smoke_ac_swap_guard | 27 | "敌方 AC 行值在已裁带宽内(杂鱼 0~5·重击手/重装手 8~9·Boss 8~12——迭代 5 返工批 v1 重投,越带行%s)" % [ ac_detail if ac_detail … | table | enemy.rows[4].enemy_id:-1 table_set / 表行改写 enemy.rows[4].enemy_id: ogre_chief → ogre_chief_x | 验红 | 5ms | 0 | +| table_structure_guard | smoke_ac_swap_guard | 28 | "Boss AC 关卡台阶严格递增(%d < %d < %d)" % [int(boss_ac.get(1, 0)), int(boss_ac.get(2, 0)), int(boss_ac.… | table | enemy.rows[12].level_index:-1 table 删键 / 表行删键 enemy.rows[12].level_index | 验红 | 5ms | 0 | +| table_structure_guard | smoke_ac_swap_guard | 29 | "熊地精首遇带(1,battle,%d)降档锚定(hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1;首遇带数据驱动定位)〔fv21〕"… | table | enemy.rows[2].level_index:-1 table 删键 / 表行删键 enemy.rows[2].level_index | 验红 | 5ms | 1 | +| table_structure_guard | smoke_ac_swap_guard | 30 | "第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" % [ curve_detail if curve_detail !=… | table | enemy.rows[2].level_index:-1 table 删键 / 表行删键 enemy.rows[2].level_index | 验红 | 5ms | 1 | +| table_structure_guard | smoke_ac_swap_guard | 31 | "掷点参数拍板值在位(d20 / nat20 暴击 ×200%% / 下限 1 / attack_bonus 0 / armor_roll 流)" | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 21 | 验红 | 5ms | 0 | +| table_structure_guard | smoke_ac_swap_guard | 32 | "档位表拍板值在位(缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25——无零档)" | table | settlement.roll_tier_bounds:-1 table 删行 / 表删行 settlement.roll_tier_bounds[0] | 验红 | 5ms | 0 | +| table_structure_guard | smoke_enemy_bands | 17 | "敌人表 20 行且键序 = 迁移表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕" | table | enemy.rows[0].depth_band:-1 table 删键 / 表行删键 enemy.rows[0].depth_band | 验红 | 5ms | 0 | +| table_structure_guard | smoke_enemy_bands | 18 | "hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕〔fv25〕" | table | enemy.rows[3].depth_band:-1 table 删键 / 表行删键 enemy.rows[3].depth_band | 验红 | 5ms | 1 | +| table_structure_guard | smoke_enemy_bands | 19 | "hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕" | | | 异常 | — | — | +| table_structure_guard | smoke_enemy_bands | 20 | "三关 Boss 行各恰一行且 row_kind=boss、band=图 Boss 节点深度(%d/%d/%d%s)〔fv13〕" % [ _boss_depth(1), _boss_dept… | table | enemy.rows[4].depth_band:-1 table 删键 / 表行删键 enemy.rows[4].depth_band | 验红 | 5ms | 1 | +| table_structure_guard | smoke_variation_framework | 33 | "变异清单 6 行(荆棘丛/藤蔓缠绕/岩浆/烬覆 + 落石/矿尘——首批类型定稿批 + 第三关内容批;贫矿脉 2026-10-16 用户令屏蔽删行)" | table | variation.rows:-1 table 复插行 / 表复插行 variation.rows[0] → 表首 | 验红 | 5ms | 3 | +| table_structure_guard | smoke_variation_framework | 34 | "变异表校验零错误(6 行全过域与跨字段互锁——含 scope=global 三互锁负样本)" | table | variation.rows[0].variant_id:-1 table 删键 / 表行删键 variation.rows[0].variant_id | 验红 | 21ms | 0 | +| table_structure_guard | smoke_variation_framework | 35 | "L1 dungeon 候选池 = 荆棘丛+藤蔓缠绕(unlock + board_types 双闸)" | table | variation.rows:-1 table 复插行 / 表复插行 variation.rows[0] → 表首 | 验红 | 5ms | 3 | +| table_structure_guard | smoke_variation_framework | 36 | "L2 underdark 候选池 = 岩浆+烬覆(第二关解锁)" | table | variation.rows:-1 table 复插行 / 表复插行 variation.rows[2] → 表首 | 验红 | 5ms | 2 | +| table_structure_guard | smoke_variation_framework | 37 | "unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)" | table | variation.rows[2].unlock_level:-1 table_set / 表行改写 variation.rows[2].unlock_level: 2 → 1 | 验红 | 5ms | 0 | +| table_structure_guard | smoke_variation_framework | 38 | "board_types 门控:L2 dungeon 板池 = 荆棘丛+藤蔓缠绕(解锁累积;岩浆/烬覆被 board_types ∅ 交集排除)" | table | variation.rows:-1 table 复插行 / 表复插行 variation.rows[0] → 表首 | 验红 | 5ms | 3 | +| table_structure_guard | smoke_variation_framework | 39 | "清单外 variant_id 返回空(读取通道在位)" | table | variation.rows.variant_id=nonexistent:-1 table 字面量插行 / 表插行 variation.rows:variant_id=nonexistent… | 验红 | 5ms | 3 | +| table_structure_guard | smoke_variation_framework | 40 | "跨字段互锁负样本①:flat_damage + placement=any 被拒(只出现在未打开的格子)" | inject | variation_table.gd:28 key 错位:"variant_id" / variation_table.gd:29 key 错位:"variant_id" [var vid: … | 验红 | 5ms | 1 | +| table_structure_guard | smoke_variation_framework | 41 | "跨字段互锁负样本②:hide_number + trigger≠on_number_shown 被拒" | inject | variation_table.gd:28 key 错位:"variant_id" / variation_table.gd:29 key 错位:"variant_id" [var vid: … | 验红 | 5ms | 1 | +| table_structure_guard | smoke_difficulty_table | 42 | "DifficultyTable MVP 空表加载(结构位)" | table | difficulty.rows:-1 table 空表插行 / 空表插行 difficulty.rows(合成行灭 is_empty) | 验红 | 5ms | 0 | +| table_structure_guard | smoke_difficulty_table | 43 | "spawn 受理" | inject | unit_container.gd:37 key 错位:"battle" / unit_container.gd:38 key 错位:"battle" [hero_attack_type: S… | 验红 | 69ms | 0 | +| table_structure_guard | smoke_difficulty_table | 44 | "空表零修饰(hp=%d atk=%d = 行值 %d/%d)" % [int(unit.hp), int(unit.atk), int(d0.hp), int(d0.atk)] | inject | unit_container.gd:57 num 1.0→2.0 / unit_container.gd:58 num 1.0→2.0 [var hp_mult: float = float(… | 验红 | 5ms | 0 | +| table_structure_guard | smoke_difficulty_table | 45 | "修饰后 spawn 受理" | inject | unit_container.gd:116 bool true→false / unit_container.gd:117 bool true→false [return true → ret… | 验红 | 5ms | 2 | +| table_structure_guard | smoke_difficulty_table | 46 | "hp 修饰 ×1.5(%d→%d,max_hp 同步)" % [int(d0.hp), int(g2.hp)] | inject | unit_container.gd:57 key 错位:"hp_mult" / unit_container.gd:58 key 错位:"hp_mult" [var hp_mult: floa… | 验红 | 5ms | 1 | +| table_structure_guard | smoke_difficulty_table | 47 | "atk 修饰 ×2.0(%d→%d)" % [int(d0.atk), int(g2.atk)] | inject | unit_container.gd:58 key 错位:"atk_mult" / unit_container.gd:59 key 错位:"atk_mult" [var atk_mult: f… | 验红 | 5ms | 0 | +| table_structure_guard | smoke_difficulty_table | 48 | "越域修饰 spawn 受理" | inject | unit_container.gd:116 bool true→false / unit_container.gd:117 bool true→false [return true → ret… | 验红 | 5ms | 2 | +| table_structure_guard | smoke_difficulty_table | 49 | "越域乘数 clamp 至 10(%d→%d)" % [int(d0.hp), int(enemy_system_c.all_enemies()[0].hp)] | inject | unit_container.gd:57 key 错位:"hp_mult" / unit_container.gd:58 key 错位:"hp_mult" [var hp_mult: floa… | 验红 | 5ms | 1 | +| table_structure_guard | smoke_nine_types_gp_stream | 50 | "格面资源十型封闭(第九型 gp 九型批 2026-09-20 / 第十型 treasure_item 秘宝内容批 2026-09-29——词表 v1.2)" | | | 异常 | — | — | +| table_structure_guard | smoke_nine_types_gp_stream | 51 | "奖励字典 18 行(九型×正负 18 格——aux 挖格投放四行已退役 2026-10-01 用户拍板甲:战斗/挖格不掉道具,秘宝唯一来路=三选一共享池〔节点/通关〕;清图槽独立批尾不计)" | table | reward.rows:-1 table 复插行 / 表复插行 reward.rows[0] → 表首 | 验红 | 5ms | 0 | +| table_structure_guard | smoke_nine_types_gp_stream | 52 | "危险格负向直伤 %s power=%d(卡⑧投值)" % [rid, int(mine_power[rid])] | table | reward.rows[7].power:-1 table_set / 表行改写 reward.rows[7].power: 6 → 5 | 验红 | 5ms | 0 | +| table_structure_guard | smoke_nine_types_gp_stream | 53 | "正向 cell 行权重 ≥1 全在池 / 负向行权重恒 0 不入池(正向合计 %d——配置推导)" % pos_weight | table | reward.rows[0].weight:-1 table_set / 表行改写 reward.rows[0].weight: 14 → 0 | 验红 | 5ms | 0 | +| table_structure_guard | smoke_nine_types_gp_stream | 54 | "gp 型两行(GP 袋 gp_gain 正向 weight 2 / 破财 gp_loss 负向 weight 0 不入池)" | table | reward.rows:-1 table 删行 / 表删行 reward.rows[18] | 验红 | 5ms | 1 | +| table_structure_guard | smoke_nine_types_gp_stream | 55 | "derive_at 纯函数不推计数器(统一流全局计数器恒 0——并流后纯函数语义不变,gp_drop 专用向量随分盐作废、统一向量中央断言)" | inject | rng_service.gd:103 return 取反 / rng_service.gd:104 return 取反 [return _counter → return not (_coun… | 验红 | 5ms | 0 | +| table_structure_guard | smoke_adventure_table | 0 | "db.adventure 已加载(adventure.tres 入 manifest)" | | | 异常 | — | — | +| table_structure_guard | smoke_adventure_table | 1 | "AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)" | table | adventure.clear_heal_pct:-1 table_set / 表改写 adventure.clear_heal_pct: 20 → 200 | 验红 | 117ms | 1 | +| table_structure_guard | smoke_adventure_table | 2 | "clear_heal_pct ∈ [0,100](%d——通关部分回血比例)" % int(_db.adventure.clear_heal_pct) | table | adventure.clear_heal_pct:-1 table_set / 表改写 adventure.clear_heal_pct: 20 → 200 | 验红 | 125ms | 1 | +| table_structure_guard | smoke_adventure_table | 3 | "档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" % [tier, int(_db.adventure.TIER_QUOTAS[tier])… | table | adventure.curio_quota_attack_common:-1 table_set / 表改写 adventure.curio_quota_attack_common: 18 →… | 验红 | 125ms | 2 | +| table_structure_guard | smoke_adventure_table | 4 | "令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕" | table | adventure.curio_quota_attack_common:-1 table_set / 表改写 adventure.curio_quota_attack_common: 18 →… | 验红 | 124ms | 2 | +| table_structure_guard | smoke_adventure_table | 5 | "数值对(四级换代承袭):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2" | table | adventure.curio_value_attack_common:-1 table_set / 表改写 adventure.curio_value_attack_common: 1 → … | 验红 | 126ms | 0 | +| table_structure_guard | smoke_adventure_table | 6 | "恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕" | | | 异常 | — | — | +| table_structure_guard | smoke_adventure_table | 7 | "cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕" | | | 异常 | — | — | +| table_structure_guard | smoke_adventure_table | 8 | "同调位读数源=树同调位节点点亮数(迭代 21 T4 槽位收编——null 档求值 0;谓词表已退役)〔天赋树批 V14〕" | | | 异常 | — | — | +| table_structure_guard | smoke_adventure_table | 9 | "新目录 12 件全量落表(旧 12 行删除;装配词表=活行 12 件零重复)〔秘宝批 V1〕" | table | treasure_items.rows:-1 table 复插行 / 表复插行 treasure_items.rows[0] → 表首 | 验红 | 119ms | 2 | +| table_structure_guard | smoke_adventure_table | 10 | "命名锁定:12 件 item_id 与 display_name 逐件=正式名(C2 代号即名)〔秘宝批 V2 初筛〕" | table | treasure_items.rows[0].item_id:-1 table 删键 / 表行删键 treasure_items.rows[0].item_id | 验红 | 125ms | 1 | +| table_structure_guard | smoke_adventure_table | 11 | "逐行解锁方式与契约 8 分配一致(钥石 8 件带价格、条件 4 件带谓词行互斥)+三档 7/3/2〔秘宝批 V1〕" | table | treasure_items.rows[0].item_id:-1 table 删键 / 表行删键 treasure_items.rows[0].item_id | 验红 | 117ms | 1 | +| table_structure_guard | smoke_adventure_table | 12 | "装配→战斗贯通:enc.attunement=[morning_order] 起战 attune_has=true(注入面+只读出口)〔秘宝批换主语〕" | inject | battle_orchestrator.gd:198 return 取反 / battle_orchestrator.gd:199 return 取反 [return _attune_has(… | 验红 | 320ms | 1 | +| table_structure_guard | smoke_adventure_table | 13 | "空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)〔秘宝批换主语〕" | inject | battle_orchestrator.gd:198 return 取反 / battle_orchestrator.gd:199 return 取反 [return _attune_has(… | 验红 | 309ms | 1 | +| table_structure_guard | smoke_adventure_table | 14 | "新 12 行效果词零棋盘改写类(危险格/安全格分布同池冻结——真视/开格动作外无写面)〔秘宝批 V9〕" | | | 异常 | — | — | +| table_structure_guard | smoke_adventure_table | 15 | "效果三方向覆盖在册(信息/行动资源/操作能力=plan·抉择刷新——12 件目录结构对齐设计权威)〔秘宝批 V10 覆盖半〕" | | | 异常 | — | — | +| table_structure_guard | smoke_adventure_table | 16 | "装配面空目录合法态:白板档 12 件全未解锁且 mode 分派正确(空目录=合法可玩态)〔秘宝批 V18〕" | table | treasure_items.rows:-1 table 复插行 / 表复插行 treasure_items.rows[0] → 表首 | 验红 | 119ms | 2 | +| table_structure_guard | smoke_meta | 56 | "MetaTable 非空 + validate 零错(三行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)" | table | meta.boss_keystone[0].repeat_kill:-1 table 删键 / 表行删键 meta.boss_keystone[0].repeat_kill | 验红 | 5ms | 0 | +| table_structure_guard | smoke_meta | 57 | "MetaTable 行族规模 3/3/2(Boss/换算恰三/Y 升级两档——forge_products 迭代 21 退役、workshop_items 迭代 22 退役)" | table | meta.boss_keystone:-1 table 复插行 / 表复插行 meta.boss_keystone[0] → 表首 | 验红 | 5ms | 1 | +| table_structure_guard | smoke_meta | 58 | "conversion attr 词表封闭恰全(三属性轴——meta_* 访问器可达)" | table | meta.conversion[0].attr:-1 table 删键 / 表行删键 meta.conversion[0].attr | 验红 | 5ms | 1 | +| table_structure_guard | smoke_meta | 59 | "boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)" | table | meta.boss_keystone[0].level_index:-1 table 删键 / 表行删键 meta.boss_keystone[0].level_index | 验红 | 5ms | 1 | +| board_gen_reward | smoke_layout_cell_formula | 0 | "L%d %dx%d 板网格高 %d ≤ 可用高 %d − 预留 24(格边 %dpx——下缘不贴边不裁切)" % [L, b.width, b.height, grid_h, int(ava… | inject | board_view.gd:226 op ==→!= / board_view.gd:227 op ==→!= [if _board == null or _board.width == 0:… | 验红 | 214ms | 0 | +| board_gen_reward | smoke_layout_cell_formula | 1 | "L%d 格边在 26–46 档内(%dpx)" % [L, int(cell)] | inject | board_view.gd:233 num 46→23(折半) / board_view.gd:234 num 46→23(折半) [_cell = float(clampi(cell, 26… | 验红 | 294ms | 0 | +| board_gen_reward | smoke_reward_identity_unit | 2 | "roll_board 受理" | inject | reward_economy.gd:41 if 取反 / reward_economy.gd:42 if 取反 [if eligible.is_empty(): → if not (eligi… | 验红 | 101ms | 0 | +| board_gen_reward | smoke_reward_identity_unit | 3 | "重复 roll_board 被拒(恰一次——double-roll 守卫)" | inject | reward_economy.gd:40 bool false→true / reward_economy.gd:41 bool false→true [return false → retu… | 验红 | 106ms | 0 | +| board_gen_reward | smoke_reward_identity_unit | 4 | "query_identity 投影 = {reward_id, cell_type, tier} 且五型封闭(样本 %s)" % str(types_seen.keys()) | inject | reward_economy.gd:112 key 错位:"reward_id" / reward_economy.gd:113 key 错位:"reward_id" [return {"re… | 验红 | 101ms | 0 | +| board_gen_reward | smoke_reward_identity_unit | 5 | "清单外坐标查询 = null(封锁格无身份同口径)" | inject | reward_economy.gd:110 return 取反 / reward_economy.gd:111 return 取反 [return null → return not (nul… | 验红 | 101ms | 0 | +| board_gen_reward | smoke_reward_identity_unit | 6 | "同格重复 grant 幂等(同一快照——受理恰一次)" | | | 异常 | — | — | +| board_gen_reward | smoke_reward_identity_unit | 7 | "清单外 grant 拒绝" | inject | reward_economy.gd:136 return 取反 / reward_economy.gd:137 return 取反 [return null → return not (nul… | 验红 | 100ms | 0 | +| board_gen_reward | smoke_reward_identity_unit | 8 | "清图批尾身份 = clear_blast(clear_default 唯一活行)" | inject | reward_economy.gd:342 op ==→!= / reward_economy.gd:343 op ==→!= [if row.reward_id == rid: → if r… | 验红 | 100ms | 1 | +| board_gen_reward | smoke_reward_identity_unit | 9 | "清图奖励 power ∈ 表区间 [%d, %d](roll 值 %d——配置推导)" % [cb_range[0], cb_range[1], int(cs1.power)] | inject | reward_economy.gd:342 op ==→!= / reward_economy.gd:343 op ==→!= [if row.reward_id == rid: → if r… | 验红 | 101ms | 1 | +| board_gen_reward | smoke_reward_identity_unit | 10 | "grant_clear 同批次重调幂等" | | | 异常 | — | — | +| board_gen_reward | smoke_reward_identity_unit | 11 | "映射退役后 grant 拒绝(生命周期闭合)" | inject | reward_economy.gd:133 return 取反 / reward_economy.gd:134 return 取反 [return null → return not (nul… | 验红 | 100ms | 0 | +| board_gen_reward | smoke_board_generator | 12 | "BoardGen 行解析 (%d,0)" % L | | | 异常 | — | — | +| board_gen_reward | smoke_board_generator | 13 | "R1 ceil 解析 (2,3)→(2,4)(向上取整偏难)" | | | 异常 | — | — | +| board_gen_reward | smoke_board_generator | 14 | "L%d 生成产物非空" % L | inject | board_generator.gd:206 key 错位:"inferability" / board_generator.gd:207 key 错位:"inferability" ["in… | 验红 | 85ms | 0 | +| board_gen_reward | smoke_board_generator | 15 | "L%d clearable(安全格 8 邻连通)" % L | inject | board_generator.gd:268 if 去not / board_generator.gd:269 if 去not [if not safe_set.has(c): → if sa… | 验红 | 1013ms | 4 | +| board_gen_reward | smoke_board_generator | 16 | "L%d legality 全过" % L | inject | board_generator.gd:282 return 取反 / board_generator.gd:283 return 取反 [return reasons.is_empty() →… | 验红 | 509ms | 2 | +| board_gen_reward | smoke_board_generator | 17 | "L%d inferability %.3f %s target %.2f(accept_mode=%s)" % [L, inf, "≥" if inf >= float(row.infera… | | | 异常 | — | — | +| board_gen_reward | smoke_board_generator | 18 | "L%d 生成器产物零变体格(变异布置归战斗初始化第 4 步——生成器管线不布置,2026-09-19 用户拍板棋盘设置顺序)" % L | | | 异常 | — | — | +| board_gen_reward | smoke_board_generator | 19 | "L%d 指标集输出降级标记与目标雷数(三分出口表)" % L | inject | board_generator.gd:341 key 错位:"target_mines" / board_generator.gd:342 key 错位:"target_mines" ["ta… | 验红 | 67ms | 0 | +| board_gen_reward | smoke_board_generator | 20 | "L%d 降级产物标记 = (accept_mode==accept_best)(%s→%s)" % [L, mode, str(b1.degraded_product)] | inject | board_generator.gd:353 op ==→!= / board_generator.gd:354 op ==→!= [b.degraded_product = accept_m… | 验红 | 1237ms | 0 | +| board_gen_reward | smoke_board_generator | 21 | "L%d 非法产物标记 = (accept_mode==accept_worst)(%s→%s)" % [L, mode, str(b1.illegal_product)] | inject | board_generator.gd:352 op ==→!= / board_generator.gd:353 op ==→!= [b.illegal_product = accept_mo… | 验红 | 1141ms | 2 | +| board_gen_reward | smoke_board_generator | 22 | "L%d 降级/非法产物标记互斥(两出口不可同真)" % L | | | 异常 | — | — | +| board_gen_reward | smoke_board_generator | 23 | "L%d 产物断言:实际雷数 %d ≤ 目标雷数 N=%d(先布后删只减不增)" % [L, int(b1.metrics.actual_mines), int(b1.metrics.targ… | | | 异常 | — | — | +| board_gen_reward | smoke_board_generator | 24 | "L%d attempts_used ∈ [1,%d]" % [L, int(row.max_gen_attempts)] | inject | board_generator.gd:343 key 错位:"attempts_used" / board_generator.gd:344 key 错位:"attempts_used" ["… | 验红 | 1149ms | 0 | +| board_gen_reward | smoke_board_generator | 25 | "L%d 非法产物标记 = false" % L | inject | board_generator.gd:282 return 置假 / board_generator.gd:283 return 置假 [return reasons.is_empty() →… | 验红 | 1123ms | 2 | +| board_gen_reward | smoke_board_generator | 26 | "L%d 同种子两次生成逐格相同" % L | | | 异常 | — | — | +| board_gen_reward | smoke_board_generator | 27 | "L%d 异种子产物不同(退化护栏)" % L | | | 异常 | — | — | +| board_gen_reward | smoke_board_generator | 28 | "L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 9——板面 30%% 锚)" % [L, cap_val] | | | 异常 | — | — | +| board_gen_reward | smoke_board_generator | 29 | "L%d 结构断言 R₀=%d ≤ cap=%d(R₀≤cap 重掷闸承载)" % [L, int(b1.metrics.r0_count), cap_val] | inject | board_generator.gd:282 return 取反 / board_generator.gd:283 return 取反 [return reasons.is_empty() →… | 验红 | 511ms | 2 | +| board_gen_reward | smoke_board_generator | 30 | "L%d r0_cap_rejected 尝试事实指标在位(本图 %d 次触发)" % [L, int(b1.metrics.get("r0_cap_rejected", -1))] | inject | board_generator.gd:350 key 错位:"r0_cap_rejected" / board_generator.gd:351 key 错位:"r0_cap_rejected… | 验红 | 1278ms | 0 | +| board_gen_reward | smoke_board_generator | 31 | "L%d ring_removed=%d ≤ 剃雷域候选危险格数((3+2r)²−9——全域剃雷退役)" % [L, int(b1.metrics.get("ring_removed", 99… | inject | board_generator.gd:347 key 错位:"ring_removed" / board_generator.gd:348 key 错位:"ring_removed" ["ri… | 验红 | 1220ms | 0 | +| board_gen_reward | smoke_board_generator | 32 | "L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" % [int(b9.metrics.r0_count), "… | inject | board_generator.gd:282 return 取反 / board_generator.gd:283 return 取反 [return reasons.is_empty() →… | 验红 | 490ms | 2 | +| board_gen_reward | smoke_board_generator | 33 | "L1 cap 压线出图指标集完整(accept_mode=%s / 闸触发 %d 次)" % [String(b9.metrics.get("accept_mode", "")), int(… | inject | board_generator.gd:344 key 错位:"accept_mode" / board_generator.gd:345 key 错位:"accept_mode" ["acce… | 验红 | 1292ms | 2 | +| board_gen_reward | smoke_power_interval | 34 | "区间行配对齐全且 power = floor 中点(配置推导 %d 行:%s)" % [interval_rows.size(), str(interval_rows.keys())] | table | reward.rows[3].power:-1 table_set / 表行改写 reward.rows[3].power: 2 → 1 | 验红 | 8ms | 0 | +| board_gen_reward | smoke_power_interval | 35 | "24 板全区间行快照 power ∈ [power_min, power_max](含清图槽)" | table | reward.rows[3].power_max:-1 table_set / 表行改写 reward.rows[3].power_max: 2 → 1 | 验红 | 8ms | 1 | +| board_gen_reward | smoke_power_interval | 36 | "定值行快照 power 恒等表值——区间批零外溢" | inject | reward_economy.gd:125 num 1→0 / reward_economy.gd:126 num 1→0 [return _snapshot(_row_by_id(rid),… | 验红 | 8ms | 0 | +| board_gen_reward | smoke_power_interval | 37 | "区间行上下阶实际流动(≥2 样本的区间行须 ≥2 个不同 roll 值)" | | | 异常 | — | — | +| board_gen_reward | smoke_power_interval | 38 | "定值行(lo=hi)roll 恒等表值" | | | 异常 | — | — | +| roll_multiplier | smoke_enemy_attack_type | 0 | "敌方 %d 行 attack_type 全填 normal(迭代 24 N3 轴开列,越轨行%s)" % [ _db.enemy.rows.size(), at_detail if at_d… | table | enemy.rows[0].attack_type:-1 table_set / 表行改写 enemy.rows[0].attack_type: normal → normal_x | 验红 | 185ms | 0 | +| roll_multiplier | smoke_enemy_attack_type | 1 | "敌方单位快照 attack_type 非空(spawn 注入面——迭代 24 N3)" | | | 异常 | — | — | +| roll_multiplier | smoke_enemy_attack_type | 2 | "克制矩阵命中证词:同种子三甲型实测 heavy %d == light %d − 1 · ward %d == light %d + 1(迭代 24 N3 全局平移 −1/0/+1)" % … | table | enemy.rows[5].level_index:-1 table_set / 表行改写 enemy.rows[5].level_index: 2 → 3 | 验红 | 1673ms | 0 | +| roll_multiplier | smoke_enemy_attack_type | 3 | "敌方伤害技单据载荷 attack_type 非空(= %s——第 9 参传值面,迭代 24 N3)" % payload_at | table | enemy.rows[13].unit_count:-1 table_set / 表行改写 enemy.rows[13].unit_count: 1 → 0 | 验红 | 5850ms | 1 | +| roll_multiplier | smoke_armor_roll | 4 | "查档 AC%d 全掷点域分布(100/75/50/25 = %s)" % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])] | table | settlement.roll_tier_bounds:-1 table 删行 / 表删行 settlement.roll_tier_bounds[0] | 验红 | 1135ms | 1 | +| roll_multiplier | smoke_armor_roll | 5 | "AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" % ev_sum | table | settlement.roll_tier_bounds:-1 table 删行 / 表删行 settlement.roll_tier_bounds[0] | 验红 | 1131ms | 1 | +| roll_multiplier | smoke_armor_roll | 6 | "nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.settlement.crit_roll)] | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 19 | 验红 | 50384ms | 2 | +| roll_multiplier | smoke_armor_roll | 7 | "nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)" | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 19 | 验红 | 50180ms | 2 | +| roll_multiplier | smoke_armor_roll | 8 | "nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.hero… | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 19 | 验红 | 50107ms | 2 | +| roll_multiplier | smoke_armor_roll | 9 | "削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)" | | | 异常 | — | — | +| roll_multiplier | smoke_armor_roll | 10 | "削弱单层池读数 = 行值(enemy_output 池单层贡献——×2 叠满触底 ×0 不可达,免伤轮根治)" | table | settlement.effect_pool_map.weaken:-1 table_set / 表行改写 settlement.effect_pool_map.weaken: enemy_o… | 验红 | 1139ms | 0 | +| roll_multiplier | smoke_armor_roll | 11 | "AC11 攻击拍逐拍判定(×%d 拍全部落档域 {100,75,50,25})" % tiers.size() | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 0 | 验红 | 51284ms | 4 | +| roll_multiplier | smoke_armor_roll | 12 | "折扣档实际流动(AC11 实测档位 %s——≥2 档可见)" % str(tier_distinct.keys()) | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 0 | 验红 | 51319ms | 4 | +| roll_multiplier | smoke_armor_roll | 13 | "全战斗流零 v=0 伤害行(伤害下限全局生效)" | | | 异常 | — | — | +| roll_multiplier | smoke_armor_roll | 14 | "hero_attack/enemy_action 伤害行掷点读数+档位随行留档(P2 归因)" | | | 异常 | — | — | +| roll_multiplier | smoke_armor_roll | 15 | "棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)" | | | 异常 | — | — | +| roll_multiplier | smoke_armor_roll | 16 | "棋盘克制直伤贴底 → 下限 1(点数 2 vs 不利修正 −1)" | table | settlement.damage_floor:-1 table_set / 表改写 settlement.damage_floor: 1 → 2 | 验红 | 1152ms | 1 | +| roll_multiplier | smoke_armor_roll | 17 | "克制修正触底穿透 → 下限 1(点数 1 vs 修正 −3)" | table | settlement.damage_floor:-1 table_set / 表改写 settlement.damage_floor: 1 → 2 | 验红 | 1123ms | 1 | +| roll_multiplier | smoke_armor_roll | 18 | "空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)" | | | 异常 | — | — | +| roll_multiplier | smoke_pool_wiring | 19 | "单分量基线在位(normal 分量 = 落地值、基线 %d——充能退役批)" % base_v | | | 异常 | — | — | +| roll_multiplier | smoke_pool_wiring | 20 | "增伤经乘区加成合计(%d → %d = ×1.5——attack_up 直乘旁路退役,覆盖面=攻击合计)" % [base_v, int(up_row.get("value", -1))] | table | status_effects.rows[0].effect_id:-1 table 删键 / 表行删键 status_effects.rows[0].effect_id | 验红 | 194ms | 0 | +| roll_multiplier | smoke_pool_wiring | 21 | "乏力经乘区削减合计(%d → %d = ×0.75)" % [base_v, int(sapped_row.get("value", -1))] | table | status_effects.rows[5].effect_id:-1 table 删键 / 表行删键 status_effects.rows[5].effect_id | 验红 | 194ms | 0 | +| roll_multiplier | smoke_pool_wiring | 22 | "硬化折半方向(%d → %d = ×0.5 且 < 基线——敌方防御池负值减伤,翻倍 bug 退役)" % [base_v, int(fortify_row.get("value", -1)… | table | status_effects.rows[7].effect_id:-1 table 删键 / 表行删键 status_effects.rows[7].effect_id | 验红 | 203ms | 0 | +| roll_multiplier | smoke_damage_types | 23 | "goblin 护甲 = light(轻甲——教学位)" | table | enemy.rows[0].unit_count:-1 table_set / 表行改写 enemy.rows[0].unit_count: 1 → 0 | 验红 | 412ms | 0 | +| roll_multiplier | smoke_damage_types | 24 | "bugbear 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)" | table | enemy.rows[2].unit_count:-1 table_set / 表行改写 enemy.rows[2].unit_count: 1 → 0 | 验红 | 400ms | 0 | +| roll_multiplier | smoke_damage_types | 25 | "ogre_chief 护甲 = light(Boss 行集——护甲类型重配批 2026-09-18:关卡 1 全轻甲)" | table | enemy.rows[3].armor_type:-1 table_set / 表行改写 enemy.rows[3].armor_type: light → light_x | 验红 | 397ms | 0 | +| roll_multiplier | smoke_damage_types | 26 | "普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)" | | | 异常 | — | — | +| roll_multiplier | smoke_damage_types | 27 | "穿刺行:轻甲 −1 / 魔法护甲 +1 / 重甲 0 中性(用户拍板)" | | | 异常 | — | — | +| roll_multiplier | smoke_damage_types | 28 | "魔法行:轻甲 0 / 魔法护甲 −1 / 重甲 +1(加法核)" | | | 异常 | — | — | +| roll_multiplier | smoke_damage_types | 29 | "标错封印产生 seal_miss 归因行(×%d)" % miss_rows | table | enemy.rows[4].level_index:-1 table 删键 / 表行删键 enemy.rows[4].level_index | 验红 | 68ms | 2 | +| roll_multiplier | smoke_damage_types | 30 | "标错封印兑付该安全格资源的负向字典行(×%d——attack/defense/weaken 效果行或 heal 直伤行)" % negative_rows | table | enemy.rows[4].level_index:-1 table 删键 / 表行删键 enemy.rows[4].level_index | 验红 | 72ms | 2 | +| roll_multiplier | smoke_damage_types | 31 | "封印命中兑付该危险格资源的正向字典行(×%d——充能退役后无魔法分量,正向=格面资源)" % hit_rows | table | enemy.rows[4].level_index:-1 table 删键 / 表行删键 enemy.rows[4].level_index | 验红 | 67ms | 2 | +| roll_multiplier | smoke_damage_types | 32 | "标错惩罚轮回放断言:同种子步骤流逐行相同(类型化负向兑付确定性)" | | | 异常 | — | — | +| roll_multiplier | smoke_attack_chain_fresh_pool | 33 | "fresh 池回归战斗启动(Boss 行)" | inject | battle_orchestrator.gd:380 asgn 置null / battle_orchestrator.gd:381 asgn 置null [_ledger = ledger_… | 验红 | 249ms | 1 | +| roll_multiplier | smoke_attack_chain_fresh_pool | 34 | "非施放攻击拍采样充足(%d 拍——增益施放行零兑付为设计语义不计)" % ticks.size() | inject | battle_orchestrator.gd:304 key 错位:"[战斗结算] begin_battle " / battle_orchestrator.gd:305 key 错位:"[战… | 验红 | 324ms | 0 | +| roll_multiplier | smoke_attack_chain_fresh_pool | 35 | "fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线)(Boss 攻击链健康——Bug#2 回归断言)" | | | 异常 | — | — | +| seal_polarity | smoke_seal_ap_exhausted | 0 | "回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)" | state | battle_orchestrator.gd:43 num 0→1 / [状态·初值] battle_orchestrator.gd:44 num 0→1 [var round_index :… | 验红 | 113ms | 4 | +| seal_polarity | smoke_seal_ap_exhausted | 1 | "%d 次挖掘后恰剩 1 AP(phase=%s ap=%d)" % [ap_total - 1, str(settlement.action_economy.phase), settleme… | state | battle_orchestrator.gd:43 num 0→1 / [状态·初值] battle_orchestrator.gd:44 num 0→1 [var round_index :… | 验红 | 131ms | 4 | +| seal_polarity | smoke_seal_ap_exhausted | 2 | "仍存在可挖目标(封印目标来源)" | state | battle_orchestrator.gd:43 num 0→1 / [状态·初值] battle_orchestrator.gd:44 num 0→1 [var round_index :… | 验红 | 114ms | 4 | +| seal_polarity | smoke_seal_ap_exhausted | 3 | "末点 AP 封印被受理" | state | battle_orchestrator.gd:43 num 0→1 / [状态·初值] battle_orchestrator.gd:44 num 0→1 [var round_index :… | 验红 | 113ms | 4 | +| seal_polarity | smoke_seal_ap_exhausted | 4 | "封印动作耗尽 AP 触发轮末结算并进入第 2 轮(rounds=%s)" % str(rounds) | state | battle_orchestrator.gd:20 key 错位:"decay" / [状态·初值] battle_orchestrator.gd:21 key 错位:"decay" [con… | 验红 | 362ms | 0 | + +## 二、异常清单(打不红的条+原因) + +- system_test:smoke_scene_split #1「menu.start_overlay != null and menu.lvbtn != null and menu.resumebtn != null and menu.campbtn !=…」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_scene_split #2「menu.start_overlay.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形;断言表达式读 schema 常量(大写成员,宿主 preload 编译期绑定原件)——… +- system_test:smoke_scene_split #3「menu.mouse_filter == Control.MOUSE_FILTER_IGNORE」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形;断言表达式读 schema 常量(大写成员,宿主 preload 编译期绑定原件)——… +- system_test:smoke_scene_split #4「camp.camp_overlay != null」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_scene_split #5「not camp.camp_overlay.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_scene_split #6「adv.battle_root != null and adv.stage != null and adv.board_zone != null and adv.board_view != n…」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_boot_chain #12「db.is_ready and db.hero != null」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_boot_chain #13「AudioServer.is_bus_mute(master) == (ss.volume_linear <= 0.001) and is_equal_approx(AudioServer.g…」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_boot_chain #14「ui_kit.motion_scale == (0.5 if ss.reduced_motion else 1.0)」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_boot_chain #16「not sm.has_breakpoint() and not main._resume_btn.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_boot_chain #18「sm.keystone_balance() == 0 and not sm.has_breakpoint() and sm.adventure_total() == 0」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_boot_chain #20「not (FileAccess.file_exists("user://save.json") and not FileAccess.file_exists(sm.slot_path(1)))」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形;断言表达式含磁盘读取(FileAccess/DirAccess)——内存内改坏不可达,… +- system_test:smoke_boot_chain #21「not sm.has_breakpoint() and not main._resume_btn.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_boot_chain #23「not main._resume_btn.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_boot_chain #25「not main.debug_overlay.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_boot_chain #—「」——跑后基线未复绿——存在跨试跑状态残留,须人工排查 +- system_test:smoke_run_lifecycle #28「main.run_scope.hints_shown.is_empty()」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_run_lifecycle #29「main.run_scope.adventure.abandon()」——复验未红(改坏候选 1 个均不复现——疑似非确定断言) +- system_test:smoke_run_lifecycle #35「not sm.has_breakpoint()」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_lifecycle_matrix #39「main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live()」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_lifecycle_matrix #40「main.run_scope.adventure.abandon()」——复验未红(改坏候选 1 个均不复现——疑似非确定断言) +- system_test:smoke_lifecycle_matrix #41「finished.done and main._end_overlay.is_open()」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_lifecycle_matrix #44「false」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_lifecycle_matrix #45「main.run_scope.adventure != null and main.run_scope.adventure != first_adv」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_lifecycle_matrix #46「main.run_scope.adventure.abandon()」——复验未红(改坏候选 1 个均不复现——疑似非确定断言) +- table_structure_guard:smoke_ac_swap_guard #25「"hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [ wall_detail if wall_deta…」——预算内未找到能翻红的改坏(试跑 1608 轮,武器=table, inject)——死断言候选或改坏面缺形 +- table_structure_guard:smoke_enemy_bands #19「"hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕"」——预算内未找到能翻红的改坏(试跑 708 轮,武器=table, inject)——死断言候选或改坏面缺形;断言表达式含磁盘读取(FileAccess/DirAccess)——内存内改坏不可达,… +- table_structure_guard:smoke_nine_types_gp_stream #50「"格面资源十型封闭(第九型 gp 九型批 2026-09-20 / 第十型 treasure_item 秘宝内容批 2026-09-29——词表 v1.2)"」——预算内未找到能翻红的改坏(试跑 639 轮,武器=table, inject)——死断言候选或改坏面缺形;断言表达式读 schema 常量(大写成员,宿主 preload 编译期绑定原件)——… +- table_structure_guard:smoke_adventure_table #0「"db.adventure 已加载(adventure.tres 入 manifest)"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形 +- table_structure_guard:smoke_adventure_table #6「"恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形 +- table_structure_guard:smoke_adventure_table #7「"cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形 +- table_structure_guard:smoke_adventure_table #8「"同调位读数源=树同调位节点点亮数(迭代 21 T4 槽位收编——null 档求值 0;谓词表已退役)〔天赋树批 V14〕"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形 +- table_structure_guard:smoke_adventure_table #14「"新 12 行效果词零棋盘改写类(危险格/安全格分布同池冻结——真视/开格动作外无写面)〔秘宝批 V9〕"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形 +- table_structure_guard:smoke_adventure_table #15「"效果三方向覆盖在册(信息/行动资源/操作能力=plan·抉择刷新——12 件目录结构对齐设计权威)〔秘宝批 V10 覆盖半〕"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形 +- board_gen_reward:smoke_reward_identity_unit #6「"同格重复 grant 幂等(同一快照——受理恰一次)"」——预算内未找到能翻红的改坏(试跑 339 轮,武器=inject, state)——死断言候选或改坏面缺形;标签含幂等语义——确定性快照的内容等值在单行确定性变异下难破,疑似结构性打不红,须人工… +- board_gen_reward:smoke_reward_identity_unit #10「"grant_clear 同批次重调幂等"」——预算内未找到能翻红的改坏(试跑 339 轮,武器=inject, state)——死断言候选或改坏面缺形;标签含幂等语义——确定性快照的内容等值在单行确定性变异下难破,疑似结构性打不红,须人工… +- board_gen_reward:smoke_board_generator #12「"BoardGen 行解析 (%d,0)" % L」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形 +- board_gen_reward:smoke_board_generator #13「"R1 ceil 解析 (2,3)→(2,4)(向上取整偏难)"」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形 +- board_gen_reward:smoke_board_generator #17「"L%d inferability %.3f %s target %.2f(accept_mode=%s)" % [L, inf, "≥" if inf >= float(row.infera…」——复验未红(改坏候选 1 个均不复现——疑似非确定断言) +- board_gen_reward:smoke_board_generator #18「"L%d 生成器产物零变体格(变异布置归战斗初始化第 4 步——生成器管线不布置,2026-09-19 用户拍板棋盘设置顺序)" % L」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形 +- board_gen_reward:smoke_board_generator #22「"L%d 降级/非法产物标记互斥(两出口不可同真)" % L」——复验未红(改坏候选 1 个均不复现——疑似非确定断言) +- board_gen_reward:smoke_board_generator #23「"L%d 产物断言:实际雷数 %d ≤ 目标雷数 N=%d(先布后删只减不增)" % [L, int(b1.metrics.actual_mines), int(b1.metrics.targ…」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形 +- board_gen_reward:smoke_board_generator #26「"L%d 同种子两次生成逐格相同" % L」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形 +- board_gen_reward:smoke_board_generator #27「"L%d 异种子产物不同(退化护栏)" % L」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形 +- board_gen_reward:smoke_board_generator #28「"L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 9——板面 30%% 锚)" % [L, cap_val]」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形;断言表达式读 schema 常量(大写成员,宿主 preload 编译期绑定原件)——路… +- board_gen_reward:smoke_power_interval #37「"区间行上下阶实际流动(≥2 样本的区间行须 ≥2 个不同 roll 值)"」——预算内未找到能翻红的改坏(试跑 596 轮,武器=table, inject)——死断言候选或改坏面缺形 +- board_gen_reward:smoke_power_interval #38「"定值行(lo=hi)roll 恒等表值"」——预算内未找到能翻红的改坏(试跑 596 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_enemy_attack_type #1「"敌方单位快照 attack_type 非空(spawn 注入面——迭代 24 N3)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_armor_roll #9「"削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_armor_roll #13「"全战斗流零 v=0 伤害行(伤害下限全局生效)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_armor_roll #14「"hero_attack/enemy_action 伤害行掷点读数+档位随行留档(P2 归因)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_armor_roll #15「"棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_armor_roll #18「"空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_pool_wiring #19「"单分量基线在位(normal 分量 = 落地值、基线 %d——充能退役批)" % base_v」——预算内未找到能翻红的改坏(试跑 500 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_damage_types #26「"普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)"」——预算内未找到能翻红的改坏(试跑 500 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_damage_types #27「"穿刺行:轻甲 −1 / 魔法护甲 +1 / 重甲 0 中性(用户拍板)"」——预算内未找到能翻红的改坏(试跑 500 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_damage_types #28「"魔法行:轻甲 0 / 魔法护甲 −1 / 重甲 +1(加法核)"」——预算内未找到能翻红的改坏(试跑 500 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_damage_types #32「"标错惩罚轮回放断言:同种子步骤流逐行相同(类型化负向兑付确定性)"」——预算内未找到能翻红的改坏(试跑 500 轮,武器=table, inject)——死断言候选或改坏面缺形 +- roll_multiplier:smoke_attack_chain_fresh_pool #35「"fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线)(Boss 攻击链健康——Bug#2 回归断言)"」——预算内未找到能翻红的改坏(试跑 500 轮,武器=inject, state)——死断言候选或改坏面缺形 +- system_test:smoke_boot_chain——跑后基线未复绿(存在跨试跑状态残留) + +## 三、汇总 + +- system_test:split_tail——1 条(验红 1 / 异常 0)· 改坏试跑 1 轮 · 跑后基线复绿 +- system_test:smoke_scene_split——11 条(验红 5 / 异常 6)· 改坏试跑 600 轮 · 跑后基线复绿 +- system_test:smoke_boot_chain——15 条(验红 6 / 异常 9)· 改坏试跑 600 轮 · 跑后基线**未复绿** +- system_test:smoke_run_lifecycle——11 条(验红 8 / 异常 3)· 改坏试跑 600 轮 · 跑后基线复绿 +- system_test:smoke_lifecycle_matrix——10 条(验红 4 / 异常 6)· 改坏试跑 600 轮 · 跑后基线复绿 +- system_test:stage_faces——1 条(验红 1 / 异常 0)· 改坏试跑 10 轮 · 跑后基线复绿 +- table_structure_guard:smoke_ac_swap_guard——12 条(验红 11 / 异常 1)· 改坏试跑 1608 轮 · 跑后基线复绿 +- table_structure_guard:smoke_enemy_bands——4 条(验红 3 / 异常 1)· 改坏试跑 708 轮 · 跑后基线复绿 +- table_structure_guard:smoke_variation_framework——9 条(验红 9 / 异常 0)· 改坏试跑 103 轮 · 跑后基线复绿 +- table_structure_guard:smoke_difficulty_table——8 条(验红 8 / 异常 0)· 改坏试跑 131 轮 · 跑后基线复绿 +- table_structure_guard:smoke_nine_types_gp_stream——6 条(验红 5 / 异常 1)· 改坏试跑 639 轮 · 跑后基线复绿 +- table_structure_guard:smoke_adventure_table——17 条(验红 11 / 异常 6)· 改坏试跑 3000 轮 · 跑后基线复绿 +- table_structure_guard:smoke_meta——4 条(验红 4 / 异常 0)· 改坏试跑 41 轮 · 跑后基线复绿 +- board_gen_reward:smoke_layout_cell_formula——2 条(验红 2 / 异常 0)· 改坏试跑 14 轮 · 跑后基线复绿 +- board_gen_reward:smoke_reward_identity_unit——10 条(验红 8 / 异常 2)· 改坏试跑 339 轮 · 跑后基线复绿 +- board_gen_reward:smoke_board_generator——22 条(验红 13 / 异常 9)· 改坏试跑 60 轮 · 跑后基线复绿 +- board_gen_reward:smoke_power_interval——5 条(验红 3 / 异常 2)· 改坏试跑 596 轮 · 跑后基线复绿 +- roll_multiplier:smoke_enemy_attack_type——4 条(验红 3 / 异常 1)· 改坏试跑 600 轮 · 跑后基线复绿 +- roll_multiplier:smoke_armor_roll——15 条(验红 10 / 异常 5)· 改坏试跑 600 轮 · 跑后基线复绿 +- roll_multiplier:smoke_pool_wiring——4 条(验红 3 / 异常 1)· 改坏试跑 500 轮 · 跑后基线复绿 +- roll_multiplier:smoke_damage_types——10 条(验红 6 / 异常 4)· 改坏试跑 500 轮 · 跑后基线复绿 +- roll_multiplier:smoke_attack_chain_fresh_pool——3 条(验红 2 / 异常 1)· 改坏试跑 500 轮 · 跑后基线复绿 +- seal_polarity:smoke_seal_ap_exhausted——5 条(验红 5 / 异常 0)· 改坏试跑 9 轮 · 跑后基线复绿 + +合计 189 条:验红 131 / 异常 58。