docs(plan): 补 Step3b 断言 push_error + 订正绕缓存法在类注册后失效

Task 1 评审实测发现两处计划缺陷:
① 断言 ④ 的数字部分对「add_int 拒 pct」是空的——删掉整个拒绝块 ④ 仍返回 13.0,
   因为 ADD_INT 的 match 分支不读 pct_prod。拒绝行为唯一可观测证据是 push_error,
   补 Step 3b 用唯一标记值 + get_editor_errors 单独断言。
② GDScript.new()+source_code+reload() 在该类被注册为全局类之后同样报
   hides a global script class——Task 1 首次运行因尚未注册才侥幸通过,
   后续任务照抄会失败。改为先剥离 class_name 行,或直接调已注册的全局类。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 10:53:17 +08:00
co-authored by Claude Opus 5
parent ecf765d03e
commit cd3618c066
+36 -2
View File
@@ -9,7 +9,14 @@
**Tech Stack:** Godot 4.7.1 Mono / GDScript / 纯 JSON 数据驱动 / godot-mcp-pro(公式为纯函数 → `execute_editor_script` 单元断言;接线为运行时 `execute_game_script` 实测)。 **Tech Stack:** Godot 4.7.1 Mono / GDScript / 纯 JSON 数据驱动 / godot-mcp-pro(公式为纯函数 → `execute_editor_script` 单元断言;接线为运行时 `execute_game_script` 实测)。
> **规范**:提交中文;纯数据驱动,文件缺失 `push_error` 不静默回退。spec`docs_dev/specs/2026-07-31-player-attributes-design.md`。 > **规范**:提交中文;纯数据驱动,文件缺失 `push_error` 不静默回退。spec`docs_dev/specs/2026-07-31-player-attributes-design.md`。
> **已知工具坑**① `validate_script` 对任何带 `class_name` 的脚本必然假阴性(报 `hides a global script class`)—— `attribute_formula.gd` 属此类,验证改用 `GDScript.new()` + 从磁盘读 `source_code` + `reload()``player_stats.gd` / `player_manager.gd` / `spell_evaluator.gd` / `combat_manager.gd` 无 `class_name`,可正常 `validate_script`。② `@tool` 编辑器类在 dock 里被缓存,`load(...).new()` 拿到旧类,绕法同上。③ `execute_editor_script` 静默阻止运行时 `FileAccess.WRITE`。 > **已知工具坑**
> ① `validate_script` 对任何带 `class_name` 的脚本必然假阴性(报 `hides a global script class`)—— `attribute_formula.gd` 属此类;`player_stats.gd` / `player_manager.gd` / `spell_evaluator.gd` / `combat_manager.gd` / `attribute_tab.gd` / `designer_panel.gd` 无 `class_name`,可正常 `validate_script`。
> ② **`GDScript.new()` + `source_code` + `reload()` 这个绕法,在该类被引擎注册为全局类之后同样会失败**(同一条 `hides a global script class`)。Task 1 首次运行时因尚未注册而侥幸通过,**后续任务不能照抄**。正确做法二选一:
> - **剥离 `class_name` 行再 `reload()`**`S.source_code = src.replace("class_name AttributeFormula\n", "")``@tool` 编辑器类同理剥自己的 `class_name`);
> - 或**直接调用已注册的全局类**`AttributeFormula.compute(...)`(只验证已落盘版本时更省事)。
> ③ `@tool` 编辑器类在 dock 里被缓存,`load(...).new()` 拿到旧类,绕法同 ②。
> ④ `execute_editor_script` 静默阻止运行时 `FileAccess.WRITE`。
> ⑤ 新增 `.gd` 后 `.gd.uid` 不会自动出现,需 `EditorInterface.get_resource_filesystem().scan()` 触发扫描才生成;项目约定二者一并提交。
> **关键既有事实**`spell_evaluator.gd:332` `var max_ops: int = MAX_OPS_PER_CPU * 5``MAX_OPS_PER_CPU = 40``:8`)。`player_manager.gd:7` `const MOVE_SPEED: float = 200.0``:45` 消费,`:88` `_cast_interval = core.cast_interval if core else 0.5`。`combat_manager.gd:257` `_rebuild_wand()``:267-271` 为 `mana_leech` 重算段。`player_stats.gd``hp_max` :10、`cpu_limit` :21、`armor` :22、`resistance` :23、`_ready()` :33、`reset_for_run()` :125、`get_save_data()` :135、`load_save_data()` :138。`CompiledDeck` **没有** `cpu_limit` 字段(只有 `feature_tags`),故 `spell_evaluator` 侧不需要新增字段。 > **关键既有事实**`spell_evaluator.gd:332` `var max_ops: int = MAX_OPS_PER_CPU * 5``MAX_OPS_PER_CPU = 40``:8`)。`player_manager.gd:7` `const MOVE_SPEED: float = 200.0``:45` 消费,`:88` `_cast_interval = core.cast_interval if core else 0.5`。`combat_manager.gd:257` `_rebuild_wand()``:267-271` 为 `mana_leech` 重算段。`player_stats.gd``hp_max` :10、`cpu_limit` :21、`armor` :22、`resistance` :23、`_ready()` :33、`reset_for_run()` :125、`get_save_data()` :135、`load_save_data()` :138。`CompiledDeck` **没有** `cpu_limit` 字段(只有 `feature_tags`),故 `spell_evaluator` 侧不需要新增字段。
--- ---
@@ -147,7 +154,34 @@ for c in cases:
_mcp_print("FAILS=%d %s" % [fails, str(bad)]) _mcp_print("FAILS=%d %s" % [fails, str(bad)])
``` ```
Expected: `FAILS=0 []` Expected: `FAILS=0 []`
> 预期输出中会伴随两条 `push_error` 文本(④ 的 add_int 拒 pct、⑥b 的未知 combine)—— **那是被断言的正确行为**,不是错误。
> ⚠️ **断言 ④ 的数字部分对「拒绝 `pct`」是空的**(Task 1 评审实测发现):把整个拒绝块删掉,④ 仍返回 13.0 —— 因为 `ADD_INT` 的 match 分支根本不读 `pct_prod`。数字只证明了「`pct` 之后的 `flat` 仍能累加」。**拒绝行为唯一的可观测证据是 `push_error`**,必须单独断言,否则意图②无人守卫。
- [ ] **Step 3b: 断言 `push_error` 真的发出(补 ④ 的空缺)**
用**唯一标记值**发起调用,再从编辑器错误日志里找它 —— 标记值确保读到的不是历史遗留日志。
Run: godot-mcp-pro `clear_output`,然后 `execute_editor_script`
```gdscript
var S := GDScript.new()
# ⚠️ 必须剥掉 class_name:该类此时已被引擎注册为全局类,带 class_name 的内存副本会报
# "hides a global script class"Task 1 首次运行时尚未注册才侥幸通过)
S.source_code = FileAccess.get_file_as_string("res://scripts/domain/attribute_formula.gd") \
.replace("class_name AttributeFormula\n", "")
if S.reload() != OK:
_mcp_print("PARSE_FAIL"); return
# 三条各带唯一标记,逐一触发报错路径
S.compute(0.0, [{"mode":"pct","value":0.123456}], {"combine":"add_int","hard":50.0})
S.compute(1.0, [], {"combine":"zzzprobe9182","hard":0.0})
S.compute(1.0, [{"mode":"zzzmode9182","value":1.0}], {"combine":"hybrid","hard":0.0})
_mcp_print("probes_fired")
```
再 Run: godot-mcp-pro `get_editor_errors`,断言三条标记均出现:
- `add_int 属性不接受 pct 加成(value=0.123456`
- `未知 combine「zzzprobe9182」`
- `未知 mode「zzzmode9182」`
Expected: 三条全部命中。**任一缺失即说明对应的报错路径未执行** —— 那是真缺陷,不是日志问题。
- [ ] **Step 4: 提交** - [ ] **Step 4: 提交**