diff --git a/design/qa/iter28-实现报告-2026-10-15.md b/design/qa/iter28-实现报告-2026-10-15.md new file mode 100644 index 00000000..a3ee3e7c --- /dev/null +++ b/design/qa/iter28-实现报告-2026-10-15.md @@ -0,0 +1,188 @@ +# 迭代 28 实现报告——启动装配与生命周期验收补全批(六任务全量入库) + +**日期**:2026-10-15 **分支**:iter28-bootlife-20261015(worktree `g-plan2-iter28-bootlife`,基点 39c6544) +**需求卡片**:`design/research/需求卡片-启动装配与生命周期验收补全批-2026-10-15.md` v1.0(零待裁点,用户预批连做全批,不入 git) +**一句话**:把「启动后玩家该看到什么、状态对象何时该活何时该死」从口头约定变成**可执行验收函数**,并逐条用变异实验证明这些函数真能抓住对应缺陷。 + +--- + +## 一、六卡交付 + +| 卡 | 内容 | 提交 | +|---|---|---| +| T0 | worktree + 基线三查 + 运行期树序实采(临时探针两枚,采完即删) | 无产物(定义面已在前置笔 39c6544) | +| T1 | 装配语义合同单点化 + 装配顺序修复 + V2 断言重写 | `6f71509` | +| T2 | boot 链路时序后置条件(`_smoke_boot_chain`) | `a35581e` | +| T3 | 生命周期存活性矩阵(`_smoke_lifecycle_matrix`) | `77075e6` | +| T4 | 渲染面内容断言三件(探针自检 + 墨色判据 + 启动态菜单 golden) | `67342c6` | +| T5 | 变异验证(横切,六次注入;**零提交——注入即还原**) | 无提交(见 §五) | +| T6 | 规范三处落文 + 机械面文档同步闸 | `db1bf42` | + +### T0 开工基线与事实复核 + +基线冻结:冒烟 **1086/0**(roadmap 与卡片所记 1081 系 iter27 补遗批 `d5eea25` +V8×5 未回写,以实跑为准,**已随本批回写**)、run_all **27 套**全绿、probe_gate --click 全绿残留 0。 + +运行期实采修正了计划书与卡片的三处描述偏差(按「诊断订正」惯例以代码/运行期为准): + +1. **屏壳是容器、不当「顶层可见层」候选**(卡片诊断订正①,运行期实证):三屏壳根均 `MOUSE_FILTER_IGNORE` 纯容器,camp 壳 boot 态 `visible=true` 但成员恒隐;裸树遍历自顶向下会先命中透明壳。断言改为遍历合同**内容层**候选集。 +2. **`_row_pacer` 句柄数**(卡片诊断订正②):计划书称 6 视图句柄,实查 5 + 2 Callable。 +3. **`main.gd:378` 十二模态登记数组**形态与卡片描述有差异——合同常量按运行期实采自行定义为 **16 层自洽表**(含 host 宿住层、shell 屏壳占树位两类特例)。 + +T0 临时探针 `t28_treeorder_probe.gd` / `t28_treeorder_probe2.gd` 采完即删,未提交。 + +### T1 装配语义合同单点化 + 顺序修复 + V2 断言重写 + +- **合同常量 `LAYER_CONTRACT`**(`main.gd`)=全仓唯一事实源:16 层(屏壳 shell / 模态 modal / 全屏态 state / 非视觉 node),带 `tags`(模态互斥域)、`host`(宿住层:level_screen 宿冒险屏壳)、`shell`(屏壳占树位、成员占登记位:camp_overlay/camp_screen)四类字段。树序=登记序=z 序同源派生。 +- **唯一落位口 `_apply_layer_contract()`**:按合同序 `move_child(node, slot)` + `_register_modal(member, tags)`;删除了原先散落在 `_build_ui` 的两处 `move_child`(camp 屏壳插位、curio 对位)。新增 `_layer_node(key)` 查表寻址函数。 +- **装配顺序修复**:`_adventure_screen` 先实例化挂树、`_menu_screen` 后挂——修复坏事实①「菜单屏先挂垫底被冒险屏盖死,F5 见战斗界面」。 +- **V2 断言重写**(坏事实②:旧断言把错误顺序写成期望值,测试在保护 bug):删除三条裸下标断言(`kids.find(_menu_screen)==0 and kids.find(_adventure_screen)==1` 等),改为 + - **V1a** 启动态顶层可见层=start_overlay(内容层遍历,屏壳不当候选) + - **V1b** 语义关系:菜单屏壳树序 > 冒险屏壳树序 + - **V1c** 合同树序链逐对保序(运行期树序 == `LAYER_CONTRACT` 序) + - **V1d** 落位单口源码断言(`_apply_layer_contract` 函数体之外 `move_child(` 计数 == 0) + - 场景探针同步:`ui_struct_probe` 新增 `_modal_registry_contract`(登记序==合同模态序逐位点名、直接子层树序==合同序、level_screen 宿主特例、camp 屏壳夹位特例),旧 `_career_registry_contract` 同批退役。 + +### T2 boot 链路时序后置条件 + +新增 `_smoke_boot_chain`,**十断言**(卡片写「六断言」,实施期按可断言面扩为十):ConfigDB 就绪且表已载 / 音量与减动效引擎态 == settings_store 现值 / start_overlay 可见 / **续玩钮态 == 断点在档态(boot 路径关系判据 + 隔离新档双向三子态:无断点隐、合成断点显、清断点回隐)** / run_scope 闲时会话在树 / 调试浮窗态 == DS_DEBUG 门控。存档隔离走 `attach_path("user://smoke_boot_chain.json")`。 + +### T3 生命周期存活性矩阵 + +新增 `_smoke_lifecycle_matrix`,**九阶段 × 五引用面**:闲时 / 开局停点(地图选路停点态合并)/ 战斗中 / 结算后(胜利停点)/ 选路页 / 终局面 / 回菜单收束 / 各模态开合后(菜单域四模态)/ 两连局 + 续玩。每阶段判 20+ 取数面的存活性:`run_scope` 在树、adventure / runner / settlement 三面活死、四处回调句柄(treasure/bonus/camp/modal_flow)与 `run_scope` 成员同源。活战斗驱动复用现有 `_auto_play_round` 模式(phase/round 守卫 + `AutoPlayer.choose_dig`)。 + +### T4 渲染面内容断言三件(坏事实③) + +1. **`shot_probe.gd` 两自检**:boot 态顶层可见层 == start_overlay;菜单标题/副标题 `is_visible_in_tree` 且 `get_global_rect()` 落视口内。隔离调用改 `ProbeUiUtil.isolate_save(main, "smoke_shot_probe", false, true, true)`(refresh_resume 开——菜单截图/golden 不依赖真实档断点态)。 +2. **`shot_checker.py` 墨色判据 F**:新增 `check_text_presence()`——与中位灰度差 >60 的像素占比落在 [2%, 55%] 带(**极性无关**,不预设字色深浅)。 +3. **启动态菜单 golden**:`golden_baseline_refresh.sh` 新增 `--menu` 模式(含字体与 `button_stone.png` 素材守卫),基线 `game/tests/golden/menu_boot.png` 入库;`render_gate.sh` 补菜单 golden 逐像素对比段(基线缺失=FAIL、截图/素材缺位=SKIP,沿既有 Q5 口径)。帧差前置验证:菜单 **changed=0.0000% / max_diff=0**(光柱是静态贴片),故全屏 golden 安全、无需收窄区。 + +### T6 规范落文 + 机械面文档同步 + +- **新硬规则「验收函数无变异红记录不算验收成立」**三处落文:`CLAUDE.md` 批次工作流第 9 条(原 9–14 顺延 10–15)、`design/dev/code-conventions.md` §3.6 变异验证操作细则(注入→最窄闸见红→`git checkout` 精确还原→记录四步;变异不红的二选一处理;判据强弱分层)、req-card 技能 §7 验证闸(WorkBuddy 与 ZCode 两份副本已同步;CodeBuddy / Qoder CN 无此技能副本,不动)。 +- 机械面文档随批更新:roadmap 迭代 28 行换代+近期主战场前沿改 29+ M4;06 登记条销号改写+反哺点三条专条;07 新增 ㉗ 启动态菜单目检节;doc 三闸对基线零新增。 + +--- + +## 二、闸记录 + +| 闸 | 结果 | +|---|---| +| 冒烟全量 | **1125 / 0**(基线 1086/0,+39) | +| 单元测试 run_all | **27 套全绿** | +| probe_gate --click | **全绿**,残留 Godot = 0 | +| probe_gate --full | ⚠️ **t17_defer_probe 红**(存量红,非本批引入——详见 §六),其余五件全绿 | +| render_gate | **全绿**:shot_checker A–D 过(含新增 F 判据 I-OK×2)、棋盘 golden 0.00%×3、**菜单 golden 0.00%** | +| doc 三闸 | 对基线零新增(gdd_lint 三件 / 死链 3 / 术语 5 组,与 master 39c6544 同) | +| MC 模拟 | **零跑**(本批零数值零行为变更,装配顺序修复除外) | + +**顺带清理(非本批引入,基线带入)**:`design/gdd/07-体验验证清单.md` v1.0 行处残留 `<<<<<<<` / `=======` / `>>>>>>>` 合并冲突标记(iter27-techdebt-20261014 分支合并进 master 时漏解)。两侧均有效故全留(qa 收尾批人闸核销行 + ㉖ 目检节登记行与正文节),版本号顺延 v1.1,清理注记入 v1.2 行。 + +--- + +## 三、代拍板追认单(待用户逐项追认) + +白名单内八项(卡片 §9)全部按授权执行;以下为实施期实际取定的形态与**白名单外需追认项**: + +| # | 事项 | 取定 | 归属 | +|---|---|---|---| +| 1 | 合同常量数据形态 | 有序 `Array[Dictionary]` + tags/host/shell 特例字段(白名单①) | 白名单 | +| 2 | 特例层断言表达 | 经宿主屏壳 / 成员间接比较(白名单②) | 白名单 | +| 3 | boot 顶层断言候选集 | 显式内容层清单(屏壳不当候选)(白名单③) | 白名单 | +| 4 | 墨色阈值 | 差 >60、占比带 [2%, 55%],按菜单文字实测定 provisional(白名单④) | 白名单 | +| 5 | 菜单 golden 收窄区 | 不收窄——帧差实证 0.0000%,直接全屏比对(白名单⑤,结论为「无需收窄」) | 白名单 | +| 6 | 变异注入手段 | 改常量序 / 插 `move_child` / 注释 `add_child` / 注释 `queue_free` / `visible=false` / 状态源取反(白名单⑥) | 白名单 | +| 7 | 新冒烟段挂载位置 | `_smoke_scene_split` → `_smoke_boot_chain` → `_smoke_run_lifecycle` → `_smoke_lifecycle_matrix`,挂 `_split_tail`(白名单⑦) | 白名单 | +| 8 | 计划书存档文件名 | `.zcode/plans/启动装配与生命周期验收补全批-loop-plan-2026-10-15.md`(白名单⑧) | 白名单 | +| 9 | **合同表规模 12 → 16 层** | 卡片基于「十二模态登记数组」,实采后扩为覆盖全视觉层的 16 层(屏壳/非视觉节点/全屏态亦入表)——约束「单点化+同源派生」未破 | **白名单外,请追认** | +| 10 | **T2 断言数 6 → 10** | 卡片写六断言,实施期把续玩钮拆出隔离三子态、补全 ConfigDB/设置/会话/浮窗门控面 | **白名单外,请追认** | +| 11 | **T3 阶段划分调整** | 卡片写「十阶段」,实施期把「地图选路停点态」并入「开局停点」(同一运行期状态,无独立断言面),得九阶段 | **白名单外,请追认** | +| 12 | **变异 2b 形态偏离计划书** | 计划书原文「注释 `_begin_session`/提前 free」;实施期取 2a=注释 `add_child`(该活不活)、2b=**注释 `queue_free`(该死不死)** 互补双向。「提前 free」改为 `free()` 会与 V3f 断言同向(仍绿),不构成有效变异,故未取 | **白名单外,请追认** | +| 13 | **07 冲突标记清理** | 基线带入的未解合并标记,本批因需同文件登记 ㉗ 而顺手解开(两侧全留,零内容发明) | **白名单外,请追认** | +| 14 | **req-card 技能双副本** | ZCode 副本与 WorkBuddy 副本已各自演进(ZCode 多两条自有条目),本批只把变异验证一条补进 ZCode 副本,**未擅自取齐**两副本差异 | 知会项;是否取齐请裁 | + +--- + +## 四、GDD 反哺点(三条,已登记 06,待用户逐点裁决另批落文) + +| # | 文档 | 反哺点 | +|---|---|---| +| 1 | `design/dev/architecture/architecture.md` + ADR 系列 | **视觉层装配合同**=`main.gd` 的 `LAYER_CONTRACT` 常量(16 层),树序=登记序=z 序同源派生,全仓唯一落位口 `_apply_layer_contract`;现行架构文档无此条款(仅 :124 提「组合根 main.gd」、:173 提「主场景装配视图单元」)——是否立条款、合同表是否随架构文档摘录待裁 | +| 2 | `design/dev/architecture/architecture.md` | **boot 链路后置条件**已由 `_smoke_boot_chain` 十断言成文;架构文档启动流程(:173)只有装配动作无后置条件——是否入册待裁 | +| 3 | `design/dev/architecture/architecture.md` | **RunScope 生命周期存活性矩阵**(九阶段×五引用面)已可执行化;架构文档 :124 只登记了 RunScope 的存在——矩阵是否入册、阶段划分是否认账待裁 | + +**与迭代 27 反哺点①的关系**:iter27 只登记了「场景分层/会话生命周期陈述滞后于实态」的方向性缺口;本批把其中「层序=谁说了算」「boot 后置条件是什么」「会话引用面何时活何时死」三件做成了代码级事实与可执行断言,本条是其**具体化承接**,落文时可合并处理。 + +--- + +## 五、变异验证记录表(六次注入,全部见红并精确还原) + +判据:**注入 → 跑最窄的那一道闸 → 见红 → `git checkout -- <文件>` 精确还原 → 复跑确认回绿**。还原后统一复跑冒烟得 **1125/0**,六轮全以 `REVERTED-CLEAN` 收尾。 + +| # | 变异点(file:line) | 注入方式 | 缺陷类 | 跑的闸 | 红输出(摘要) | +|---|---|---|---|---|---| +| 变异 1 | `game/presentation/main.gd` `LAYER_CONTRACT` 首两行 | `adventure_screen` / `menu_screen` 对调(翻装配序) | 装配序错 | `python tools/run_smoke.py` | 1125 项 **2 失败**:`V1a`(实测顶层=`@VBoxContainer@648`,菜单被冒险屏盖住)、`V1b`(菜单屏壳树序不在冒险屏壳之上)红;`V1c/V1d` 绿 | +| 变异 1b | `main.gd` `_apply_layer_contract()` 调用后 | 插入散落 `move_child(debug_overlay, 0)` | 层序硬编码(合同外落位) | 同上 | 1125 项 **2 失败**:`V1d`(函数体之外 move_child 调用点=1 应=0)、`V1c`(违例 `@Control@1057(14)≤@Control@1119(0)`)红 | +| 变异 2a | `main.gd:195` `_begin_session` | 注释 `add_child(run_scope)`(会话对象不挂树) | 对象该活不活 | 同上 | 1125 项 **22 失败**:`V2e` 红 + V3 全部 21 个阶段面「run_scope=不在树」 | +| 变异 2b | `main.gd:191-192` `_begin_session` | 注释 `run_scope.queue_free()`(旧会话不销毁) | 对象该死不死 | 同上 | 1125 项 **1 失败**:`V3f`「收束后旧局经 queue_free 消亡(is_instance_valid=false)」红 | +| 变异 3 | `main.gd:218` `_ready` | `_start_overlay.visible = false`(藏菜单层) | 层被盖/不可见 | 冒烟 + 窗口化 `shot_probe` + `golden_compare` + `shot_checker` | 冒烟 **3 失败**(`V1a`/`V2c`/`V3a`);探针自检 **3 问题**(顶层≠start_overlay、标题/副标题 `is_visible_in_tree=false`);golden `changed=34.23%` FAIL;`shot_checker` exit=1 | +| 变异 4 | `main.gd:531` `_refresh_resume_button` | `_resume_btn.visible = not has`(状态源倒置) | 状态源/时序倒置 | `python tools/run_smoke.py` | 1125 项 **6 失败**:`V2d` 四条全红(boot 态、无断点态、有断点态、清断点回隐)+ `V3a` + `V3i` | + +### ⚠️ 变异 3 暴露的一条判据边界(重要,如实记录) + +**F 判据(墨色占比)本身没有红**——菜单层被藏后,底层棋盘内容透过标题框仍给出 33.48% 的高反差像素,落在 [2%, 55%] 带内,`I-OK` 照过。真正拦下这个缺陷的是另外两件:探针的 `is_visible_in_tree` 自检,和 golden 逐像素(34.23%)。`shot_checker` 虽 exit=1,但红的是 **C 判据**(背景纹理幅度 79.4 > 18),属间接命中。 + +**结论**:F 判据只能证伪「整框空白/整框糊」,**不证明「框里是菜单标题」**。菜单内容断言的强判据是「可见性自检 + golden 逐像素」,F 判据为补充。这条边界已同步写进 `code-conventions.md` §3.6 的「判据强弱分层」,供后续写卡配判据时援引。 + +--- + +## 六、残留与去向 + +### 6.1 probe_gate --full 的 t17_defer_probe 红(存量红,**非本批引入**,待裁) + +**① 现象**:`tools/probe_gate.sh --full` 中 `t17_defer_probe` 失败——`[t17_defer] FAIL — 关卡画面未出现`(exit=2;首次运行时表现为 exit=124 超时挂死,同一根因的两种表现)。`--full` 的其余五件(ui_input / ui_struct / ui_contrast / ui_probe / ui_tune / fx)**全绿**,`--click` 亦全绿。 + +**② 证据链(三条,均指向「非本批引入」)**: + +| 证据 | 结果 | +|---|---| +| 主区 master 39c6544(**不含本批任何改动**)跑同一探针 | **同样 FAIL 4/4**,失败行与本批完全一致 | +| 把真实档 `user://save.json` 临时改名移开后跑同一探针 | **PASS**(exit=0,15s,`胜利同帧挂起+排播完开窗 两断言全过`) | +| 探针自身行为 | 首次跑绿、跑完即把真实档写成带断点状态,其后必红——**探针会污染它本应隔离的真实档** | + +**③ 机理**:`_start_adventure_from_menu()`(`main.gd:498`)在 `save_manager.has_breakpoint()` 为真时不开冒险,改弹「已有未完冒险」确认框。真实 `user://save.json` 当前存在未完冒险断点时,探针的隔离(`main.save_manager = iso_c`)没能覆盖到这条路径,于是 60 帧内等不到 `level_screen.visible` → FAIL。 + +**④ 补充事实(须告知)**:`user://` 目录(`%APPDATA%/Godot/app_userdata/DungeonSweeperVerticalSlice/`)由**全部 worktree 共享**(同 project name),所以主区与任何 worktree 的探针/冒烟都在读写同一份存档。本机这份真实档现在带有一个未完冒险断点。**本批验证过程中曾为定位根因临时改名移开该档,已按原样还原并通过逐字节 `cmp` 校验一致**(备份留 `/d/tmp_save_backup_20261015.json`)。 + +**⑤ 历史沿革**:iter26 台账记「probe click/full 全绿」,**iter27 台账只记了 `probe click` 未记 `--full`** —— 该红大概率自 iter27(row_pacer 排播泵迁出 + ModalFlow 抽取)起就存在而未被跑到。probe_gate 头注亦有预言:「超时多半=探针未适配新停点层,归 probe 适配批」。 + +**⑥ 待裁(不擅自处理)**:本红属**探针适配存量债**,修它=改探针隔离方式,是卡片外的新需求。三个选项请裁: + +- **甲**:认可存量红、照常合并——红与本批无关,master 现状同红,合并不会使任何东西变差; +- **乙**:先修探针隔离(补真实档隔离/跑前清断点)再合并——属新需求,需用户批后另起小批; +- **丙**:用户先自行处置本机真实档断点(游戏内开新冒险覆盖或删档),我再复跑 `--full` 取绿后合并——但此法只治本机这一次,探针自污染的根因仍在。 + +### 6.2 实机目检(登记不阻塞,2026-10-12 立规) + +已登记 `design/gdd/07-体验验证清单.md` **㉗ 启动态菜单目检**五观测项:启动即见主菜单 / 菜单六钮可点 / 菜单与战斗互不遮挡 / 各覆盖层开合层级正确 / 音量与减动效启动即生效。**登记完毕即批内义务了结**,用户自行安排时间验收。 + +### 6.3 其他 + +- 计划书存档:`.zcode/plans/启动装配与生命周期验收补全批-loop-plan-2026-10-15.md`(不入 git)。 +- 需求卡片按「中间产物吸收即死」惯例不入 git;卡片所记基线 1081 与实测 1086 的差异已随本批回写 roadmap 与 06。 +- req-card 技能两份副本(WorkBuddy / ZCode)本批各补一条后**仍有两条 ZCode 自有条目未取齐**,是否取齐请裁(§三 #14)。 + +--- + +## 七、冒烟断言数增量呈报 + +**1086 → 1125,净 +39**,逐项可溯源: + +| 项 | 条数 | +|---|---| +| 新增 V1(装配合同四断言:V1a/V1b/V1c/V1d) | +4 | +| 新增 V2(boot 链路十断言) | +10 | +| 新增 V3(生命周期矩阵,九阶段面 + 各附加判据) | +28 | +| 退役裸下标断言(`kids.find(...)` 三处——合同链断言已覆盖) | −3 | +| **合计** | **+39** | + +(新增 42 条全部带 `〔装配生命批 Vn〕` 尾缀,可与需求卡片 §7 逐条 grep 对账。) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 7f5f76e9..886aa302 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -450,7 +450,7 @@ func _layer_node(key: String) -> Node: func _apply_layer_contract() -> void: var slot := 0 for entry: Dictionary in LAYER_CONTRACT: - var tags: Array = entry.get("tags", []) + var tags: Array[Variant] = entry.get("tags", []) if entry.has("shell"): var shell: Node = _layer_node(String(entry["shell"])) var member: Node = _layer_node(String(entry["key"]))