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:
@@ -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: 提交**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user