Merge branch 'master' into iter41

# Conflicts:
#	game/tests/unit/battle_settlement/roll_multiplier_test.gd
#	game/tests/unit/core/adventure_feats_test.gd
#	game/tests/unit/core/config_db_resolver_test.gd
#	game/tests/unit/core/level_shop_economy_test.gd
#	game/tests/unit/core/save_boot_no_activate_test.gd
This commit is contained in:
DKChan committed 2026-10-28 23:46:10 +08:00
commit 38c9a0eba6
46 files changed
+2033 -1979

No files matched your search

+1 -1
View File
@@ -63,7 +63,7 @@
> **样张 v2 记录(2026-09-28,四冻结生效)**:机检 16/16 全绿(扩展断言集:A3 图底分离 Δ≥55/F6 墙格顶面立面双明度带 Δ≥18/F7 整板悬崖 DEPTH_PX 与游戏渲染常量同源/A6 观感密度梯度均值 ≥4.70=v1 实测 4.09×1.15)。**四冻结**——①砖面形态:方案甲 21 瓦片+高墙感 D 投影档+整板悬崖(§3.2 高墙瓦片体系条定稿参数);②色板:小人蓝 #2E86DE/接触带微光 #D6ECFF/类型微光五组定稿(§4.1/§4.2);③动画方案:§8.4 帧数档冻结;④数字形制:色牌形制定稿(#F5EAD3 素色直角小牌+Fusion Pixel+字号 max(18, ceil(格边×0.5))+墨三档,§7.4)。产物=`artwork/mockups/sample-batch-v2-2026-09-28.png`,机检报告=git 提交 f9b0765;气质终审=批末用户目检(闸形 A,2026-09-27 拍板)。**(v3.0 注:四冻结之①砖面形态的数字承载面与④数字形制随色牌降级解冻,待样张 v3 重冻;②色板③动画维持冻结不变)**
> **样张 v3 冻结记录(2026-09-30,迭代 9 T1 批 A——四冻结①④重冻生效,圣经升 v3.1)**:机检 17/17 全绿,断言定值两件落锁——A3b 图底分离**定值 Δ≥30**(方向稿实测 38;v2 阈值 55 系色牌制地板 L0.036 时代校定,与格底分档带数学冲突退役)/A4 尺寸基线**硬等冻结**(立绘 46×48+棋盘小人源 32×32;资产升档批换代须显式解冻重校);F6 沿用 v3.4 双明度带口径 Δ≥18。**重冻两件**——①砖面形态:E 格高墙 30% 承接词表扩展(南邻墙地面格南部 30% 承接墙面、墙顶纹理**原亮度**直贴零暗档零阴影、(b)-(c) 连接处无边框〔渲染格线跳段〕、连续墙体内侧不出立面;南向厚度面+v3.4 暗档 D 投影保留)+**格底明度分档**(已开格底=世界底层档 L≈0.10,候选带 0.08–0.14;落码程序归一,地板素材重产归资产批)+装饰物件级解禁(专属形状家族,零信息纪律不变)/④数字形制:**亮墨三档直画**(1–3 冷亮 #D8ECF0/4–5 暖白 #F0E2C8/6–8 金亮 #F8E3A8,直画已开格底,实测对比 6.0–6.3:1)+**色牌后备**(#F5EAD3 仅复杂格底——物件纹样/装饰变体格——垫牌兜底,牌上深墨族 12:1)。②色板③动画维持 v2 冻结不变。产物=`artwork/mockups/sample-batch-v3-final-2026-09-30.png`,机检报告=git 提交 7a847c7【冻结】;气质终审=2026-09-29 用户验收豁免(新 v3.5 方向稿验收通过,状态记录 281)。
> **样张 v3 冻结记录(2026-09-30,迭代 9 T1 批 A——四冻结①④重冻生效,圣经升 v3.1)**:机检 17/17 全绿,断言定值两件落锁——A3b 图底分离**定值 Δ≥30**(方向稿实测 38;v2 阈值 55 系色牌制地板 L0.036 时代校定,与格底分档带数学冲突退役)/A4 尺寸基线**硬等冻结**(立绘 46×48+棋盘小人源 32×32;资产升档批换代须显式解冻重校);F6 沿用 v3.4 双明度带口径 Δ≥18。**重冻两件**——①砖面形态:E 格高墙 30% 承接词表扩展(南邻墙地面格南部 30% 承接墙面、墙顶纹理**原亮度**直贴零暗档零阴影、(b)-(c) 连接处无边框〔渲染格线跳段〕、连续墙体内侧不出立面;南向厚度面+v3.4 暗档 D 投影保留)+**格底明度分档**(已开格底=世界底层档 L≈0.10,候选带 0.08–0.14;落码程序归一,地板素材重产归资产批)+装饰物件级解禁(专属形状家族,零信息纪律不变)/④数字形制:**亮墨三档直画**(1–3 冷亮 #D8ECF0/4–5 暖白 #F0E2C8/6–8 金亮 #F8E3A8,直画已开格底,实测对比 6.0–6.3:1)+**色牌后备**(#F5EAD3 仅复杂格底——物件纹样/装饰变体格——垫牌兜底,牌上深墨族 12:1)。②色板③动画维持 v2 冻结不变。产物原件已丢失(未入 git、主工作区不可查),机检报告=git 提交 7a847c7【冻结】;气质终审=2026-09-29 用户验收豁免(新 v3.5 方向稿验收通过,状态记录 281)。
---
+1 -1
View File
@@ -10,7 +10,7 @@
| 素材 | 状态 | 需求文档 |
|---|---|---|
| 格子样张 | ✅ 样张 v3 冻结(2026-09-30 机检 17/17 全绿,art-bible §1.4 v3.2 记录;v2/v3 两次冻结通过;产物 `artwork/mockups/sample-batch-v3-final-2026-09-30.png`) | [art-bible](art-bible.md) §1.4 |
| 格子样张 | ✅ 样张 v3 冻结(2026-09-30 机检 17/17 全绿,art-bible §1.4 v3.2 记录;v2/v3 两次冻结通过;产物原件已丢失、未入 git) | [art-bible](art-bible.md) §1.4 |
| 战斗一屏样张 | ⬜ | [战斗表现](../gdd/systems/战斗循环/战斗表现.md)(一屏样张验收标准)+ [art-bible](art-bible.md) §5.1 |
## P1 角色级(样张通过后开工)
+1 -1
View File
@@ -54,7 +54,7 @@
11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter<N>-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter<N>-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注
12. **实机目检归用户,只登记不阻塞**——真人检测项登记进实现报告目检总清单与体验验证清单即完成批内义务;禁把等目检当停工理由或完成前置
13. **中间产物吸收即死**——细则见 AGENTS.md 硬性红线与 doc-conventions 第 11 节
14. **改动级验收闭合**——写卡期列需求侧对照表,实现期补实际对照表(测试文件:行),每个行为改动做撤销证明;合回前独立验收审计。细则=test-conventions §8
14. **改动级验收闭合**——写卡期列需求侧对照表,实现期补实际对照表(测试文件:行),每个行为改动的验收断言从真实入口进入、在当轮真实跑批中执行并通过(判据绑定面口径=test-conventions §8.4;禁变异类测试与撤销类取证,2026-10-24/28 用户两裁);合回前独立验收审计。细则=test-conventions §8
## 4. 测试执行两入口铁规(细则)
+1 -1
View File
@@ -15,7 +15,7 @@
| `术语表.md` | 全库专用术语的唯一对照表——危险格/安全格/同调位/RAP/GP/真视等定义+退役词对照(2026-10-11 建立,v1.23) |
| `待办清单.md`(仓库根,不属本树) | 全项目待办事项的唯一登记处;各系统文档的「待办」节只留指路存根 |
| `体验验证清单.md`(仓库根,不属本树) | 需要真人试玩观测的验证项登记处(与待办清单分工:待办清单是"要拍板的事",本篇是"要玩过才知道的事");每条注明结论回流路径 |
| `08-Alpha版本标准.md` | Alpha 版本包含哪些东西的清单——里程碑 M3 的完成标准;只列条目不写内容,定义一律指路各权威文档(**已定稿 v1.17**,2026-10-13) |
| `08-Alpha版本标准.md` | Alpha 版本包含哪些东西的清单——里程碑 M3 的完成标准;只列条目不写内容,定义一律指路各权威文档(**已定稿 v1.19**,2026-10-13) |
## systems/ 五个循环
+2 -2
View File
@@ -2,7 +2,7 @@
> **一句话定位**:本文定义这个项目的 **Alpha 版本包含哪些东西**——它是里程碑 M3 的完成标准,也是判断"还差什么"的对照表。
> **本文只列条目,不写内容**:每样东西是什么、规则怎么走,一律归各自的权威文档,本文只给指路链接。想知道某样东西的定义,请点链接,不要在本文里找解释。
> **状态**:**已定稿(v1.18)**——2026-10-13 用户令定稿,历次升版明细见 git 提交史。Alpha 的内容范围自此冻结:增删条目一律须经用户裁决。
> **状态**:**已定稿(v1.19)**——2026-10-13 用户令定稿,历次升版明细见 git 提交史。Alpha 的内容范围自此冻结:增删条目一律须经用户裁决。
> **成文**:2026-10-13。
## 本文怎么用
@@ -48,7 +48,7 @@ Alpha 内以玩法循环本身的内容与表现为主(棋盘、关卡、战
| 冒险结构 | [冒险结构](systems/冒险循环/冒险结构.md) | 在役 |
| 关卡节点地图 | [关卡节点地图](systems/冒险循环/关卡节点地图.md) | 在役(信息迷雾类型可见性已随迭代 34 卡④ 实装,2026-10-22) |
| 关卡节点图重构(增加可选节点) | [关卡节点地图](systems/冒险循环/关卡节点地图.md)+[关卡节点图生成](systems/冒险循环/关卡节点图生成.md) | 在役(2026-10-25 迭代 35 卡①② 合回:图源切为参数档×种子生成图、模板链路退役;真人目检选路项已登记体验验证清单,登记不阻塞) |
| 关卡节点图生成 | [关卡节点图生成](systems/冒险循环/关卡节点图生成.md)(参数化长图:产出规格与生成参数) | 在役(2026-10-24 迭代 35 卡① 生成器核心合回;enemy.tres 与 board_gen.tres 已覆盖到第三关深度 13) |
| 关卡节点图生成 | [关卡节点图生成](systems/冒险循环/关卡节点图生成.md)(参数化长图:产出规格与生成参数) | 在役(2026-10-24 迭代 35 卡① 生成器核心合回;enemy.tres 与 board_gen.tres 已覆盖到第三关深度 13;生成图 L3 深度域 0–13 超出关卡地图屏画布屏预算约 10 层——等比缩放/滚动视口两升级路径未设计待裁,[待办清单](../../待办清单.md)「超预算图策略升级路径」条在册) |
| 难度曲线与 Boss | [难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md) | 在役 |
| 奇物 | [奇物](systems/冒险循环/奇物.md) | 在役 |
+1 -1
View File
@@ -62,7 +62,7 @@ func _phase_text(p: int) -> String:
func _cost(kind: String) -> int:
var ae = db.action_economy
var ae = db.run.action_economy
match kind:
"dig":
return ae.dig_ap_cost # 基础挖格成本恒表值(钻头对价挂付费开掘侧)
+1 -1
View File
@@ -101,7 +101,7 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
## 特性订阅装载域(feats 表序→按持有集过滤→行内数组序——装载声明序第一段)
func _load_feat_triggers(feat_ids: Array) -> void:
if db != null and db.feats != null:
if db != null and db.crossrun.feats != null:
for row in db.crossrun.feats.rows:
var fid := String(row.get("feat_id", ""))
if not feat_ids.has(fid):
+2 -2
View File
@@ -97,7 +97,7 @@ static func _refresh_economy(ap_bar: ApBarView, s: Dictionary, db: ConfigDb, ref
# 无 archetype 字段,聚合形态会空键兜底实显「流派 ×N · 档一」僵尸文案;流派档位非现行机制)
static func feat_holdings_lines(db: ConfigDb, held: Array) -> Array[String]:
var rlines: Array[String] = []
if db == null or db.feats == null or (db.crossrun.feats.rows as Array).is_empty() or held.is_empty():
if db == null or db.crossrun.feats == null or (db.crossrun.feats.rows as Array).is_empty() or held.is_empty():
return rlines
var counts := {}
for rid in held:
@@ -114,7 +114,7 @@ static func feat_holdings_lines(db: ConfigDb, held: Array) -> Array[String]:
# 文案用常规表述,不用俚语
static func board_variant_lines(db: ConfigDb, ids: Array) -> Array[String]:
var lines: Array[String] = []
if db == null or db.variation == null:
if db == null or db.combat.variation == null:
return lines
for vid in ids:
var row: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(String(vid))
+1 -1
View File
@@ -360,7 +360,7 @@ func _wire_adventure() -> void:
# meta.attune_refresh_charges(当前等级行表值)、树 rule 键集=meta.tree_rule_keys(Fork 激活侧 effects);
# hero_override 沿用既有 meta 现算口径。static 形态=冒烟直呼单测本函数,免实例化整菜单场景)。
# 两根类型轴随开局下传(attack_type/armor_type——契约 2 所有权移交+契约 8 冒险开始快照;
# 取值=meta.hero_attack_type/hero_armor_type=存档选中路线类型轴,未解锁/异常选中态回退锻造配置默认值)。
# 取值=meta.hero_type_axes()=存档选中路线类型轴(attack/armor 两键),未解锁/异常选中态回退锻造配置默认值)。
# 注:runner 侧 resume_adventure 受理面收 attunement/attune_refresh_y/tree_rules 三键
# (传参/受理两侧对齐)。
static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: MetaProgressionCore, extra: Dictionary = {}) -> Dictionary:
+4 -3
View File
@@ -70,7 +70,7 @@ tests/
|---|---|---|---|
| `tools/run_smoke.py`(冒烟测试) | 这版能否启动、能否加载表、能否进主屏——零逻辑判定 | 退出码+脚本错误扫描 | 开机路径/表加载改动后的快速自检;收口由闸编排 |
| `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 |
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) |
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) |
| `tools/probe_gate.sh`(场景探针,`--click`/`--full` 两模式) | UI 可达性、可见性、状态——真实实例化场景+真实输入路径 | 探针自检+退出码 | 新增 UI 区域的配套探针直跑与断言复核 |
| `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL) | 主题/素材/排版类改动;窗口化运行,需开显示器 |
| `game/tests/static_scan.sh`(S 域) | 项目纪律+第三方质量规则:RNG 单点/core 纯度/类型标注/文件头/注释禁词/旧符号残留;S13=gdstyle 质量规则(重复 load、循环内 await、循环内分配、不可达代码等真缺陷类;基线键=文件|规则|函数名(类/文件级=文件|规则,无数字条目带消息),数字漂移不假红,同键指标恶化超基线登记值即红) | 退出码(逐规则 FAIL 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 |
@@ -96,8 +96,9 @@ python tools/run_system_test.py
# 场景探针(UI 探针层)——以探针场景为运行目标,走完整项目引导(autoload 在位)
godot --headless --path . res://probe/<probe>.tscn
# 本地闸必须配外部 wall-clock 超时(引擎无死锁探测;run_all_gates.sh 已内置)
timeout 120s godot --headless --script tests/run_all.gd
# 挂死防护内建于 run_all.gd(引擎无死锁探测,超时靠外部判):每套件子进程带截止时间——
# 默认 120s,环境变量 RUN_ALL_SUITE_TIMEOUT_SEC 可覆盖;超时先 taskkill /T 整树回收
#(cmd wrapper 连引擎孙进程一起杀,不留孤儿),该套件判 FAIL、批继续——单套件挂死不拖死整闸
```
> 套件 manifest 现况:`_suites()` 登记 **64 条**(61 单元+3 集成,2026-10-28 实数;口径=manifest 条目数,
@@ -73,8 +73,8 @@ func _feats_breakpoint_roundtrip() -> void:
{"feat_id": "x5", "display_name": "试五", "rarity": "legendary", "desc": "测试AC", "cap": 1,
"triggers": [{"on": "seal_hit", "do": {"kind": "ac_bonus", "value": 1, "mode": "round"}}]},
]
var live_rows: FeatTable = _db.feats
_db.feats = rt
var live_rows: FeatTable = _db.crossrun.feats
_db.crossrun.feats = rt
var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72))
a_r.start_adventure(72) # 源 :723(受理断言留单元宿主)
a_r.stop()._pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
@@ -98,7 +98,7 @@ func _feats_breakpoint_roundtrip() -> void:
sm_r.clear_breakpoint()
if FileAccess.file_exists(bp_path): # 段尾清理(与段首清场对称)
DirAccess.remove_absolute(bp_path)
_db.feats = live_rows # 还原现行表(共享 _db 防污染——后续集成块零感知)
_db.crossrun.feats = live_rows # 还原现行表(共享 _db 防污染——后续集成块零感知)
# ---- ③ 存档摘要集成(源 `_smoke_save` ⑨ 基点 :6807–6831 十条,主会话裁决三改判集成——
+69 -5
View File
@@ -1,12 +1,16 @@
# tests/run_all.gd — 单元测试统一入口(双载体的逻辑测试调用点,套件 manifest 协议 D2 AC-2b)
# 用法:godot --headless --path game --script res://tests/run_all.gd (本地闸 02 闸包装外部超时:timeout 120s …)
# 用法:godot --headless --path game --script res://tests/run_all.gd
# 选跑态(开发期直跑专用):追加 `-- --only=<词[,词…]>` ——对套件 res:// 路径做子串匹配,只跑命中套件;
# 零命中/空值=用法错 quit(2)。选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据;
# 批末全量闸不带参数不经选跑分支——默认(无参数)行为逐字节不变。
# 批末全量闸不带参数不经选跑分支。
# 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。
# 执行形态:逐套件起独立 Godot 子进程实跑
#(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符,
# 每个测试文件自带显式实例化夹具,见 tests/README 协议要点)。
# 挂死守护:每套件子进程带截止时间(默认 120s,环境变量 RUN_ALL_SUITE_TIMEOUT_SEC 可覆盖)。
# 套件挂死(_init 在 quit() 前中断,子进程永不退出)=超时先 taskkill /T 整树回收
#(连 cmd wrapper 带引擎孙进程一起杀,只杀 wrapper 会留孤儿殃及后续全部 Godot 启动)
# 再判该套件 FAIL 继续下一套件——单套件挂死不拖死整批;正常退出路径判定形态不变。
# 场景探针(.tscn 载体)由本地闸(probe_gate.sh)单独步骤调用,不经本入口。
extends SceneTree
@@ -14,6 +18,11 @@ extends SceneTree
# 错误不改进程退出码、不计失败数,只能由调用方扫输出——三硬闸之二)。
const ERROR_MARKERS: Array[String] = ["SCRIPT ERROR", "Parse Error"]
# 每套件截止时间(秒):默认与 probe_gate 每段同额;环境变量可覆盖——服务挂死守护的
# 机制验证与日后调参,非法值/非正值一律回落默认。
const SUITE_TIMEOUT_ENV := "RUN_ALL_SUITE_TIMEOUT_SEC"
const SUITE_TIMEOUT_DEFAULT_SEC := 120
func _init() -> void:
# 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配。
@@ -154,21 +163,69 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int:
+ "非 console 版是 GUI 子系统,子进程 stdout/stderr 句柄无效,脚本错误扫描无从落地")
return 1
# 子进程输出只能落盘再读:OS.execute 的 output 管道在本机 Godot/Windows 组合下恒为空
#(实测 0 行),故整条命令交给 cmd /c 并重定向到日志,退出码由 cmd 原样回传。
#(实测 0 行),故整条命令交给 cmd 并重定向到日志。退出码同样走文件通道——
# 阻塞式 execute 无超时能力(套件挂死时整批陪葬),换 create_process 非阻塞起进程
# 后又没有退出码读取 API,故由 cmd /V:ON 在套件退出后把 !ERRORLEVEL! 回显落盘,
# 值与阻塞式 execute 经 cmd 回传的同一。
var proj := ProjectSettings.globalize_path("res://")
var log := proj + "/tests/.gates/run_all%s_%02d.log" % ["_filter" if filtered else "", idx]
var codefile := log + ".code"
if FileAccess.file_exists(codefile):
DirAccess.remove_absolute(codefile) # 陈值防护:上轮临界残件不许冒充本轮退出码
DirAccess.make_dir_recursive_absolute(proj + "/tests/.gates")
var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1' % [bin, proj, path, log]
# /V:ON=延迟展开:!ERRORLEVEL! 到 echo 执行时(套件已退出)才取值;
# 延迟展开会吞字面 !,命令串各段(引擎路径/工程路径/套件路径/日志路径)不得含 !。
var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1 & echo !ERRORLEVEL! > "%s"' % [bin, proj, path, log, codefile]
var out := PackedStringArray()
var code: int = OS.execute("cmd.exe", PackedStringArray(["/c", cmd]), out, true, false)
var pid: int = OS.create_process("cmd.exe", PackedStringArray(["/V:ON", "/C", cmd]))
if pid < 0:
push_error("[run_all] FAIL %s — cmd 子进程启动失败(pid=%d)" % [path, pid])
return 1
# 轮询先查存活再查截止:进程在轮询间隙自然退出时按正常路径判,截止临界不误杀。
var timed_out := false
var deadline_ms := Time.get_ticks_msec() + _suite_timeout_sec() * 1000
while true:
if not OS.is_process_running(pid):
break
if Time.get_ticks_msec() >= deadline_ms:
timed_out = true
break
OS.delay_msec(200)
var text := FileAccess.get_file_as_string(log) if FileAccess.file_exists(log) else ""
var negative := bool(suite.get("negative", false))
if timed_out:
var kill_out := PackedStringArray()
OS.execute("taskkill", PackedStringArray(["/F", "/T", "/PID", str(pid)]), kill_out, true)
# taskkill 落刀到进程对象消失有间隔:等死确认再放行下一套件(10s 宽限),
# 确认不了也如实报红留 PID 供手工清杀,不静默放行。
var reap_deadline := Time.get_ticks_msec() + 10000
while OS.is_process_running(pid) and Time.get_ticks_msec() < reap_deadline:
OS.delay_msec(200)
var reaped := not OS.is_process_running(pid)
# 临界窗口:截止到落刀之间套件可能恰好跑完 echo——同删退出码文件,.gates 不积残件。
if FileAccess.file_exists(codefile):
DirAccess.remove_absolute(codefile)
if negative:
print("[run_all] %s:负面套件——全文输出已折叠(输出中的错误为其测试目标,非缺陷)" % path)
else:
print(text)
if reaped:
push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树已整树回收(pid=%d)" % [path, _suite_timeout_sec(), pid])
else:
push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树回收未确认(pid=%d,需手工清杀残留)" % [path, _suite_timeout_sec(), pid])
return 1
# 负面套件(坏表/违规输入是它的测试目标)全文折叠,只回显结论行——
# 它输出里的 Parse Error 是预期产物,回显出去会让编排层的日志扫描误判成真实脚本错误。
if negative:
print("[run_all] %s:负面套件——全文输出已折叠(输出中的错误为其测试目标,非缺陷)" % path)
else:
print(text)
var code_txt := FileAccess.get_file_as_string(codefile).strip_edges() if FileAccess.file_exists(codefile) else ""
if code_txt == "" or not code_txt.is_valid_int():
push_error("[run_all] FAIL %s — 退出码文件缺失或非数字(%s)——子进程未走到退出码落盘即死,判红" % [path, codefile])
return 1
var code: int = int(code_txt)
DirAccess.remove_absolute(codefile) # 用毕即删:退出码经文件通道但不落 residuum(.gates 只留 *.log)
if not negative and _has_script_error(text):
push_error("[run_all] FAIL %s — 输出含脚本错误标记:%s" % [path, _script_error_line(text)])
return 1
@@ -205,6 +262,13 @@ func _engine_bin() -> String:
return ""
func _suite_timeout_sec() -> int:
var env := OS.get_environment(SUITE_TIMEOUT_ENV)
if env != "" and env.is_valid_int() and int(env) > 0:
return int(env)
return SUITE_TIMEOUT_DEFAULT_SEC
func _has_script_error(text: String) -> bool:
return _script_error_line(text) != ""
-2
View File
@@ -66,10 +66,8 @@ func _check_main_scene(main: Node) -> void:
if main == null:
return
var run_scope: Node = main.get("run_scope")
check(run_scope != null, "闲时会话 run_scope 非空(永非 null 契约)")
check(run_scope != null and run_scope.is_inside_tree(), "闲时会话 run_scope 在树")
var start_overlay: Control = main.get("_start_overlay")
check(start_overlay != null, "开始菜单层已装配(_start_overlay)")
check(start_overlay != null and start_overlay.visible, "启动态菜单可见(boot 后玩家见菜单)")
check(main.get("settings_store") != null, "系统设置存储已装配(settings_store)")
check(main.get("debug_badge") != null, "调试入口角标已装配(debug_badge)")
+1 -3
View File
@@ -154,7 +154,7 @@ func _smoke_boot_chain() -> void:
await process_frame
# ① ConfigDB 就绪且表已载
var db = main.get_node("/root/ConfigDB")
check(db.is_ready and db.hero != null,
check(db.is_ready and db.combat.hero != null,
"V2a ConfigDB 就绪且表已载(is_ready+hero 表在座)〔装配生命批 V2〕")
# ② 设置已应用(引擎态=settings_store 现值——真实设置文件照读,一致性判据与文件内容无关)
var ss = main.settings_store
@@ -173,8 +173,6 @@ func _smoke_boot_chain() -> void:
var sm = main.get_node("/root/SaveManager")
# ④-1 boot 路径:启动态钉死(卡① sv2)——boot 无激活态、断点在档恒否,续玩钮恒隐
#(原「按钮态==真实档断点态」关系判据随启动态换代改写:test-conventions §9.5 申报在案,卡② mn8 再承接)
check(not sm.has_breakpoint(),
"V2d boot 无断点在档(启动态不隐式激活——卡① sv2 钉死)〔装配生命批 V2·卡② mn8 改写〕")
# ④-1b 启动态三断言(卡① sv2——真实 autoload boot 链)
check(sm.slots.active_slot() == 0 and not sm.slots.has_write_target(),
"V2-s1 boot 后无激活态(active_slot=0、写盘目标空)〔卡① sv2〕")
@@ -128,8 +128,7 @@ func _smoke_armor_roll() -> void:
if int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll):
crit_seed = s
break
Kit.check("nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.combat.settlement.damage_roll.crit_roll)], crit_seed > 0)
Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)",
Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量——种子可达 seed=%d)" % crit_seed,
int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false)))
Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.combat.hero.atk)],
int(crit_rows.get("normal", -1)) == 2 * int(_db.combat.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.combat.hero.atk))
@@ -58,7 +58,6 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void:
break
Kit.check("%s 回放断言:结算步骤流逐行相同" % label, JSON.stringify(run1.stream) == JSON.stringify(run2.stream))
Kit.check("%s 回放断言:事件流逐位相同" % label, JSON.stringify(run1.events) == JSON.stringify(run2.events))
Kit.check("%s 结算步骤流非空(%d 行)" % [label, run1.stream.size()], run1.stream.size() > 0)
var has_verdict := false
var hero_attacks := 0
var hero_breakdown_ok := true
@@ -892,7 +891,7 @@ func _smoke_skill_pool_validation_reject() -> void:
{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 0},
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 4}])
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "atk", "weight": 1}, {"skill_id": "w1", "weight": 1}]}])
Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side"))
Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table, _db.combat.status_effects), "cast_side"))
func _has_err_category(errs: Array, category: String) -> bool:
@@ -439,10 +439,16 @@ func _smoke_polarity_routing() -> void:
if _polarity_probe_accepts(probe):
polarity_seed = s
break
Kit.check("极性路由构造种子可达(seed=%d——并流后行为面重锚)" % polarity_seed, polarity_seed > 0)
Kit.check("构造触达:标中真雷 + 标空安全格均入队", bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"]))
Kit.check("构造触达:挖到危险格 ≥1(%d)" % int(probe["mine_events"]), int(probe["mine_events"]) >= 1)
Kit.check("构造触达:挖到安全格 ≥1(%d)" % int(probe["safe_events"]), int(probe["safe_events"]) >= 1)
# 触达三腿(标中标空入队/挖危险格/挖安全格)是 _polarity_probe_accepts 的合取项,
# 可达即三腿同真——缺腿明细进标签承载诊断,不再各立一条(2026-10-28 断言精简批删并)
var legs := ""
if not (bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"])):
legs += "标中标空未入队 "
if int(probe["mine_events"]) < 1:
legs += "挖危险格零 "
if int(probe["safe_events"]) < 1:
legs += "挖安全格零 "
Kit.check("极性路由构造种子可达(seed=%d——并流后行为面重锚;缺腿:%s)" % [polarity_seed, legs], polarity_seed > 0)
# 挖到的安全格:只出正向——其全部来源行必存在且 effect 行 ∈ 正向集
# (action 型排队行 effect_id=ap_delta 正负两极共用路由键——按行值符号辨极性:正向格恒 +power)
var safe_all_pos := true
@@ -16,7 +16,7 @@ func _init() -> void:
return
# ---- V1 出域拒绝(六反例+合法行零 error——构造行夹具直改 rows 后重校验)----
var skill = db.skill
var skill = db.combat.skill
var base_rows: Array = skill.rows.duplicate(true)
var ok_row := {"skill_id": "t_probe", "display_name": "探针", "type": "damage", "damage_mult": 1.5,
"cooldown_beats": 2, "trigger": {"fact": "round_index", "op": "gte", "value": 3, "latch": false}}
@@ -133,7 +133,7 @@ func _init() -> void:
# ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)----
var cv = load("res://core/config_db_cross_validator.gd").new(db)
var pool_errs: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects)
var pool_errs: Array = cv.validate_skill_pool_links(db.combat.skill, db.combat.enemy, db.combat.status_effects)
var comp_violations := 0
for e in pool_errs:
if String((e as Dictionary).get("category", "")) == "pool_composition" and String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
@@ -150,7 +150,7 @@ func _init() -> void:
cond_pool.append({"skill_id": "sneak_ambush", "weight": 9}) # 条件技唯一特殊技→纪律违规形态
cond_only_row["pool"] = cond_pool
db.combat.enemy.rows[0] = cond_only_row
var errs2: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects)
var errs2: Array = cv.validate_skill_pool_links(db.combat.skill, db.combat.enemy, db.combat.status_effects)
var hit2 := false
for e in errs2:
if String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
+58 -47
View File
@@ -44,6 +44,15 @@ func _smoke_find_token(candidates: Array, kind: String, tier: String) -> Diction
return {}
# 效果命中分桶(公开 dispatch 返回值的本地过滤——断言钉公开可观测面;编排器私有过滤器的测试侧等价承载)
func _hits_kind(hits: Array, kind: String) -> Array:
var out: Array = []
for h in hits:
if String((h as Dictionary).get("kind", "")) == kind:
out.append(h)
return out
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态)
@@ -355,9 +364,11 @@ func _smoke_adventure() -> void:
Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.buy_product("hp_up"))
var acc_shop: int = a8.journal().bonus_total("max_hp") # L1 三选一后 + 商店 3(首件随种子)
Kit.check("统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop, acc_shop >= 3)
var l2_cap := {"payload": {}} # 公开信号面采集(curio_choice_presented 停点载荷——容器写回规避 lambda 按值捕获)
a8.stop().curio_choice_presented.connect(func(p): l2_cap["payload"] = p)
a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index,
"node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟 L2 关卡完成(victory 载荷直调——非末关)
Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕", not a8.stop()._pending.is_empty())
Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕", not (l2_cap["payload"] as Dictionary).is_empty())
Kit.check("未兑付推进拒绝(选择停点零自动推进契约)", not a8.continue_to_next_level())
var l2_cands: Array = a8.stop()._pending["candidates"]
var l2_pick: Dictionary = l2_cands[0]
@@ -438,7 +449,7 @@ func _smoke_adventure() -> void:
a13.stop().present_node_curio([{"kind": "gp", "tier": "common"}])
var gp13: int = a13.wallet().gp
Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.run.adventure.curio_value("gp", "common")), a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.run.adventure.curio_value("gp", "common")))
Kit.check("节点停点态兑付后清零(恰一次语义同构)", a13.stop()._node_pending.is_empty())
Kit.check("节点停点态兑付后清零(恰一次语义同构)", (a13.stop().node_curio_pending_state().get("candidates", []) as Array).is_empty())
var s10: Dictionary = a10.summary
Kit.check("放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)", String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)))
@@ -451,7 +462,7 @@ func _smoke_adventure() -> void:
func _smoke_feats() -> void:
print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---")
# ① 现行表牌库构成:30 件全量入池——102 数值令牌+30 relic 令牌(四级 rarity 直映射)
Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.feats != null and (_db.crossrun.feats.rows as Array).size() == 30)
Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.crossrun.feats != null and (_db.crossrun.feats.rows as Array).size() == 30)
Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.crossrun.feats.validate().is_empty())
var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71))
Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71))
@@ -462,7 +473,7 @@ func _smoke_feats() -> void:
feat_tokens += 1
var tk := String((t as Dictionary).get("tier", ""))
tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1
Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0._curio.deck.size() == 132)
Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0.deck().deck.size() == 132)
# ② 旧 27 件零残留钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归静态核查)
Kit.check("旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕", _db.crossrun.feats.row_by_id("arcane_ember").is_empty() and _db.crossrun.feats.row_by_id("magma_body").is_empty())
Kit.check("迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕", _db.crossrun.treasure_items.row_by_id("venom_edge").is_empty() and _db.crossrun.treasure_items.row_by_id("seal_burst").is_empty())
@@ -505,15 +516,15 @@ func _smoke_feats() -> void:
"triggers": [{"on": "seal_hit", "do": {"kind": "ac_bonus", "value": 1, "mode": "round"}}]},
]
Kit.check("新 schema 构造表 validate 零违例(四级 rarity/cap 字段/词表全过——流派标签已退役零 archetype)", rt.validate().is_empty())
var live_rows: FeatTable = _db.feats
_db.feats = rt
var live_rows: FeatTable = _db.crossrun.feats
_db.crossrun.feats = rt
var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72))
Kit.check("专长带场景:冒险启动(seed 72——注入 5 行临时表)", a_r.start_adventure(72))
var deck_relic := 0
for t in a_r._curio.deck:
if String((t as Dictionary).get("kind", "")) == "relic":
deck_relic += 1
Kit.check("专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107", deck_relic == 5 and a_r._curio.deck.size() == 107)
Kit.check("专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107", deck_relic == 5 and a_r.deck().deck.size() == 107)
# 渠道白名单断言(渠道轴定位:关卡完成渠道只出专长/节点渠道零专长)
var feat_seen: Dictionary = {}
var node_relic_seen := false
@@ -550,7 +561,7 @@ func _smoke_feats() -> void:
a_t.stop()._deck.ticket_pity = int(a_t.db.run.adventure.feat_ticket_pity_step)
var rg_t: RandomNumberGenerator = a_t.rng.open_stream("reward_pick")
Kit.check("A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)", a_t.deck().feat_ticket_consider(rg_t))
Kit.check("A6 触发后清零", a_t.stop()._deck.ticket_pity == 0)
Kit.check("A6 触发后清零", a_t.deck().ticket_pity == 0)
a_t.stop()._deck.ticket_sold = int(a_t.db.run.adventure.feat_ticket_limit)
a_t.stop()._deck.ticket_pity = int(a_t.db.run.adventure.feat_ticket_pity_step)
Kit.check("A6 限购满不再触发(保底冻结)", not a_t.deck().feat_ticket_consider(rg_t))
@@ -566,7 +577,7 @@ func _smoke_feats() -> void:
Kit.check("relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1], a_r.stop().pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.deck().pool_stock("relic") == deck_relic - 1)
# 断点往返三条(基点 1420–1426:relics 快照/计数器键/余库逐枚还原)按主会话裁决②改判集成宿主——
# 驱动代码(save_manager 装配+_on_level_finished+快照取读)随三条同去,集成批落位时重建。
_db.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知)
_db.crossrun.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知)
# ⑤ V3 订阅面逐件(附表 A 三列——命中面=EffectEngine dispatch 直测;账本面=counter 直读;
# 落地行为=发射点施加面〔T1 施加段承载——本段按声明逐件钉命中与边界/反例〕)
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(73))
@@ -593,13 +604,13 @@ func _smoke_feats() -> void:
fx.rebuild(["geologist"], [], [])
var geo_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": true})
apply_fx.call(geo_hits)
Kit.check("地质学家:首开新类型集 1 点〔专长批 V3〕", orch._hits_of(geo_hits, "counter_add").size() == 1)
Kit.check("地质学家:首开新类型集 1 点〔专长批 V3〕", _hits_kind(geo_hits, "counter_add").size() == 1)
EffectEngine.counter_add(orch.fx_host().counters_ctx, "geo_types", 2) # 手动基数补齐(row1 已落账 1+2=3——满 4 种的预评估现值)
var geo_hits2: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": true})
apply_fx.call(geo_hits2)
Kit.check("地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕", orch._hits_of(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0)
Kit.check("地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕", _hits_kind(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0)
var geo_hits3: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": false})
Kit.check("地质学家反例:同类型重复首开不叠点〔专长批 V3〕", orch._hits_of(geo_hits3, "counter_add").is_empty())
Kit.check("地质学家反例:同类型重复首开不叠点〔专长批 V3〕", _hits_kind(geo_hits3, "counter_add").is_empty())
fx.rebuild(["hard_hat"], [], [])
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}))
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}))
@@ -613,29 +624,29 @@ func _smoke_feats() -> void:
fx.rebuild(["skip_mining"], [], [])
var sk_zero: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": true})
apply_fx.call(sk_zero)
Kit.check("跳格采掘:空白 0 格攒 1 点〔专长批 V3〕", orch._hits_of(sk_zero, "counter_add").size() == 1)
Kit.check("跳格采掘:空白 0 格攒 1 点〔专长批 V3〕", _hits_kind(sk_zero, "counter_add").size() == 1)
EffectEngine.counter_add(orch.fx_host().counters_ctx, "skip_pts", 1) # 手动基数补齐(已落账 1+1=2——满 3 点的预评估现值)
var sk_full: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": true})
apply_fx.call(sk_full)
Kit.check("跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕", orch._hits_of(sk_full, "reveal_tiles").size() == 1)
Kit.check("跳格采掘反例:普通开格不计点〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty())
Kit.check("跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕", _hits_kind(sk_full, "reveal_tiles").size() == 1)
Kit.check("跳格采掘反例:普通开格不计点〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty())
fx.rebuild(["blast_accounting"], [], [])
for i in 5:
var ba_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_paid", {"ap_cost": 1})
for h in ba_hits:
if String(h.get("kind", "")) == "counter_add":
EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1)
Kit.check("爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1)
Kit.check("爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1)
fx.rebuild(["frontier_miner"], [], [])
Kit.check("边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1
and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty())
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty())
# 封印系
fx.rebuild(["seal_echo"], [], [])
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}))
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}))
var se_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_end", {})
Kit.check("封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕", orch._hits_of(se_hits, "grant_ap").size() == 1 and String((orch._hits_of(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue")
Kit.check("封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕", _hits_kind(se_hits, "grant_ap").size() == 1 and String((_hits_kind(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue")
fx.rebuild(["seal_smith"], [], [])
var sm_remove := 0
for i in 3:
@@ -647,10 +658,10 @@ func _smoke_feats() -> void:
Kit.check("封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕", sm_remove == 1)
fx.rebuild(["seal_stake"], [], [])
Kit.check("封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1
and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round")
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1
and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round")
fx.rebuild(["warning_beacon"], [], [])
Kit.check("示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1)
Kit.check("示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1)
# 踩雷/受伤系
fx.rebuild(["pain_memory"], [], [])
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}))
@@ -666,13 +677,13 @@ func _smoke_feats() -> void:
for h in fw_hits:
if String(h.get("kind", "")) == "shield":
fw_shield += int(h.get("value", 0))
Kit.check("保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty())
Kit.check("保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty())
for h in fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 6}):
if String(h.get("kind", "")) == "counter_add":
EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)))
Kit.check("保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕", fw_shield == 0)
fx.rebuild(["blood_insight"], [], [])
Kit.check("见血开窍:踩雷揭示 1 格〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1)
Kit.check("见血开窍:踩雷揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1)
fx.rebuild(["trap_instinct"], [], [])
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}))
var ti_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
@@ -688,67 +699,67 @@ func _smoke_feats() -> void:
fx.rebuild(["trap_burst"], [], [])
var tb_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"damage": 8})
Kit.check("陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕",
orch._hits_of(tb_hits, "blast_scaled").size() == 1
and float((orch._hits_of(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0)
_hits_kind(tb_hits, "blast_scaled").size() == 1
and float((_hits_kind(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0)
fx.rebuild(["blood_honey"], [], [])
Kit.check("血蜜:踩雷回复 N 生命〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1)
Kit.check("血蜜:踩雷回复 N 生命〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1)
fx.rebuild(["stone_skin"], [], [])
var ss_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1})
Kit.check("石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕",
orch._hits_of(ss_hits, "ac_bonus").size() == 1
and int((orch._hits_of(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3)
_hits_kind(ss_hits, "ac_bonus").size() == 1
and int((_hits_kind(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3)
fx.rebuild(["last_stand"], [], [])
Kit.check("背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1
and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty())
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty())
# 跨族桥
fx.rebuild(["breaker_strike"], [], [])
Kit.check("破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1
and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty())
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty())
# 节奏系
fx.rebuild(["map_exchange"], [], [])
var me_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 4})
apply_fx.call(me_hits)
EffectEngine.counter_add(orch.fx_host().counters_ctx, "stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账
Kit.check("弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕", orch._hits_of(me_hits, "counter_add").size() == 1)
Kit.check("弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕", _hits_kind(me_hits, "counter_add").size() == 1)
var me_atk := 0
for h in orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus"):
me_atk = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", "")))
Kit.check("弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕", me_atk == 4)
Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty())
Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty())
fx.rebuild(["last_drop"], [], [])
var ld_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 2, "ap_exhausted_last": true})
Kit.check("一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕", orch._hits_of(ld_hits, "grant_ap").size() == 1)
Kit.check("一滴不剩反例:上轮有余不发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty())
Kit.check("一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕", _hits_kind(ld_hits, "grant_ap").size() == 1)
Kit.check("一滴不剩反例:上轮有余不发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty())
fx.rebuild(["market_close"], [], [])
for i in 8:
for h in fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}):
if String(h.get("kind", "")) == "counter_add":
EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1)
Kit.check("收盘:本轮开格≥8→+1 GP〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1)
Kit.check("收盘:本轮开格≥8→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1)
# 战斗系
fx.rebuild(["execution_scholar"], [], [])
Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1)
Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1)
fx.rebuild(["executioner"], [], [])
var ex_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 30, "is_boss": false})
Kit.check("处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕",
orch._hits_of(ex_hits, "target_attack_bonus").size() == 1
and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty())
_hits_kind(ex_hits, "target_attack_bonus").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty())
fx.rebuild(["field_dressing"], [], [])
Kit.check("战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1)
Kit.check("战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1)
# 契约尖点
fx.rebuild(["blood_tax"], [], [])
Kit.check("血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1
and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk")
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1
and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk")
# 迁入 2 件
fx.rebuild(["venom_edge"], [], [])
Kit.check("淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1
and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty())
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty())
fx.rebuild(["seal_burst"], [], [])
Kit.check("封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1)
Kit.check("封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1)
# 零持有
var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74))
orch0.fx_host().rebuild_subscriptions([], [], [])
@@ -176,8 +176,6 @@ func _smoke_board_generator() -> void:
b1.degraded_product == (mode == "accept_best"))
Kit.check("L%d 非法产物标记 = (accept_mode==accept_worst)(%s→%s)" % [L, mode, str(b1.illegal_product)],
b1.illegal_product == (mode == "accept_worst"))
Kit.check("L%d 降级/非法产物标记互斥(两出口不可同真)" % L,
not b1.degraded_product or not b1.illegal_product)
Kit.check("L%d 产物断言:实际雷数 %d ≤ 目标雷数 N=%d(先布后删只减不增)" % [L, int(b1.metrics.actual_mines), int(b1.metrics.target_mines)],
int(b1.metrics.actual_mines) <= int(b1.metrics.target_mines))
Kit.check("L%d attempts_used ∈ [1,%d]" % [L, int(row.max_gen_attempts)],
@@ -110,13 +110,13 @@ func _init() -> void:
Kit.check_quiet(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)")
Kit.check_quiet(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)")
Kit.check_quiet(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)")
Kit.check_quiet(int(adr_f._curio.taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
Kit.check_quiet(int(adr_f.deck().taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
Kit.check_quiet(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)")
var pick_e: Dictionary = cands_e[0]
Kit.check_quiet(adr_f.stop().pick_curio(String(pick_e.get("kind", "")), String(pick_e.get("tier", "")), String(pick_e.get("feat_id", ""))), "P5 点选兑付受理")
var picked_e: String = String(adr_f.stop()._resolved.get("picked", ""))
if adr_f._curio.taken.has(picked_e):
Kit.check_quiet(int(adr_f._curio.taken[picked_e]) == (2 if picked_e == "attack" else 1),
if adr_f.deck().taken.has(picked_e):
Kit.check_quiet(int(adr_f.deck().taken[picked_e]) == (2 if picked_e == "attack" else 1),
"P5 兑付计数一致(%s——预置 1 + 本次兑付)" % picked_e)
Kit.check_quiet(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)")
var bp_g: Dictionary = sm_f.breakpoint_snapshot()
@@ -67,7 +67,6 @@ func _test_dangling_skill_id() -> void:
{"skill_id": "attack", "weight": 9}, {"skill_id": "not_in_table", "weight": 3}]},
])
var errs: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemies, null)
Kit.check("池内悬空 skill_id 被报(跨表护栏不静默放过)", not errs.is_empty())
Kit.check("悬空项的错误类别可定位(category 非空)",
not errs.is_empty() and String((errs[0] as Dictionary).get("category", "")) != "")
@@ -64,7 +64,6 @@ func _test_band_hit() -> void:
func _test_rowset_order_preserved() -> void:
var rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "battle")
Kit.check("行集返回同带全部行(2 行)〔fv2〕", rows.size() == 2)
var ids: Array[String] = []
for r in rows:
ids.append(String((r as Dictionary).get("enemy_id", "")))
@@ -28,20 +28,21 @@ func _init() -> void:
Kit.check_quiet(adr.stop().curio_stop_pending() == true, "关卡完成停点在途=谓词 true")
adr.refresh_bank()._refresh_x = 1
adr.refresh_bank()._refresh_y = 2
Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_x == 0 and adr.refresh_bank()._refresh_y == 2, "X 优先消耗(x1y2→x0y2)")
Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_y == 1, "X 穷转 Y 池(y2→y1)")
Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank().refresh_y() == 2
and adr.refresh_bank().free_left() == adr.refresh_bank().refresh_y() + adr.refresh_bank().refresh_stock(), "X 优先消耗(x1y2→x0y2)")
Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank().refresh_y() == 1, "X 穷转 Y 池(y2→y1)")
# ---- ③ GP 付费档:计数递增爬价 + 零触碰免费位 ----
Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "首刷报价=基价(免费位尚余 y1,报价独立于免费位)")
Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank()._refresh_y == 1, "付费刷新不触碰免费位(y1 保持)")
Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank().refresh_y() == 1, "付费刷新不触碰免费位(y1 保持)")
Kit.check_quiet(adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve)), "二刷价爬升=ceil(基价×曲线)(已付费刷=1)")
Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve * curve)), "三刷价再爬升=ceil(基价×曲线²)(已付费刷=2)")
# ---- ④ 停点重建清零付费计数(对齐商店进店清零——不结转)----
adr.stop().present_node_curio(_cands())
Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 0 and adr.refresh_bank().gp_cost() == base, "节点停点重建=付费计数清零回基价")
Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "节点停点重建=付费计数清零回基价")
adr.stop().refresh_curio_paid()
Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 1, "付费刷新计数在途递增(=1)")
Kit.check_quiet(adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve)), "付费刷新计数在途递增(=1)")
adr.stop().present_node_curio(_cands())
Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "再碰三选一付费计数仍清零(同 X 余量不结转口径)")
@@ -56,10 +57,10 @@ func _init() -> void:
var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1}
var adr_r = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(adr_r.resume_adventure(bp_y, {"hero_override": {}, "attune_refresh_y": 3}), "续玩受理(最小断点+参数面)")
Kit.check_quiet(adr_r.refresh_bank()._refresh_y == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)")
Kit.check_quiet(adr_r.refresh_bank().refresh_y() == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)")
var adr_o = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(adr_o.resume_adventure({"level_index": 1, "hero_hp": 10, "graph_gen_v2": 1}, {"hero_override": {}, "attune_refresh_y": 3}), "旧断点续玩受理(快照缺键)")
Kit.check_quiet(adr_o.refresh_bank()._refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)")
Kit.check_quiet(adr_o.refresh_bank().refresh_y() == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)")
quit(Kit.finish("curio_refresh_paid"))
+37 -11
View File
@@ -32,8 +32,9 @@ func _init() -> void:
# V9-1 刷新券:入存量账(冒险级)——反例=写进 _attune_refresh_x(X 每停点重置会吞存量)
a.refresh_bank().reset_x() # X 归零基线(模拟新停点)
Kit.check_quiet(a.stop()._apply_curio_pick("refresh", "excellent", 1), "刷新券兑付受理〔专长批 V9〕")
Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank()._refresh_x == 0,
a.stop()._pending = {"candidates": [{"kind": "refresh", "tier": "excellent"}]} # 停点候选直写(构造面——pick_curio 真实入口前置)
Kit.check_quiet(a.stop().pick_curio("refresh", "excellent"), "刷新券兑付受理〔专长批 V9〕")
Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank().free_left() == a.refresh_bank().refresh_stock() + a.refresh_bank().refresh_y(),
"F-1 修复:刷新券入存量账、X 账零写入(写错账反例不复发)〔专长批 V9〕")
a.refresh_bank().reset_x() # 再碰停点——X 重置但存量不吞
Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank().free_left() == 1,
@@ -50,19 +51,21 @@ func _init() -> void:
Kit.check_quiet(deck_refresh_before == 0 or true, "牌库 refresh 令牌读数在域(投放 %d)" % deck_refresh_before)
# V9-2 储备金袋:待注入账 + 战首注入(节点级)
Kit.check_quiet(a.stop()._apply_curio_pick("rap", "rare", 3), "储备金袋兑付受理〔专长批 V9〕")
a.stop()._pending = {"candidates": [{"kind": "rap", "tier": "rare"}]} # 停点候选直写(构造面——pick_curio 真实入口前置)
Kit.check_quiet(a.stop().pick_curio("rap", "rare"), "储备金袋兑付受理〔专长批 V9〕")
Kit.check_quiet(a.stop()._pending_rap == 3, "储备金袋:待注入 RAP +3(节点级账)〔专长批 V9〕")
# V9-3 时沙:下一战首轮 AP 队列(回合级池增量)
Kit.check_quiet(a.stop()._apply_curio_pick("time_sand", "rare", 2), "时沙兑付受理〔专长批 V9〕")
a.stop()._pending = {"candidates": [{"kind": "time_sand", "tier": "rare"}]} # 停点候选直写(构造面——pick_curio 真实入口前置)
Kit.check_quiet(a.stop().pick_curio("time_sand", "rare"), "时沙兑付受理〔专长批 V9〕")
Kit.check_quiet(a.stop()._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕")
# V23 两件出池(R1——同调电池删除/完美保险迁专长)
Kit.check_quiet(not db.run.adventure.CURIO_KINDS.has("attune_battery")
and not db.run.adventure.CURIO_KINDS.has("perfect_insurance"),
"出池词表:CURIO_KINDS 零两件(三选一池物理不可再出)〔卡② R1〕")
Kit.check_quiet(not a.stop()._apply_curio_pick("attune_battery", "rare", 1)
and not a.stop()._apply_curio_pick("perfect_insurance", "rare", 1),
Kit.check_quiet(not a.stop().consume_pool_purchase("attune_battery", "rare")
and not a.stop().consume_pool_purchase("perfect_insurance", "rare"),
"出池反例:两件兑付不再受理(牌库无令牌=拒绝零副作用)〔卡② R1〕")
Kit.check_quiet(not a.has_method("choose_battery_target") and not a.has_method("battery_pending"),
"电池符号清零:runner 无点选函数与 getter(编译级孤儿面)〔卡② R1〕")
@@ -73,7 +76,11 @@ func _init() -> void:
a._loadout.set_attunement(["incense_stick"])
a._loadout_seeded = false # 模拟首关 bind 时点(分派走 seed_for_adventure 分支)
a._loadout_on_level_inventory() # 种行(首关 bind 时点语义)
Kit.check_quiet(a._loadout.charges_left("incense_stick", a.runner().treasure_inventory) == 1,
var incense_left := 0
for r in a.runner().treasure_inventory.snapshot_inventory():
if String(r.get("item_id", "")) == "incense_stick" and String(r.get("source_class", "")) == "attunement" and not bool(r.get("used", false)):
incense_left += 1
Kit.check_quiet(incense_left == 1,
"账本保留:种行按装备表值投行(线香 battle 级 1——attune_charges 语义零变化)〔卡② R1〕")
# V24 配额净删锚(R3——「净删」口径,rare 9→7/总量 104→102)
var rare_sum := 0
@@ -99,7 +106,7 @@ func _init() -> void:
a._curio.deck = [{"kind": "gp", "tier": "common"}, {"kind": "heal", "tier": "excellent"}]
a.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
var gp_taken_before := int(a._curio.taken.get("gp", 0))
Kit.check_quiet(a.stop().pick_curio("gp", "common") and int(a._curio.taken.get("gp", 0)) == gp_taken_before + 1,
Kit.check_quiet(a.stop().pick_curio("gp", "common") and int(a.deck().taken.get("gp", 0)) == gp_taken_before + 1,
"留任小GP 袋:GP 通道兑付受理(gp 计数入账——路由零变化)〔专长批 V10〕")
var hp_before: int = a.stats().hero_hp
a.stop()._pending = {"candidates": [{"kind": "heal", "tier": "excellent"}]}
@@ -165,17 +172,30 @@ func _init() -> void:
if safe_cells.size() >= 2:
break
Kit.check_quiet(safe_cells.size() == 2, "保险 rig:两枚安全格定位(去重闸按格实例——限次样本须异格)〔卡② R2〕")
var rows_orch: Array[Dictionary] = []
orch.step_row.connect(func(row): rows_orch.append(row)) # step_row 公开信号面(insurance_used 载荷/seal_miss highlight=豁免账与误标账观测面)
Kit.check_quiet(int(orch._perfect_hit_false) == 0 and int(orch._perfect_forgiven) == 0,
"保险前置:误标账与豁免账双零起〔卡② R2〕")
orch._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1)
Kit.check_quiet(int(orch._perfect_hit_false) == 1,
"计数纯净:首次标错 _perfect_hit_false 落 1 未被回退改写(契约 2)〔卡② R2〕")
Kit.check_quiet(int(orch._perfect_forgiven) == 1,
var ins_row := {}
for r in rows_orch:
if String(r.get("effect_id", "")) == "insurance_used":
ins_row = r
Kit.check_quiet(not ins_row.is_empty() and int(ins_row.get("forgiven", -1)) == 1,
"豁免一次:首次标错 _perfect_forgiven=1(forgive_seal_miss 派发)〔卡② R2〕")
Kit.check_quiet(int(orch.action_economy.pending_ap_delta()) == 1,
"事件不吞:同一标错的 seal_missed 仍被危险直觉·守收到(queue AP+1)〔卡② R2〕")
orch._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2)
Kit.check_quiet(int(orch._perfect_hit_false) == 2 and int(orch._perfect_forgiven) == 1,
var ins_count := 0
var miss_hl_last := ""
for r in rows_orch:
if String(r.get("effect_id", "")) == "insurance_used":
ins_count += 1
if String(r.get("type", "")) == "seal_miss":
miss_hl_last = String(r.get("highlight", ""))
Kit.check_quiet(ins_count == 1 and miss_hl_last == "perfect_broken",
"限次:第二次标错不获豁免(forgiven 恒 1——when counter lt 1)〔卡② R2〕")
Kit.check_quiet(not orch.perfect_sweep_earned(),
"判定相减:误标 2 − 豁免 1 = 有效 1 → 完美不成立〔卡② R2 真值表〕")
@@ -195,9 +215,15 @@ func _init() -> void:
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
"次战 rig:第二战编排启动(feats 同件)〔卡② R2〕")
var rows_orch2: Array[Dictionary] = []
orch2.step_row.connect(func(row): rows_orch2.append(row)) # step_row 公开信号面(insurance_used 载荷=豁免账观测面)
var sc2 = safe_cells[0]
orch2._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1)
Kit.check_quiet(int(orch2._perfect_forgiven) == 1 and int(orch2._perfect_hit_false) == 1,
var ins_row2 := {}
for r in rows_orch2:
if String(r.get("effect_id", "")) == "insurance_used":
ins_row2 = r
Kit.check_quiet(not ins_row2.is_empty() and int(ins_row2.get("forgiven", -1)) == 1,
"跨战清零:domain=battle 豁免账每战重建——次战首次标错再豁免一次〔卡② R2〕")
Kit.check_quiet(EffectEngineKindProbe(db), "词表闭集:forgive_seal_miss 在 EFFECT_KINDS(出域即加载致命——扩签合规)〔卡② R4〕")
+4 -2
View File
@@ -119,7 +119,8 @@ func _init() -> void:
Kit.check_quiet(w3.gp == 37 and j3.keystone_gained() == 3 and j3.vision_used() == 1
and s3.hero_hp == 21 and (s3.feats_held() as Array).has("twin_pick"),
"hr2 片段往返:三件账面键逐值(生命/特性/钱包/钥石/真视)")
Kit.check_quiet((j3._gain_log as Array).size() == 3 and (j3._bonus_detail as Array).size() == 3,
var jfrag: Dictionary = j3.breakpoint_fragment().get("gain_journal", {}) as Dictionary
Kit.check_quiet((jfrag.get("records", []) as Array).size() == 3 and (jfrag.get("details", []) as Array).size() == 3,
"hr2 片段往返:日记全量还原")
# hr2 旧档回退(无日记键→汇总直载跳过比对)
@@ -127,7 +128,8 @@ func _init() -> void:
legacy.erase("gain_journal")
var j4 = GainJournalScript.new()
j4.restore_fragment(legacy, {})
Kit.check_quiet(j4.bonus_total("atk") == 7 and j4.bonus_total("armor_class") == 1 and (j4._gain_log as Array).is_empty(),
Kit.check_quiet(j4.bonus_total("atk") == 7 and j4.bonus_total("armor_class") == 1
and ((j4.breakpoint_fragment().get("gain_journal", {}) as Dictionary).get("records", []) as Array).is_empty(),
"hr2 旧档缺日记→汇总键直载(跳过比对——旧档行为零变化)")
# hr2 日记在档推导比对不符→拒绝载入(fail-fast——汇总不回填)
@@ -85,7 +85,7 @@ func _runner(db, nodes: Array, anchor_id: int, candidates: Array, n: int = 2):
var t := LevelTableScript.new()
t.info_visible_layers = n
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}]
db.level = t
db.run.level = t
var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes})
r.visited.clear()
@@ -202,7 +202,7 @@ func _test_resume_rebuild(db) -> void:
var t := LevelTableScript.new()
t.info_visible_layers = 2
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30}]
db.level = t
db.run.level = t
var resumed = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
resumed.hero_hp = 10 # 冒险模式生命经主角账预置(旧 ledger 注入参数的等效构造面)
var ok: bool = resumed.start_level_external(1, Callable(), false,
@@ -1322,7 +1322,7 @@ func _smoke_shop_pool() -> void:
Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.buy_pool_item(kind))
Kit.check("钱包扣现价(%d → %d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - int(buy_c.get("price", 0)))
Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.run.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.run.adventure.curio_value(kind, tier)))
Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8._curio.taken.get(kind, 0))], int(a8._curio.taken.get(kind, 0)) == taken0 + 1)
Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8.deck().taken.get(kind, 0))], int(a8.deck().taken.get(kind, 0)) == taken0 + 1)
# A 链:同visit 第二购现价 = ceil(base×b^(l2−1)×A^1)(货架已全重抽——新卡新价同公式)
# A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为)
r8.gp = int(r8.gp) + 64
@@ -294,13 +294,13 @@ func _meta_settle_and_workshop() -> void:
a12.refresh_bank()._refresh_y = 2
a12.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}, {"kind": "attack", "tier": "common"}, {"kind": "max_hp", "tier": "common"}], "payload": {}}
Kit.check("刷新双计数器——X 优先消耗(x1y2 → x0y2)",
a12.stop().refresh_curio() and a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank()._refresh_y == 2)
a12.stop().refresh_curio() and a12.refresh_bank().refresh_y() == 2 and a12.refresh_bank().free_left() == 2 + a12.refresh_bank().refresh_stock())
Kit.check("刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)",
a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank()._refresh_y == 0
a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank().refresh_y() == 0
and a12.stop().refresh_curio() == false)
a12.refresh_bank().reset_x()
Kit.check("刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕",
a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank().free_left() == 0)
a12.refresh_bank().free_left() == 0)
# ⑬ 主菜单开局传参面(开新/续玩两入口同调三键随开局下传防回归:真实入口曾
# 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params
# 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源)
@@ -338,7 +338,8 @@ func _meta_settle_and_workshop() -> void:
Kit.check("继续上局受理面:resume_adventure 受理三键参数面(尾裁微批b 受理闭环——受理不再吃白板)",
adr_res.resume_adventure(bp_min, p_resume))
Kit.check("继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)",
(adr_res._tree_rules as Array) == rules_e)
adr_res.runner() != null and adr_res.runner().pick_next_node(int(adr_res.runner().next_candidate_ids()[0]))
and ((adr_res.runner().current_battle().encounter.get("tree_rules", []) as Array) == rules_e))
_meta_smoke_cleanup()
@@ -390,7 +391,7 @@ func _smoke_forge_axes() -> void:
print("--- 锻造工坊·类型轴所有权移交(T2)---")
# V5:hero.tres 静态表两轴已删(全表加载零违例——config_db_test 同款闸;此处钉字段不在位)
Kit.check("hero.tres 无 attack_type/armor_type 字段(契约 2——静态表两轴删除)〔锻造工坊批 V5〕",
not ("attack_type" in _db.hero) and not ("armor_type" in _db.hero))
not ("attack_type" in _db.combat.hero) and not ("armor_type" in _db.combat.hero))
# 锻造配置默认值 = normal/light(常驻持有——与柜台解锁状态解耦,未点亮也照常可读)
Kit.check("锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕",
_db.crossrun.forge.default_attack_type == "normal" and _db.crossrun.forge.default_armor_type == "light")
@@ -546,7 +547,7 @@ func _smoke_tree() -> void:
print("--- 天赋树(12 节点 · 购买面 · Fork 双态 · 彩蛋觉醒)---")
# 〔V1〕结构:12 行 + schema 零违例
Kit.check("meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕",
_db.meta_tree != null and (_db.crossrun.meta_tree.rows as Array).size() == 12)
_db.crossrun.meta_tree != null and (_db.crossrun.meta_tree.rows as Array).size() == 12)
Kit.check("MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)",
_db.crossrun.meta_tree.validate().is_empty())
# 彩蛋行硬要求(契约 9 红线落点):effects 键集 ⊆ {max_hp, hit}
@@ -730,11 +731,13 @@ func _smoke_tree_rules() -> void:
# 财富之道·散:商品标价折价(level_runner 计价单点)
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004),
{"tree_rules": []}) # 构造注入(拆件批 T3——setter 群退役)
Kit.check("散未激活:商品标价原价(无 economy_spend 键)", int(lr._spend_discounted(100)) == 100)
Kit.check("散未激活:商品标价原价(无 economy_spend 键)",
int(lr.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0)))
lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005),
{"tree_rules": ["economy_spend"]}) # 构造注入(拆件批 T3——setter 群退役)
Kit.check("散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)",
int(lr._spend_discounted(100)) == 80 and int(lr._spend_discounted(1)) == 1)
int(lr.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))
and int(lr.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))))
# 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×)
var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
Kit.check("聚 rig:基线编排启动(无增益侧)",
@@ -310,7 +310,6 @@ func _test_rng_single_point() -> void:
svc.new_adventure(4242)
var before: int = svc.counter()
var g = GenScript.new(svc).generate(_params(2), 42)
Kit.check("pure_stream 不推全局计数器(产图前后 counter 同值)〔fv10〕", svc.counter() == before)
var a = svc.pure_stream(42, 0)
var b = svc.pure_stream(42, 0)
Kit.check("pure_stream 同 (seed,scope) 同流〔fv10〕", a.randf() == b.randf() and a.randi() == b.randi())
+3 -3
View File
@@ -36,7 +36,7 @@ func _init() -> void:
var e = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
e.start_adventure(501)
e.stop()._pending = {"candidates": []}
Kit.check_quiet(e.stop().skip_empty_curio() and e.stop()._pending.is_empty(),
Kit.check_quiet(e.stop().skip_empty_curio() and (e.stop().breakpoint_fragment().get("bonus_pending", {}) as Dictionary).is_empty(),
"空配置闸:三选一池空=停点整体跳过(skip_empty_curio 消化)〔专长批 T10〕")
e.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
Kit.check_quiet(not e.stop().skip_empty_curio(),
@@ -170,12 +170,12 @@ func check_v8(c, db) -> void:
c._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
for i in 20:
c.deck().draw_candidates("reward_node")
Kit.check_quiet(bool(c._curio.cap_ok("gp", "")) and int(c._curio.taken.get("gp", 0)) == 0,
Kit.check_quiet(bool(c.deck().cap_ok("gp", "")) and int(c.deck().taken.get("gp", 0)) == 0,
"cap 刷出不计:20 次抽签后 gp 未拿、cap 未触发〔专长批 V8〕")
# 拿了才计:pick 一次→cap 满→提前退役(候选视图剔除、牌库本体不动)
c.stop()._pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]}
Kit.check_quiet(c.stop().pick_curio("gp", "common"), "V8 前置:兑付 gp 一枚")
Kit.check_quiet(not bool(c._curio.cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕")
Kit.check_quiet(not bool(c.deck().cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕")
var gp_in_deck := 0
for t in c._curio.deck:
if String((t as Dictionary).get("kind", "")) == "gp":
@@ -23,7 +23,7 @@ func _init() -> void:
# ① 无参实例(autoload 实例化形态)=无激活态+默认合法空态(sv2 类级)
func _test_no_arg_instance_has_no_activation() -> void:
var sm := SaveManagerScript.new()
Kit.check_quiet(sm._path == "" and sm.slots.active_slot() == 0 and not sm.slots.has_write_target(),
Kit.check_quiet(sm.slots.active_slot() == 0 and not sm.slots.has_write_target(),
"sv2 无参实例无激活态(路径空/激活号 0/写盘目标空)〔卡① sv2〕")
Kit.check_quiet(sm.meta_state.keystone_balance() == 0 and not sm.has_breakpoint()
and sm.meta_state.career_state().adventure_total == 0 and sm.meta_state.career_state().adventure_cleared == 0,
+33 -2
View File
@@ -29,8 +29,39 @@ func _init() -> void:
Kit.check_quiet(oc._combat_seg != null and oc._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕")
Kit.check_quiet(oc._decay_seg != null and oc._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕")
Kit.check_quiet(oc._verdict_seg != null and oc._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕")
Kit.check_quiet(oc._grant_seg._apply_counter_hit({"counter_id": "geo_types", "value": 1}) == null
and EffectEngine.counter(oc.fx_host().counters_ctx, "geo_types") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价——写口经 ctx 落 battle 桶)〔迭代27 T2〕")
# counter_add 写口真实路径(geologist 订阅行承载:公共命令链 begin_battle→move→dig→stop
# 经授予段 counter_add 单点落 counters_ctx battle 桶——受理载荷+公开读数口 fx_host().counters_ctx)
var rng_c = load("res://core/rng_service.gd").new()
rng_c.set_master_seed(4241)
var orch_c = load("res://core/battle_orchestrator.gd").new(db, rng_c)
var orch_ok: bool = orch_c.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "feats": ["geologist"]})
var tgt_c := Vector2i(-9, -9)
var dig_acc := false
if orch_ok:
var b_c = orch_c.minesweeper.current_board()
var front_c := Vector2i(-9, -9)
for y in b_c.height:
for x in b_c.width:
var cell_c := Vector2i(x, y)
if not orch_c.minesweeper.is_revealed(cell_c) or not b_c.is_safe(cell_c):
continue
for nb in b_c.neighbors8(cell_c):
if b_c.in_bounds(nb) and not orch_c.minesweeper.is_revealed(nb) and b_c.is_safe(nb) \
and b_c.digit_of(nb) != 0 and not orch_c.minesweeper.has_seal(nb):
front_c = cell_c
tgt_c = nb
break
if tgt_c != Vector2i(-9, -9):
break
if tgt_c != Vector2i(-9, -9):
break
if tgt_c != Vector2i(-9, -9):
orch_c.minesweeper.settle_batch([{"action": "move", "cell": front_c}])
var dg_c: Array = orch_c.minesweeper.settle_batch([{"action": "dig", "cell": tgt_c}])
orch_c.minesweeper.settle_batch([{"action": "stop"}])
dig_acc = not (dg_c as Array).is_empty() and bool((dg_c[0] as Dictionary).get("accepted", false))
Kit.check_quiet(orch_ok and tgt_c != Vector2i(-9, -9) and dig_acc
and EffectEngine.counter(orch_c.fx_host().counters_ctx, "geo_types") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价——写口经 ctx 落 battle 桶)〔迭代27 T2〕")
# V4-3 真实一轮结算走段序(begin_battle→停手→_maybe_settle→五段→裁定行)
var rng = load("res://core/rng_service.gd").new()
+1 -1
View File
@@ -32,7 +32,7 @@ func _stub_db() -> StubDB:
"effect_kinds": ["reward_preview"], "target": "area_3x3", "magnitude": 0, "use_phase": "active_dig"},
}
var db := StubDB.new()
db.treasure_items = t
db.crossrun.treasure_items = t
return db
@@ -89,8 +89,8 @@ func _first_encounter_band(level_index: int, enemy_id: String) -> int:
# ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸;TP0 令牌池:配额结构/件值对/池总量域检)
func _smoke_adventure_table() -> void:
print("--- 冒险表(AdventureTable——令牌池行值单点:回血/配额/件值对/同调位)---")
Kit.check("db.adventure 已加载(adventure.tres 入 manifest)", _db.adventure != null)
if _db.adventure == null:
Kit.check("db.adventure 已加载(adventure.tres 入 manifest)", _db.run.adventure != null)
if _db.run.adventure == null:
return
Kit.check("AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)", _db.run.adventure.validate().is_empty())
Kit.check("clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" % int(_db.run.adventure.clear_heal_pct),
@@ -419,7 +419,7 @@ func _smoke_variation_framework() -> void:
func _smoke_difficulty_table() -> void:
print("--- 难度曲线与Boss·DifficultyTable 结构位 ---")
Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.difficulty != null and _db.run.difficulty.rows.is_empty())
Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.run.difficulty != null and _db.run.difficulty.rows.is_empty())
# ① 空表零行为:敌人系统.spawn 敌值 = EnemyTable 行值原样
var enemy_system = load("res://core/unit_container.gd").new(_db)
Kit.check("spawn 受理", enemy_system.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}))
@@ -498,7 +498,7 @@ func _smoke_nine_types_gp_stream() -> void:
# conversion 覆盖恰三,Boss 两档 first ≥ repeat)----
func _smoke_meta() -> void:
print("--- 元进度·MetaTable 表结构 ---")
var meta_tbl = _db.meta
var meta_tbl = _db.crossrun.meta
Kit.check("MetaTable 非空 + validate 零错(三行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)",
meta_tbl != null and meta_tbl.validate().is_empty())
Kit.check("MetaTable 行族规模 3/3/2(Boss/换算恰三/Y 升级两档——forge_products 迭代 21 退役、workshop_items 迭代 22 退役)",
@@ -35,7 +35,7 @@ func _runner(db, nodes: Array, candidates: Array, n: int = 2):
var t := LevelTableScript.new()
t.info_visible_layers = n
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}]
db.level = t
db.run.level = t
var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes})
r.node_index = -1
@@ -56,7 +56,6 @@ func _sweep_narrator() -> void:
var head := String(narrator.node_curio_header())
Kit.check("奖励点头部含「冒险牌库」(u3 机制事实①同库)〔wa4〕", head.contains("冒险牌库"))
Kit.check("奖励点头部含「属性值提升与补给」(u3 机制事实②渠道分流)〔wa4〕", head.contains("属性值提升与补给"))
Kit.check("奖励点头部含「同库分流」(u3 机制事实②措辞)〔wa4〕", head.contains("同库分流"))
Kit.check("奖励点头部零「同权重」旧话(u3 清零)〔wa4〕", not head.contains("同权重"))
Kit.check("奖励点头部零「同上限」旧话(u3 清零)〔wa4〕", not head.contains("同上限"))
Kit.check("奖励点头部零「提升池」旧话(u3 清零)〔wa4〕", not head.contains("提升池"))
@@ -72,7 +71,6 @@ func _sweep_legend() -> void:
if String(e[1]) == "封印":
seal_row = e
break
Kit.check("图例封印行存在(wa5 夹具前提)〔wa5〕", not seal_row.is_empty())
Kit.check("图例封印行含「标空」(u4——与「标中」成对)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("标空"))
Kit.check("图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚"))
Kit.check("图例封印行零「插错」(u4 旗族遗留词干清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("插错"))
@@ -81,7 +81,7 @@ func _init() -> void:
stage.on_step_row_fx({"type": "damage", "target": "地精", "target_id": "goblin_b"})
await create_timer(0.6).timeout # 弹道到达拍 0.33s + 余量
Kit.check("伤害行按 target_id 命中正确站桩(闪白落点)〔wd12〕",
s2._flash_count == 1 and s1._flash_count == 0)
s2.fx_probe().flash_count == 1 and s1.fx_probe().flash_count == 0)
# 口二:enemy_standee_center —— id 先、display_name 兜底
var c_b = stage.enemy_standee_center("goblin_b", "地精")
var c_b_expect: Vector2 = stage.world_to_shell(s2.position + s2.box * 0.5)
@@ -611,18 +611,6 @@ func _smoke_button_five_states() -> void:
is_equal_approx((dds as StyleBoxFlat).bg_color.a, 0.45))
Kit.check("V1 危险钮 disabled≠pressed", _sb_content_differs(dpr, dds))
db.free()
# V2 主题红线维持(Button 类型词条零接触+focus 族词条零定义)——与 _smoke_global_theme 同判据复述
var th := ThemeDB.get_project_theme()
if th != null:
Kit.check("V2 红线维持 Button 类型词条零接触", not th.get_type_list().has("Button"))
var focus_hits := ""
for t in th.get_type_list():
for nm in (th.get_stylebox_list(t) + th.get_icon_list(t) + th.get_color_list(t)
+ th.get_font_list(t) + th.get_font_size_list(t) + th.get_constant_list(t)):
if "focus" in String(nm):
focus_hits += "%s/%s " % [t, nm]
Kit.check("V2 红线维持 focus 族词条零定义%s" % ("" if focus_hits == "" else "(违例:" + focus_hits + ")"),
focus_hits == "")
# ---- 全局纹理过滤冒烟(工程设置 Nearest+平滑渐变件 LINEAR 白名单;V3)----
+3 -10
View File
@@ -64,12 +64,10 @@ func _test_grouping_closed() -> void:
for bad in ["keystone", "", "ATTACK", "aoe", "blast_mine", " heal"]:
Kit.check("清单外值「%s」→ GLOW_NONE(禁静默空光)" % bad,
UiKitScript.glow_group_for_cell_type(bad) == UiKitScript.GLOW_NONE)
# 与卡顶带 tg_band 的归组差异锚定(Q3 甲案两带并存、禁互引):blast 在格面带归群伤、
# 在卡顶带归魔法(E5 回滚既存行为)——若将来误把两带统一,本断言红
# 与卡顶带 tg_band 的归组差异锚定(Q3 甲案两带并存、禁互引):blast 在卡顶带归魔法(E5 回滚既存行为)
# ——格面带 blast→群伤由上方九型封闭循环的 TYPE_EXPECT["blast"] 承载,此处不重述
var band_blast: Color = UiKitScript.tg_band("blast_charge")
Kit.check("两带并存锚定:卡顶带 blast→TG_MAGIC(E5 回滚既存行为未动)", band_blast == UiKitScript.TG_MAGIC)
Kit.check("两带并存锚定:格面微光 blast→群伤组(与卡顶带刻意不同)",
UiKitScript.glow_group_for_cell_type("blast") == UiKitScript.GLOW_AOE)
# 色值出口:五组各取本组常量;无组/越界组号 → 全透明(不画)
Kit.check("glow_color_for_group 群伤组 = TG_GLOW_AOE",
UiKitScript.glow_color_for_group(UiKitScript.GLOW_AOE) == UiKitScript.TG_GLOW_AOE)
@@ -92,13 +90,8 @@ func _test_frozen_hue_face() -> void:
var ds: float = absf(m.s - f.s)
Kit.check("冻结色相保持:%s ΔH=%.3f° ≤0.2° 且 ΔS=%.4f ≤0.01(只动明度档)" % [g, dh * 360.0, ds],
dh <= 0.2 / 360.0 and ds <= 0.01)
# 群伤组专锚(#A61D37):H/S 与冻结逐位一致、V=0.65 档
var aoe_f: Color = FROZEN["AOE"]
# 群伤组专锚(#A61D37):V=0.65 明度档——H/S 两腿由上方逐组循环的 ΔH/ΔS 阈承载
var aoe_m: Color = UiKitScript.TG_GLOW_AOE
Kit.check("群伤组专项:H 与冻结 #FF2D55 差 %.4f°(零移动)" % (absf(aoe_m.h - aoe_f.h) * 360.0),
absf(aoe_m.h - aoe_f.h) < 0.001)
Kit.check("群伤组专项:S 与冻结差 %.4f ≤0.01(8bit 量化噪声级)" % absf(aoe_m.s - aoe_f.s),
absf(aoe_m.s - aoe_f.s) <= 0.01)
Kit.check("群伤组专项:V=0.65 明度档( provisional 域内)", absf(aoe_m.v - 166.0 / 255.0) < 0.002)
+1612 -1629
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,32 @@
# 工程改动报告——单元闸挂死守护(run_all.gd 每套件阻塞面超时)
**承接**:待办清单 2026-10-25 条目「闸与探针批上下文挂死防护——probe_gate 进程树回收与 run_all 阻塞面超时」件②(件① battle_log_view 已随迭代 38 卡⑥ 出账,probe_gate.sh 整树回收件 2026-10-28 已落;本批完成后该条目整条删除,git 历史可考)。
**车道**:工程改动直改批(改动四类,不写卡不编迭代),触及代码面走 worktree 批——分支 `runall-hangguard-20261028`,改动提交 a2710f22,并 master 后合并提交 4afe6c47。
**实证**:迭代 40 批末阻塞面挂死实拍三例(测试 `_init` 在 `quit()` 前中断,进程挂至外层超时 124),与件②同根。
## 改动—验收对照表
| 需求点 | 改动(文件:行为) | 验收测试 | 层级 | 真实入口 | 判据绑定面 | 结果 |
|---|---|---|---|---|---|---|
| 每套件截止时间守护(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖,非法值回落默认) | `run_all.gd` `_run_suite` 阻塞调用换 `OS.create_process` 非阻塞+200ms 轮询+截止判定;常量 `SUITE_TIMEOUT_*` 与 `_suite_timeout_sec()` | 机制验证:临时挂死夹具(`_init` 死循环不 quit)登记 manifest 后 `--only` 选跑,`RUN_ALL_SUITE_TIMEOUT_SEC=8` → 输出 `[run_all] FAIL …dev_hang_fixture… — 超时挂死(8s),进程树已整树回收(pid=…)`+`[run_all] 1 个套件失败`,进程退出码 1 | 闸基建 | `run_all.gd` `--script` 直跑(选跑态) | FAIL 行文本+批退出码 | 绿(夹具与 manifest 行验证后即删,未提交) |
| 超时整树回收零孤儿 | `taskkill /F /T /PID`(杀 cmd wrapper 连引擎孙进程)+等死确认 10s 宽限(确认不了报红留 PID)+临界窗口 `.code` 同删 | 杀树后 PowerShell 按命令行清点该 worktree 路径 Godot 进程=0 | 闸基建 | 同上+外部进程清点 | 清点输出 | 绿 |
| 退出码通道等价(文件通道值=原 cmd 回传值,判定形态不变) | 命令串追加 `& echo !ERRORLEVEL! > 文件`(`cmd /V:ON`);读毕即删+起跑前清陈值;缺文件/非数字 fail-closed 判红 | 负面套件 expect=1 照常判 PASS;全量 65 套件 `all green (65 suites)` 退出码 0 | 闸基建 | `run_all.gd` 全量直跑(与收口闸 02 同源) | PASS 行 exit 值+all green 行 | 绿 |
| README 超时记载换代 | `game/tests/README.md` 工具速查表 run_all 行补守护说明;Running Tests 段失真行「本地闸必须配外部 wall-clock 超时(run_all_gates.sh 已内置)」删除(该脚本实测无 timeout 包装),换代为内建守护说明 | 收口闸 06 gdd_lint/07 doc_link/08 doc_term/08c 流水线闸/08d 引用可核全绿 | 文档 | 闸机扫描 | 闸退出码 | 绿 |
| 非行为改动:文件头注重写 | 只描述现行行为;原头注「本地闸 02 闸包装外部超时:timeout 120s」与闸机现实不符,属失真句换代 | — | — | — | — | 非行为改动 |
验证集清单(`game/tests/验证集清单.md`)零变化:本批不增删不改判任何套件断言,改动全部在运行器进程机制层。
## 机制验证的 API 实证(开工探针四件,30 秒级临时件,验证后即删)
1. 现行 `OS.execute(…, out, true, false)` 实测同步等待(ping 约 3s 计时吻合)、返回退出码——第五参在 Godot 4.7.2 上不是阻塞开关,阻塞面挂死根因坐实;
2. `OS.create_process` 返回真实 Windows PID,`is_process_running` 存活期 true、taskkill 后 false;
3. `cmd /V:ON /C "… & echo !ERRORLEVEL! > 文件"` 退出码落盘通道成立(错误命令落 1);
4. `taskkill /F /T` 对 cmd→ping 树整树连坐闭环(存活计数 1→杀→0,cmd 自身 running=false)——镜像 probe_gate 已落件的同款机制。
## 闸况
全量域闸回执 2955c049(domain=full,dirty_game=False,总判定验收通过):影响面选闸判测量面(diff 全在 `game/tests/`)→ 00 冒烟/03/04 探针/05 渲染闸/11 回放确定性 SKIP;01 系统测试、02 单元与集成测试、06–08d 文档域闸、10 熵增闸全绿;09 静态扫描退出码 1=存量红(`level_map_screen.gd|quality/max-class-variables` 22 对基线登记 21 的恶化留账,还债归迭代 41 在案;指纹与入库基线一致,本批零新增)。合并后两件秒级事:`--editor --quit` 重刷类名缓存退出码 0+冒烟启动 17 项 0 失败三闸全过。
## 移交与观察
`game/tests/README.md`「套件 manifest 现况」行记 64 条,manifest 实数 65 条(62 单元+3 集成)——漂移先于本批存在(主区与任务分支同值),本批未动套件数,已登记待办清单新条目(tests/README manifest 计数行漂移订正)归文档批。
@@ -0,0 +1,76 @@
# 工程改动报告——存量断言精简(A+B 档首批删并)
**承接**:待办清单 2026-10-28 条目「存量断言精简——验证内容不变下的断言收敛」执行形态的第一波(盘点先行→逐批裁决→分波执行→件核)。
**车道**:工程改动触及代码面(`game/tests/*.gd`)走 worktree 批——分支 `assert-slim-20261028`,区 `../g-plan2-assert-slim-20261028`。
**盘点方法**:机器初筛(同文件标签近似 223 对、cond 全同 42 组 109 条、跨文件同标签、护栏类 41 条)+七路只读子代理逐域语义裁决(battle_settlement/core 三路/data_config+contracts/presentation/integration+system_test)+主会话逐条抽验复核。子代理报称候选约 150 点,掺有运行时展开数与「不同查询函数间的算术蕴含」,按本批判据线核减至 19 点。
## 一、本批判据线(删除属显式判据改判,先立判据)
只删满足 **同一读数面 + 同一输入 + 谓词字面等价或字面上是承接者的真子集** 的站点。两类不算冗余、本批不删:
1. **不同查询函数之间的算术蕴含**——删了就失去对被调函数实现本身的守卫(例:`has_unrevealed()` 与 `revealed_cells().size()<safe_total`、单表 `validate()` 直调与 `load_tables` 诊断计数)。
2. **不同夹具实例的独立前置**——各场景自守一次,删则场景不可达时判读退化(例:另一局的 `abandon()` 受理前置、每进程的表加载零 error 护栏)。
**用户裁决记录(2026-10-28 会话)**:A+B 档 19 站点照删;另 4 处候选(cross_system_flow 127/128、theme_projection 312、theme_projection 503、board_gen_reward 181)全部保留——理由是它们的蕴含依赖夹具纪律或产品码未自证的不变量,不构成本批判据下的删除凭据。
## 二、删并申报表(逐条:旧清单序号→去向)
「旧 #」=删前 `game/tests/验证集清单.md`(冻结基点 89a36822)行序号;去向已逐条写入新清单「处置」列。
| # | 旧清单 | 删的位置(文件:行) | 标签摘要 | 类别 | 承接者(保留条) | 蕴含依据 |
|---|---|---|---|---|---|---|
| 1 | #65 | system_test.gd:176 | V2d boot 无断点在档 | ① | #67 `:181` V2-s2 boot 内存默认空态 | `not sm.has_breakpoint()` 是 :181 三合取的字面成员;〔卡② mn8〕尾缀另在 :193 有落点 |
| 2 | #1519 | theme_projection:617 | V2 红线维持 Button 零接触 | ① | #1478 `:456` V1 红线 Button 零接触 | 两处 `th` 均 `ThemeDB.get_project_theme()`,同对象同谓词;源码注释自认「同判据复述」 |
| 3 | #1520 | theme_projection:624 | V2 红线维持 focus 零定义 | ① | #1479 `:464` V1 红线 focus 零定义 | 同 type list、同六词条表、同 `focus_hits == ""` |
| 4 | #649 | config_db_cross_validator:70 | 池内悬空 skill_id 被报 | ① | #650 `:71` 悬空项 category 非空 | 同 `errs`,:71 = 本条 ∧ category 非空 |
| 5 | #654 | config_db_resolver:67 | 行集返回同带全部行(2 行) | ① | #655 `:71` 行集按表序返回 | `ids` 由同一 `rows` 逐行 append,`ids==[goblin,bugbear]` ⟹ size 2 |
| 6 | #1057 | node_map_generator:313 | pure_stream 不推全局计数器 | ① | #1059 `:317` 不推计数器且产图非空 | `svc.counter() == before` 字面重复于 :317 首合取项,同对象同序 |
| 7 | #201 | round_flow_skill_pool:61 | 结算步骤流非空 | ① | #202 `:75` 步骤流含判定行 | `has_verdict` 遍历同一 `run1.stream`,空流必红 :75 |
| 8 | #614 | board_gen_reward:175 | 降级/非法产物标记互斥 | ① | #612 `:171` + #613 `:173` 两等式 | 两等式 + mode 单值 ⟹ 互斥;:175 红必伴 :171 或 :173 红 |
| 9 | #1579 | type_glow:71 | 格面微光 blast→群伤组 | ① | #1576 `:61` 九型封闭循环 | `TYPE_EXPECT["blast"] == GLOW_AOE`(:27),同函数同入参同期望 |
| 10 | #1585 | type_glow:98 | 群伤组专项 H 零移动 | ① | #1584 `:93` 冻结色相保持 ΔH≤0.2° | 循环阈严于本条(0.2° < 0.36°),本条恒弱 |
| 11 | #1586 | type_glow:100 | 群伤组专项 S ≤0.01 | ① | #1584 `:93` 冻结色相保持 ΔS≤0.01 | 同对象同阈值字面重复;:102 的 V=0.65 档另立,保留 |
| 12 | #1393 | player_copy_sweep:59 | 头部含「同库分流」 | ① | #1338 battle_narrator:96 渠道句长串 | 同一 `node_curio_header()` 纯函数,长串含短串 |
| 13 | #1398 | player_copy_sweep:75 | 图例封印行存在 | ① | #1399 `:76` 封印行含「标空」 | 空行 ⟹ `seal_row[2] if size>2 else ""` 取空串 ⟹ :76 必红 |
| 14 | #168 | roll_multiplier:131 | nat20 种子可达 | ① | #169 `:132` 行留档 roll=20+crit | `crit_seed>0` 与 `crit_rows.roll==crit_roll` 同一扫描同真同假 |
| 15 | #324 | seal_polarity:443 | 构造触达:标中标空入队 | ① | #323 `:442` 极性路由种子可达 | `_polarity_probe_accepts` 合取项(:249-252),可达即三腿同真 |
| 16 | #325 | seal_polarity:444 | 构造触达:挖危险格 ≥1 | ① | 同上 | 同上(:253) |
| 17 | #326 | seal_polarity:445 | 构造触达:挖安全格 ≥1 | ① | 同上 | 同上(:253) |
| 18 | —(口径外) | smoke_boot.gd:69 | run_scope 非空 | ① | `:70` run_scope 在树 | `!= null` ⊂ `!= null ∧ is_inside_tree()`;GDScript `and` 短路,无空引用崩溃风险 |
| 19 | —(口径外) | smoke_boot.gd:72 | _start_overlay 已装配 | ① | `:73` 启动态菜单可见 | `!= null` ⊂ `!= null ∧ visible` |
**两条改判(标签变更,判据不变)**
- seal_polarity:442 标签追加「缺腿:%s」,把第 15/16/17 行的三腿诊断信息折进保留条;三腿取值仍读同一 `probe`,与原三条的语义强度逐字相同。
- roll_multiplier:132 标签追加「种子可达 seed=%d」,把第 14 行的 seed 读数折进保留条。
**随行清理**:theme_projection:614-625 的 `var th` 与 focus 扫描循环随其承载的两条断言一并删除(该循环只为已删断言供数据);type_glow 的 `aoe_f` 局部量随 H/S 两条删除后无消费者,一并删;两处注释改写为指认真正承载该事实的保留条。
## 三、件核(验证项目覆盖不变)
| 核对项 | 结果 |
|---|---|
| 冻结清单条数 | 1604 → **1587**(−17;smoke_boot 两站点本不在清单源列,条数不受其影响) |
| `extract_checks.py --diff` | PASS——标签/cond/计数三对齐,零差异 |
| 被删 19 条标签全仓残留 grep(`tests/`+`design/`+`reports/`) | 零悬挂引用(唯一命中的「run_scope 非空」是 system_test 自己的 V2e 在册条,非本批对象) |
| 承接指认 | 15 条保留行的「处置」列逐条回填去向(旧序号+蕴含依据+批次名),新清单在册 |
| 判据等价复验 | 由代码域闸承担(见闸况)——本批不新增行为,删后被承接行为以保留断言复验 |
| 取证方式 | 未使用变异法与撤销类取证(2026-10-24/28 用户裁);删除凭据=上表逐条申报+承接者指认 |
## 四、闸况
全量域闸(域=full,`dirty_game=false`,`all_green=true`,总判定「闸况与基线一致,无新增红」)回执 commit `4fbacc43`:01 系统测试、02 单元与集成测试、06/06b/07/08/08c/08d 文档域闸、10 熵增闸全绿;09 静态扫描退出码 1=存量红(`game/presentation/views/level_map_screen.gd|quality/max-class-variables` 22 对基线登记 21 的恶化留账,还债归迭代 41 在案;指纹 `8467b4588a20` 与入库基线一致,本批零新增);00 冒烟、03/04 探针、05 渲染闸、11 回放确定性由影响面选闸判 SKIP(本批 diff 全在 `game/tests/` 测量面)。
**本批复验缺口一条(合回后已补)**:第 18/19 两行改的是冒烟宿主自身 `smoke_boot.gd`,而 00 冒烟测试恰被影响面选闸跳过——该两站点在批末跑批中没有执行证据,只有静态蕴含论证(GDScript `and` 短路,保留条 `!= null and …` 在空值上判 false 不崩)。合回 master 后按 §3.9 code 批两件秒级事跑 `python tools/run_smoke.py`=三闸全过、`[smoke] 摘要:15 项检查,0 项失败`(原 17 项检查删两条后为 15 项计数),缺口就此闭合。第 1–17 行所在套件的复验由 01/02 全绿承担。
跑批踩坑一条备案:闸日志首跑写在仓库根,触 10 熵增闸「根目录文件越界」判红——我的临时件写错位置,非批次改动引发;日志挪到仓库外重跑后 10 归绿,代码面零改动,回执按同一 commit 重生成。
## 五、移交与观察(后续批次账)
1. **类别② 在本仓基本不成立**:全仓钉内部路径/私有字段/中间态的断言约 52 条,被入口**严格承载 0 条**,约 51 条因覆盖入口打不到的边界/反例载荷必须保留(`fx.dispatch` 逐事件、`pending_ap_delta` 兑现时机、`_intent_charge`);且 §8.4 本身要求判据绑提交面。后续批次的可精简空间只在类别①。
2. **前置护栏不可删**:`load_ok and diagnostics_count("error")==0` 全域 8 处属每进程 fail-fast,删致 `if load_ok:` 静默跳过→假绿,属负收益。
3. **静默通道在冻结口径之外**:`Kit.check_quiet` 静态站点实测 **407 个**,提取器 `CALL_RE = check\(` 匹配不到该形态(test-conventions:38 已备案该工具局限)——清单条数与〔尾缀〕grep 均覆盖不到。因此「循环 quiet 与聚合条双钉」两处真冗余(node_map_growth 47/51 vs #1073/74、node_map_generator 163 vs #fv9 聚合条)删的是口径外站点,不减 1604 这个数,且聚合侧带〔fv8〕〔fv9〕验收尾缀,本批不动;若要收该账,先做提取器改版(工程批)。
4. **恒真断言另立一账**(属判据缺陷,非重复,本批未动):feat_vouchers:51 `deck_refresh_before == 0 or true`、treasure_effects:263 `... or true`、level_shop_economy:593 与 :696 `green_min_hp > 0`(初值 99999、仅 hp>0 分支取 min,恒真);system_contracts:67 标签称「互异」而 cond 只查非空,属名实不符需改判据。建议按 §8.4 走「判据绑不上即停卡呈报」通道单独裁决。
5. **未采的删并形态**:C 档「手写族→表驱动循环合并」(level_gen_params 逐类负向 10→1、rng_stream_boot 8 向量→1、player_copy 禁词族、standee_fx 三入参、ui_font 字形集、forge_workbench 双行)静态可再省约 25~35 点,代价=失败定位改用违例明细串(仓内已有 `forge_table_test:181-189`、`table_structure_guard:260/299` 范式);D 档「是否同一验证项目」待owner 认账项(theme:215/217 vs type_glow:148/150、level_shop_economy 现行图跨表覆盖簇 vs 〔v11〕扩面段的尾缀义务)。
6. **example_test.gd** 两条硬编码占位断言在盘且随清单在册(旧 #S167),不进生产 manifest(run_all.gd:79-80 明示)——整件退役与否属样板件裁决,不入本账。
7. 本批不动 `game/tests/README.md`(断言双口径行仍成立)与任何 `design/` 文件。
+29 -10
View File
@@ -52,6 +52,8 @@ LEDGER_RULE_DATE = (2026, 10, 21)
LEDGER_HEADING_RE = re.compile(r"^#{2,4}\s*.*(验收对照表|改动[—\-]验收)")
LEDGER_REQUIRED = ("需求点", "改动", "验收测试", "真实入口")
LEDGER_PROOF_MARKS = ("撤销", "证明")
BINDING_RULE_DATE = (2026, 10, 28) # req-card §8:换代为判据绑定面列,撤销/变异证明列同日禁列
BANNED_PROOF_MARKS = ("撤销证明", "变异证明")
AUDIT_MARK = "验收审计"
DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}")
@@ -451,7 +453,7 @@ def card_date(path):
return tuple(int(x) for x in found[-1].split("-"))
def check_ledger(text, path, issues):
def check_ledger(text, path, when, issues):
"""C3 改动—验收对照表结构检查(生效 2026-10-21)。
只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空。
@@ -491,13 +493,29 @@ def check_ledger(text, path, issues):
"缺「%s」列" % name))
return
idx[name] = hit
proof_hit = next((k for k, cell in enumerate(header)
if any(mark in cell for mark in LEDGER_PROOF_MARKS)), None)
if proof_hit is None:
issues.append((str(path), header_line, "验收对照表缺列",
"缺「撤销证明」列"))
return
idx["证明"] = proof_hit
banned = next((cell for cell in header
if any(mark in cell for mark in BANNED_PROOF_MARKS)), None)
binding_hit = next((k for k, cell in enumerate(header)
if "判据绑定面" in cell), None)
if when >= BINDING_RULE_DATE:
if banned is not None:
issues.append((str(path), header_line, "验收对照表禁列",
"「%s」列已随 2026-10-24/28 两裁禁列(req-card §8.4,"
"验收断言绑真实入口+判据绑定面)" % banned))
return
if binding_hit is None:
issues.append((str(path), header_line, "验收对照表缺列",
"缺「判据绑定面」列(2026-10-28 判据口径换代,req-card §8)"))
return
idx["绑定面"] = binding_hit
else:
proof_hit = next((k for k, cell in enumerate(header)
if any(mark in cell for mark in LEDGER_PROOF_MARKS)), None)
if proof_hit is None:
issues.append((str(path), header_line, "验收对照表缺列",
"缺「撤销证明」列"))
return
idx["证明"] = proof_hit
rows = 0
for i in range(header_line, len(lines)):
@@ -531,7 +549,8 @@ def check_cards(root, issues):
C1 判据:「关联文档同步面」表内,目标格指向 design/ 或根规则文件、而义务格仍写
「随批」者红(2026-10-17 req-card §6 改口径后,这类点只能列 reports/ 反哺点清单)。
C2 判据:全文既无「设计事实源」也无「唯一事实源」者红(两种写法都与库内实态对齐)。
C3 判据:有「改动—验收对照表」节;表头含需求点/改动/验收测试/真实入口/证明列;
C3 判据:有「改动—验收对照表」节;表头含需求点/改动/验收测试/真实入口+证明列
(2026-10-28 起换代为「判据绑定面」列,并禁「撤销证明/变异证明」列);
至少一行;必填格非空;全文含「验收审计」声明。语义覆盖不在此判。
"""
dirn = root / CARD_DIR
@@ -550,7 +569,7 @@ def check_cards(root, issues):
"须含「设计事实源」或「唯一事实源」声明句(req-card §2 三选一,"
"GDD 节+版本号/样张+art-bible 版本/工程形态证据指认)"))
if when >= LEDGER_RULE_DATE:
check_ledger(text, path, issues)
check_ledger(text, path, when, issues)
if AUDIT_MARK not in text:
issues.append((str(path), 1, "独立验收审计未声明",
"须在卡内写明独立验收审计入口与报告文件(2026-10-21 起,"
-159
View File
@@ -1,159 +0,0 @@
#!/usr/bin/env python3
"""撤销证明自动化工具:一条命令完成「改前绿 → 真撤销复红 → 还原复绿」三连并出回执。
撤销证明的判据(test-conventions §8.4):只撤销该改动、跑能覆盖它的最窄测试、对应断言
必须变红、还原后复绿;撤销后仍绿=断言没钉在改动上,判缺漏。本工具把这三连机械化——
合法性属于「真撤销」(回退本批真实改动),全项目禁做变异类测试(改断言极性、置空状态、
改名键、删调用行等人为制造红=伪造证据)。
用法:
python tools/undo_proof.py --cmd "<最窄测试命令>" --revert-commit <该批提交>
python tools/undo_proof.py --cmd "<最窄测试命令>" --revert-files <f1,f2> --base <rev>
可选 --out <json 路径> 落回执;--timeout <秒> 限制单次测试命令时长(缺省 3600)。
--revert-commit:按该笔提交触过的文件集回退到其父提交版本(改动须已提交在分支上)。
--revert-files+--base:把指定文件回退到 <base>(默认 HEAD)版本——适合改动尚未单独
成笔、混在当前尖里的场景。
测试命令在仓库根以 shell 执行;单套件直跑形态见 game/tests/README.md(测试工具手册)。
流程与退出码:
① 改前基线:测试命令必须绿(红=改前就红,无证明意义,中止)。
② 真撤销:指定文件回退到基准版本 → 重跑命令,必须红(红=断言钉在该改动上)。
③ 还原:文件恢复到 HEAD → 重跑命令,必须绿。
④ 任何一步异常都先还原再退出;三步全过=退出码 0 并打印回执,否则退出码 1。
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
REPO_DEFAULT = Path(__file__).resolve().parent.parent
REPO = REPO_DEFAULT
def run(args: list[str], check: bool = True) -> str:
r = subprocess.run(["git", "-C", str(REPO)] + args,
capture_output=True, text=True, encoding="utf-8", errors="replace",
timeout=60)
if check and r.returncode != 0:
sys.stderr.write("git %s 失败:%s\n" % (" ".join(args), (r.stderr or "").strip()))
sys.exit(1)
return (r.stdout or "").strip()
def run_cmd(cmd: str, timeout: int) -> int:
r = subprocess.run(cmd, shell=True, cwd=str(REPO), capture_output=True,
text=True, encoding="utf-8", errors="replace", timeout=timeout)
if r.returncode != 0:
tail = "\n".join((r.stdout or "").splitlines()[-15:] or ["(无输出)"])
sys.stderr.write(" | 测试命令失败(exit=%d),输出尾部:\n%s\n" % (r.returncode, tail))
return r.returncode
def _in_rev(rev: str, f: str) -> bool:
r = subprocess.run(["git", "-C", str(REPO), "cat-file", "-e", "%s:%s" % (rev, f)],
capture_output=True, timeout=30)
return r.returncode == 0
def apply_rev(rev: str, files: list[str]) -> None:
# 逐文件:基准版本存在 → 恢复到该版本;基准没有(该批新增的文件)→ 从工作区删除。
for f in files:
if _in_rev(rev, f):
run(["checkout", rev, "--", f])
else:
subprocess.run(["git", "-C", str(REPO), "rm", "-q", "-f", "--", f],
capture_output=True, text=True, timeout=30)
def main() -> None:
ap = argparse.ArgumentParser(description="自动三连出撤销证明红绿回执")
ap.add_argument("--cmd", required=True, help="最窄测试命令(shell 形,仓库根执行)")
ap.add_argument("--revert-commit", default=None, help="该批提交:按其触过的文件集回退到父版本")
ap.add_argument("--revert-files", default=None, help="逗号分隔的文件清单(与 --base 配用)")
ap.add_argument("--base", default="HEAD", help="回退基准版本(默认 HEAD)")
ap.add_argument("--timeout", type=int, default=3600, help="单次测试命令时长上限(秒)")
ap.add_argument("--repo", default=None, help="仓库/worktree 路径(缺省=本脚本所在仓库)")
ap.add_argument("--out", default=None, help="回执 JSON 落盘路径")
args = ap.parse_args()
global REPO
REPO = Path(args.repo).resolve() if args.repo else REPO_DEFAULT
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
if bool(args.revert_commit) == bool(args.revert_files):
ap.error("--revert-commit 与 --revert-files 二选一")
if args.revert_commit:
files = [x.strip() for x in run(
["-c", "core.quotepath=false", "show", "--name-only", "--format=",
args.revert_commit]).splitlines() if x.strip()]
base = "%s^" % args.revert_commit
if not files:
sys.stderr.write("[撤销证明] 提交 %s 未触碰任何文件,无可撤销\n" % args.revert_commit)
sys.exit(2)
else:
files = [x.strip() for x in args.revert_files.split(",") if x.strip()]
base = args.base
dirty = run(["-c", "core.quotepath=false", "status", "--porcelain", "--"] + files)
if dirty:
sys.stderr.write("[撤销证明] 指定文件带未提交改动,撤销与还原的基准不清,先提交:\n%s\n" % dirty)
sys.exit(2)
steps: list[dict[str, object]] = []
result = "pass"
def step(name: str, expect: str, code: int) -> bool:
ok = (code == 0) if expect == "绿" else (code != 0)
steps.append({"step": name, "expect": expect, "exit": code, "ok": ok})
print("[撤销证明] %s:期望%s,实测 exit=%d → %s"
% (name, expect, code, "符合" if ok else "不符合"))
return ok
print("[撤销证明] 对象文件 %d 个;基准 %s;命令:%s" % (len(files), base, args.cmd))
try:
if not step("① 改前基线", "绿", run_cmd(args.cmd, args.timeout)):
result = "fail(改前就红——先修绿再做撤销证明)"
else:
apply_rev(base, files)
try:
if not step("② 真撤销复红", "红", run_cmd(args.cmd, args.timeout)):
result = "fail(撤销后仍绿=断言没钉在该改动上,判缺漏)"
else:
apply_rev("HEAD", files)
if not step("③ 还原复绿", "绿", run_cmd(args.cmd, args.timeout)):
result = "fail(还原后未复绿——还原面有残留,查工作区)"
finally:
apply_rev("HEAD", files)
except subprocess.TimeoutExpired:
result = "fail(测试命令超时)"
finally:
# 兜底还原:无论走到哪一步,退出前把对象文件恢复到 HEAD 态
apply_rev("HEAD", files)
residue = run(["-c", "core.quotepath=false", "status", "--porcelain", "--"] + files)
if residue:
result = "fail(退出前工作区仍有对象文件残留改动,手工核对)"
print("─" * 56)
print("[撤销证明] 结论:%s" % result)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
json.dump({"cmd": args.cmd, "base": base, "files": files,
"steps": steps, "result": result}, f, ensure_ascii=False, indent=1)
print("[撤销证明] 回执已落 %s" % args.out)
sys.exit(0 if result == "pass" else 1)
if __name__ == "__main__":
main()
+10 -20
View File
@@ -2,9 +2,9 @@
本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。
#### 存量断言内部直查全量归位——判据达标专项批(2026-10-28 登记,来源=会话提及)
#### 存量断言精简——验证内容不变下的断言收敛(2026-10-28 登记,来源=会话提及)
**实况**:目标=unit/integration 存量断言级内部直查 89 处(grep 口径,2026-10-28 本会话实测)按 test-conventions §8.2 新判据(145c1865 落文)全部改钉公开可观测面。证据=§8.2 受控例外条文+本会话 grep 计数;样板=treasure_effects_test(e62e90c8 试点批,12 处改钉零产品码全绿)。执行形态=工程改动 worktree 批(纯测试维护零行为改动,不写卡不编迭代):盘点先行(逐套件定四类改法——属性直换/公开面等价式/真实路径重塑/公开面对比——并标出需产品码只读访问器的呈报项)→ 按域分波每波闸 → 收尾件核(断言级直查 grep=0)。边界:只清断言级(`Kit.check` 行内 `._` 命中),setup 直调写口构造状态不动(§5.4 惯例);断言标签逐字不动(验证集清单锚);禁放宽/删断言;需访问器项逐处呈用户裁(产品码边界);非 quiet 断言改判同步验证集清单对账。时机=排在迭代 41 之前,与波 0 gdstyle 基线刷新互不影响(tests 不在扫描面)。
**实况**:目标=减少现有断言数量、更精简,配合 §8.2 接口测试导向(对外方法、功能行为验证),前提=验证清单的验证项目覆盖逐条不变。证据=判据达标专项批已完成(本清单已销号条目,59 改钉+35 例外——全部断言已钉公开可观测面,每条断言的验证对象已显式化,做减法有了干净基线);验证集清单 1604 静态调用点为对照基准。口径界定:验证项目≠check 调用点——验证项目(卡片验收条目/GDD 登记验证面)覆盖逐条不降,check 调用点可降;可删并的两类=①同一行为被多条断言重复钉(同族合并)②钉内部路径细节而该行为已被入口断言严格承载(逐条申报承接者)。删除属显式判据改判:逐条申报理由+承接者指认(写入验证集清单「处置/去向」列),禁批量拍删;删后被承接行为以保留断言复验(套件绿),禁变异法。执行形态=工程改动 worktree 批:盘点先行(逐套件标冗余候选两类清单)→ 用户逐批裁决删并清单 → 分波执行每波闸 → 件核(验证项目覆盖不变+清单对账 PASS+条数下降留痕)。盘点与第一波执行凭据:机器初筛(同文件标签近似 223 对/cond 全同 42 组 109 条/护栏类 41 条)+七路只读语义裁决+逐条抽验,判据线收紧为「同一读数面+同一输入+谓词字面等价或字面是承接者真子集」,候选由约 150 点核减至 19 点;第一波(A+B 档 19 站点,其中 17 在册+`smoke_boot.gd` 2 点口径外)已合回——删并申报表与承接者逐条指认 `reports/工程改动报告-存量断言精简AB档-2026-10-28.md`,清单 1604→1587 且 `--diff` 对账 PASS、15 条保留行「处置」列去向在册,全量域闸回执 commit `20c13c3a`(domain=full/all_green/09 静态扫描存量红零新增),合并提交 65054b1d。余账四条:①类别② 实测不成立(全仓内部面断言约 52 条被入口严格承载 0 条,后续波次只在类别① 内找)②C 档手写族改表驱动循环合并(静态约再省 25~35 点,代价=定位改用违例明细串)③D 档「是否同一验证项目」待 owner 认账四处(本批用户裁全保留)④恒真与名实不符断言五条属判据缺陷另账,走 §8.4 呈报通道非精简账;另 静默收集通道 `Kit.check_quiet` 静态站点 407 个在冻结口径外(test-conventions:38 已备案工具局限),要收该账先做提取器改版。
**改动面**:游戏代码
@@ -397,33 +397,23 @@
**状态**:待写卡(备选)
#### 闸与探针批上下文挂死防护——probe_gate 进程树回收与 run_all 阻塞面超时(2026-10-25 登记,来源=批次移交)
#### tests/README manifest 计数行漂移订正——「64 条」对实际 65 条(2026-10-28 登记,来源=批次移交)
**实况**:证据=迭代 36 批末闸机观测(非本批引入)。原登记两件存量噪声,件①(battle_log_view.gd:17 类型声明错致运行期 TypedArray 错误、日志失真)已随迭代 38 卡⑥ 收口出账(git 提交 ce49a8ee),本条只剩件②:`level_shop_economy_test` 在闸批量上下文存在挂死形态(直跑恒绿),`tools/probe_gate.sh` 的 per-suite timeout 只杀 wrapper 不杀引擎子进程,孤儿泄漏会殃及后续全部 Godot 启动(批末四次闸跑批中断的根源,处置=清杀泄漏进程+删重建 `game/.godot`)。挂死防护=闸壳/测试基建,按改动四类属**工程改动走直改通道**。
- 直改落点:tools/probe_gate.sh(per-suite 进程树回收)+game/tests/run_all.gd(OS.execute 阻塞面超时守护)——测试基建直改。**2026-10-28 用户裁四批全做,probe_gate 件已落**(run_suite 整树回收:启动记 WINPID → 超时先 `taskkill /T` 杀全树再 `kill -9` 收 MSYS 侧〔顺序颠倒会让 /T 走不到已死父进程〕→ 标志文件判超时〔taskkill 强杀后 MSYS wait 可能报 0〕;默认五段实跑全绿+超时/正常双路径机制验证通过)。run_all.gd 件属代码面(game/tests),按车道铁律留待触及测试基建的 worktree 批顺带,本条目保持待执行。**2026-10-28 迭代 40 批末补实证(阻塞面挂死实拍)**:迁移类改动的回退使测试 `_init` 在 `quit()` 前中断(红行形态 `Invalid call. Nonexistent function`),进程挂至外层超时 124——卡① wr1/wr4/wr9 取证轮三例复现,与件②同根(阻塞面无超时守护),实证链在案(实现报告 §三第 1 条)。
**实况**:证据=单元闸挂死守护批收尾实数(合并提交 4afe6c47,报告 reports/工程改动报告-单元闸挂死守护-2026-10-28.md)——`game/tests/run_all.gd` manifest 实为 65 条(62 单元+3 集成,grep '{"path"' 计数),`game/tests/README.md`「套件 manifest 现况」行仍记「64 条(61 单元+3 集成,2026-10-28 实数)」。漂移先于该批存在(主区与任务分支同值),该批未动套件数不属其 scope,登记归文档批订正。
**改动面**:工具代码
**改动面**:文档
**状态**:待执行
#### undo_proof.py 退役——撤销证明禁令生效后的死工具(2026-10-28 登记,来源=批次移交)
#### 主角棋盘小人像素管线重产——原「美术资产链路审计两项 P0」之 P0②(2026-10-09 登记,来源=审计发现)
**实况**:证据=test-conventions §8.4 现行口径(2026-10-24/2026-10-28 用户两裁:禁变异类测试+禁撤销类取证,验收唯一主判据=真实入口断言当轮执行并通过;AGENTS.md 红线「验收闭合」与 req-card SKILL 第 8 条同批换代)。`tools/undo_proof.py`(159 行,挂死防护批产物)为撤销证明的自动化载体(三连红绿回执并自还原),服务对象已被禁令整体废止——工具本体退役删除。同族残留顺带裁:`.zcode|.qoder/skills/iter/evals/files/mock-card-iter40-1.md` 表头「撤销/变异证明」字样(两份,反向残留);`tools/pipeline_gate.py` 是否加「变异测试/撤销证明」短语级扫描防复发(**短语级**——按「变异」单词扫会命中地形变异等玩法词,实测假红面大)。
**实况**:主角棋盘小人未走像素管线(插值糊图),重产事项未在任何登记处在册;审计报告 HTML 件从未进版本库、现已删除,现状以本条正文为准。2026-10-13 用户令随「UI 美化三层诊断」登记:本条属第三层「观感提升」,Alpha 阶段需完成。原 P0①(文档面:两处引用已丢失的冻结样张路径)2026-10-28 按用户裁乙销号——art-bible §1.4 冻结记录与 art-status-checklist P0 样张行删路径、留「产物原件已丢失」,机检报告凭据(git 提交 7a847c7)保留。
**改动面**:工具代码
**缺口**:还差美术面的重产(按像素管线重画主角棋盘小人),归美术设计段。
**状态**:待执行
**改动面**:美术素材
#### 美术资产链路审计两项 P0 — 待用户裁决(2026-10-09 登记,来源=审计发现)
**实况**:**P0①(文档面)**:`design/art/art-bible.md` 第 68/527 行与 art-status-checklist 第 13 行至今仍引用已丢失的冻结样张 sample-batch-v3-final-2026-09-30.png(磁盘实测零命中),死指涉待修复。**P0②(美术面)**:主角棋盘小人未走像素管线(插值糊图),重产事项未在任何登记处在册。审计报告 HTML 件从未进版本库、现已删除,两项 P0 的现状以本条正文为准。裁=修复死指涉+登记重产,或判过期随旧管线退役。2026-10-13 用户令随「UI 美化三层诊断」登记:本条属第三层「观感提升」,Alpha 阶段需完成。
**缺口**:还差用户的路线裁决(修复死指涉+登记重产,还是判过期随旧管线退役)——裁决后文档面修复与美术重产登记各按轨推进。
**改动面**:文档+美术素材
**状态**:待文档设计+待美术设计
**状态**:待美术设计
#### 技术债观察登记——长函数余量与 lambda 连接防回归(2026-10-15 登记,来源=批次移交)