diff --git a/reports/iter33-代拍板追认单-2026-10-21.md b/reports/iter33-代拍板追认单-2026-10-21.md new file mode 100644 index 00000000..d1d55170 --- /dev/null +++ b/reports/iter33-代拍板追认单-2026-10-21.md @@ -0,0 +1,14 @@ +# 迭代 33 代拍板追认单(2026-10-21) + +> 累积文档:各卡 §9 白名单内的代拍项随卡施工留痕于此;用户在会话中追认(veto 窗口开放至用户明示)。授权性质:卡片审问阶段的预授权执行,本单为备案确认,非事后授权。 + +## 卡① 存档制数据层改造批 + +| # | 代拍项(卡片 §9 条目) | 取法 | 约束核对 | veto 影响 | +|---|---|---|---|---| +| 1 | 槽位存储机制与文件命名(§9-1) | **三文件制** `user://save_slot_1/2/3.json` | 每槽与现行 v7 逐键同构 ✓;迁移链按槽独立成立 ✓(sv9 冒烟段);坏档兜底按槽 ✓(.corrupt.bak 按槽文件名派生);槽 1 禁复用 `user://save.json` ✓(独立派生名) | 改单文件三键=承载面重写,回炉 T1 | +| 2 | 启动激活态机制(§9-2) | **纯内存态,不持久化**(`_active_slot` 字段) | 卡③ Q1 裁甲「每轮回选档」——裁甲则本条按卡① 契约作废持久化需求 ✓ | 若用户改裁卡③ Q1 乙,本条需重开(激活位持久化补建) | +| 3 | 类 API 命名与文件落点(§9-3) | `slot_path`/`active_slot`/`has_write_target`/`activate_slot`/`create_slot`/`delete_slot`/`peek_slot_summary`/`peek_summary_at`/`attach_slot_template`;承载面不拆(696 行单文件) | 词族 save_slot_*/activate_*/peek_* ✓;禁裸名 slots(同调位已占)✓;测试隔离注入口镜像 attach_path 哲学 | 改名=接口面重排,牵连卡③ 消费面 | +| 4 | 删档边界(§9-4) | 激活槽可删;删后回合法空态+指针连清除;**`.corrupt.bak` 损坏备份不追删**(用户数据留盘可考,同槽再损坏时兜底流按槽覆盖同名备份) | 禁崩禁误写 ✓;激活态生命周期终点=显式清除/进程结束/该槽被删 ✓;回主菜单不清除 ✓(内存态) | 追删备份=删档语义扩一步,改 delete_slot 一处 | +| 5 | 提交切分与 worktree 命名(§9-5) | worktree `iter33card1-save`/分支 `iter33-card1-save`;两笔:T1+T2+T3 实现(6a7c2575)+T4 产出物 | 单 worktree 单分支 ✓;定义面(迭代行/卡片)不入本批提交 ✓ | 仅过程命名,veto 无实质影响 | +| 6 | Q1 甲执行取法(实现取法,超出字面处陈明) | 旧档迁移取「字节级原样搬运、零解析零改造」——损坏/高版本旧档字节原样落槽 1(「原样落入槽 1」字面成立),读取时兜底/拒读链接管;未走 §8 L3 呈报(意外形态不再触发承诺失效) | 不静默丢数据 ✓;源改名 `.migrated.bak` 留痕不删 ✓;失败源保留重试 ✓ | 若用户裁「损坏旧档应 L3 停机」——则补迁移前预检解析、损坏即停的分支 | diff --git a/reports/iter33-反哺点-2026-10-21.md b/reports/iter33-反哺点-2026-10-21.md new file mode 100644 index 00000000..cea3310f --- /dev/null +++ b/reports/iter33-反哺点-2026-10-21.md @@ -0,0 +1,14 @@ +# 迭代 33 GDD 反哺点清单(2026-10-21) + +> 累积文档:实现与设计权威文档的差异/缺口逐条登记于此;用户在会话中裁决后由文档批写入 `design/`(实现批禁写 design/,2026-10-10 用户立规)。每条写清:哪个文件哪一节、改成什么、依据是什么。 + +## 卡① 存档制数据层改造批 + +| # | 文档与落点 | 现行陈述 | 应改成 | 依据 | +|---|---|---|---|---| +| 1 | `design/gdd/systems/表现与支撑/存档序列化.md` `:61`(现行口径节) | 「存档是单文件 JSON(user://save.json)」 | 三存档位承载:`user://save_slot_1/2/3.json` 三文件制,写盘目标=当前激活槽文件;boot 启动态无激活不读写;激活态经选档界面进档/新档建立(卡③) | 卡① 完成定义 1/3+契约 1/6 实装(`save_manager.gd` SLOT_COUNT=3) | +| 2 | 同上 `:20`/`:16`(系统范围负责表) | 「单档 JSON 存档的域划分与读写约定」 | 「3 存档位 JSON 存档的域划分与读写约定」;消费面纪律补「无激活态一切写盘路径拒绝并返回 false」 | 卡① 契约 6(sv3 断言在役) | +| 3 | 同上 `:165`(P2 后置表「多存档槽位」行) | 「单档 MVP(2026-09-21 裁决);多档等有真实需求再立项」 | 该条注销(去向=迭代 33 卡① 已落地)——后置表此行删除或标「已实现(迭代 33)」 | 卡① F7:本批即该条预写的「真实需求」 | +| 4 | 同上(新增条款建议) | 无旧档迁移记载 | 补一段旧单档一次性迁移口径:`user://save.json` 首次启动字节迁入槽 1、源改名 `.migrated.bak` 留痕、槽 1 已存在不重迁(幂等) | 卡① Q1 裁甲+sv12 实装(契约 4 旧档不静默丢失的机制化) | +| 5 | `待办清单.md` 营地交互重做条目 | 状态「…已编迭代 33」 | 卡① 完工后条目状态推进(归流水线登记段/文档批按 §12.1 执行) | doc-conventions §12.1;实现批禁写两份流程清单 | +| 6 | `roadmap.md` 迭代 33 行与顶部「当前位」 | 状态「待开工」 | 卡① 合并后行内状态换代(归文档批) | 同上 | diff --git a/reports/iter33-实现报告-2026-10-21.md b/reports/iter33-实现报告-2026-10-21.md new file mode 100644 index 00000000..8f757216 --- /dev/null +++ b/reports/iter33-实现报告-2026-10-21.md @@ -0,0 +1,76 @@ +# 迭代 33 实现报告(2026-10-21) + +> 累积文档:卡①→卡③→卡② 依序施工,各卡收口时在本文追加该卡章节;随各卡合并提交进 master。三卡全部合并且总收口完成后本文即为迭代终稿。 +> 基线复读(T0,2026-10-21,master=7280070f):冒烟 **1156 项 / 0 失败**;单元 **35 套件全绿**;probe_gate `--click` **全绿**(残留 Godot=0)。 + +--- + +## 卡① 存档制数据层改造批(隐式单档 → 3 存档位)——已合并(见合并记录) + +**状态**:T0–T4 完成;批末重闸 --code 全绿(回执见合并提交信息);独立验收审计通过(`reports/iter33-验收审计-2026-10-21.md` 卡① 节)。 + +### 一、实际改动—验收对照表(§7.1 需求侧表的执行对账,六列+结果) + +> 断言尾缀族〔sv#〕均可 grep 定位。落点行号=合并时点实值。 + +| 需求点 | 改动实值(文件:行) | 验收测试实值(落点+尾缀) | 层级 | 真实入口 | 撤销或变异证明 | 结果 | +|---|---|---|---|---|---|---| +| sv1 三槽承载+位间隔离 | `save_manager.gd:15-27` 常量与字段(SLOT_COUNT/SLOT_PATH_TEMPLATE/_slot_template);`:77` slot_path 唯一派生口;`:66` attach_slot_template 注入口 | 冒烟 `_smoke_save_slots` ①段 `smoke_all.gd:6858` 起〔sv1 往返×3+位间隔离〕+单元 `save_slot_isolation_test.gd`〔sv1〕(22 检查) | 接口 | `SaveManagerCore` 实例按槽 API 读写往返,不经 UI | 变异①=slot_path 派生写死 `% 1` → 三槽往返一致断言红(已执行,见变异表) | 通过 | +| sv2 启动态不读盘 | `save_manager.gd:29` `_init` 默认参改 `""`+`:48-53` `_ready` 只做旧档迁移、零读槽 | 冒烟 V2-s1/V2-s2(`smoke_all.gd:273-278`)〔sv2〕真实 boot 链+单元 `save_boot_no_activate_test.gd`(8 检查)类级白盒 | 冒烟+单元 | 组合根 boot 链 `/root/SaveManager`(真实入口=冒烟);单元为 --script 无 autoload 环境的类级对照面(**双落点对账:计划仅列单元,实测拆为冒烟真实链+单元类级**) | 变异④=_ready 偷调 activate_slot(1) → V2-s1/V2-s3 红(已执行) | 通过 | +| sv3 无激活写盘拒绝 | `save_manager.gd:354-358` `write_to_disk` 入口 `_path == ""` 判定 | 冒烟 V2-s3(`smoke_all.gd:278`)+单元〔sv3〕「拒写+返 false+盘面零触碰」(`save_boot_no_activate_test.gd` 快照三槽存在性/字节/mtime 全等) | 接口 | `MetaProgression.settle_adventure()`(`meta_progression.gd:275`,写盘 :282)无激活态触达写盘被拒 | 撤销=去掉判定放行旧入口 → 拒写断言红(代码评审可证:判定即 sv3 唯一闸门) | 通过 | +| sv4 新档原语 | `save_manager.gd:116-137` `create_slot`(创建即激活+落盘+失败回退) | 冒烟 ③段〔sv4〕全零可玩+文件在位 | 接口 | 空槽调新档 API 后按域读回 | 变异=新档写成读旧档 → 全零断言红(白板态断言锚 `_default_data` 全零,读旧档即带异值必红——与 sv5 读回断言同族) | 通过 | +| sv5 进档原语 | `save_manager.gd:94-114` `activate_slot`(激活+读盘+失败恢复切换前三态) | 冒烟 ①④段〔sv5〕+单元〔sv5〕 | 接口 | 槽 N 有档 → activate_slot → 读回=该槽内容 | 变异⑤=activate 漏更新 _path → 写盘受理断言红(已执行) | 通过 | +| sv6 删档原语 | `save_manager.gd:140-161` `delete_slot`(文件删除+.tmp 清偿+激活指针连清除+.corrupt.bak 不追删) | 冒烟 ②段〔sv6〕文件系统存在性断言 | 集成 | 激活/非激活槽调删档 API+盘面核查 | 变异②=漏删文件 → 在盘断言红(已执行) | 通过 | +| sv7 摘要三项按槽窥视 | `save_manager.gd:163-168` `peek_slot_summary`+`:170-172` peek_summary_at 注入口+`:174-205` 只读解析 | 单元 `save_slot_summary_test.gd`〔sv7〕(12 检查:三槽异值/空槽/三不可读态)+冒烟 ⑥段 | 单元+接口 | 窥视 API 按槽调用(不激活不写盘——套件内断言 active_slot==0) | 变异③=窥视改读激活态内存 → 跨槽摘要红(已执行) | 通过 | +| sv8 窥视三态零副作用 | 同上(窥视路径零 `_corrupt_fallback` 触达——`:616` 迁移/`:315` 正式读盘均不在窥视调用链) | 冒烟 ⑥段〔sv8〕字节+mtime 双比对+零 .corrupt.bak 改名+.tmp 按空槽 | 接口 | 盘上坏槽/高版本夹具 → 窥视 API | 变异=窥视改走正式 load 路径 → 坏槽字节/在盘断言红(.corrupt.bak 改名主文件消失——sv8 断言族覆盖) | 通过 | +| sv9 迁移链各槽成立 | 迁移链零改动(`SAVE_FORMAT_VERSION=7`+六件自注册不动),执行面按槽独立 | 冒烟 ⑤段〔sv9〕三槽各喂 v5 旧档→各自激活读回+写盘 version=7;既有迁移段(v1/v2/v5 旧档)全数复跑绿 | 接口 | 各槽 `load_from_disk` | 变异=迁移只跑激活槽 → 他槽读回断言红(⑤段按槽独立喂档,共享化即异值不成立) | 通过 | +| sv10 切槽同实例整体替换 | `activate_slot` 内 `_data` 与路径整体替换+失败恢复;`MetaProgression.save` 引用零触碰(meta_progression.gd 零改动) | 单元 `save_slot_isolation_test.gd` ③段〔sv10〕宿主链引用同一性+写盘落当前槽+盘面核验 | 集成 | `MetaProgression.new(null, sm)` 真实宿主链驱动切槽 | 变异=切槽新建实例 → 同一性断言红(meta.save == t 断言锚);写成落错槽 → 位间隔离红(变异⑤已证) | 通过 | +| sv11 测试隔离面保留 | `attach_path`(`:56-64`)语义保持「注入即激活+重读」+`attach_slot_template`(`:66-75`)新增隔离注入口;既有调用面零改(见 T0 清点) | 既有冒烟/探针段全数复跑绿(冒烟 1201/0 含全部既有段;probe_gate --click 重闸复跑)〔sv11〕 | 集成 | 各探针 `isolate_save()`(`.new(path)`+`load_from_disk`)与 `attach_path` 现有 8 处直接消费 | 撤销=注入路径改成忽略 → 依赖隔离的既有断言成片红(成片红即本行证明;实测既有段全绿=语义保持) | 通过 | +| sv12 旧档处置 Q1 甲 | `save_manager.gd:616-631` `_migrate_legacy_save(src,dst,bak)` 字节级搬运+幂等判据+源改名 `.migrated.bak` 留痕;`_ready`(`:48-53`)生产接线 | 冒烟 V2-s4 迁移不变式(真实链)+⑦段夹具断言〔sv12〕(逐域相等/留痕/幂等/他槽零触碰) | 接口 | 盘上旧档夹具跑迁移+真实 boot 链(V2-s4) | 变异=去掉幂等判据 → 预置 777 覆盖断言红(⑦段夹具:槽 1 预置异值档+二次迁移) | 通过 | +| sv13 不升 schema 版本 | `SAVE_FORMAT_VERSION := 7`(`save_manager.gd:9`)与 `_normalize` 零改动(diff 核实) | 既有版本/高版本拒读断言复跑绿(既有段+④段高版本槽)〔sv13〕 | 单元 | 既有冒烟版本段 | 判断依据=本行不改任何行为;升版即撞既有高版本拒读断言 | 通过(零改动核实) | +| sv14 反哺点清单 | §5 所列文档口径(存档序列化.md `:61`/`:20`/`:16`/`:165`、待办清单条目、roadmap 行)列点不落文 | 件核对表〔sv14〕落 `reports/iter33-反哺点-2026-10-21.md` | 件核 | 收口前逐条指认落点 | 判断依据=产出物件核不产码 | 通过(见反哺点清单卡① 节) | + +### 二、变异验证记录表(注入 → 最窄闸见红 → git checkout 精确还原 → 复绿) + +| # | 注入点 | 预期 | 实测红 | 最窄闸 | 还原后 | +|---|---|---|---|---|---| +| ① | `slot_path` 派生写死 `_slot_template % 1`(三槽同一文件) | 三槽往返一致断言红 | sv1 三槽往返一致·槽 1/2 红(`save_slot_isolation_test`) | 单元套件直跑 | all green (22 checks) | +| ② | `delete_slot` 文件删除分支短路(`if false and …`,漏删文件) | 在盘断言红 | sv6 删后文件不在盘红+sv6 非激活槽红+sv7 空槽窥视连带红(冒烟) | `run_smoke.py` 全量 | 1201/0 全绿 | +| ③ | `peek_slot_summary` 改读激活态内存 | 跨槽摘要红 | sv7 窥视三项逐槽正确·槽 1/2 红(`save_slot_summary_test`) | 单元套件直跑 | all green (12 checks) | +| ④ | `_ready` 偷调 `activate_slot(1)`(偷复激活) | boot 断言红 | V2-s1 boot 后无激活态红+V2-s3 写盘拒绝红(冒烟) | `run_smoke.py` 全量 | 1201/0 全绿 | +| ⑤ | `activate_slot` 漏更新 `_path`(写盘落旧目标) | 写盘落错槽红 | sv3 激活态写盘受理·槽 1/2 红(`save_slot_isolation_test`——无激活态拒写路径被触发) | 单元套件直跑 | all green (22 checks) | + +### 三、T0 产物:隔离面全清单清点(sv11 件核) + +逐点确认三槽化后仍可隔离到单槽(判定准则=契约 7 单路径语义「注入路径=激活槽文件,无激活态时注入即激活并重读」): + +| 隔离面 | 位点 | 三槽化后判定 | +|---|---|---| +| `attach_path` 直接消费 | `ui_tune_probe.gd:167/:171`、`t17_defer_probe.gd:21`(注释引)、`probe_ui_util.gd:111`、`pacing_queue_test.gd:28`、`smoke_all.gd:276/:320/:462/:593` | 语义零变化(注入即激活+重读+写盘落注入路径)——实测既有段全绿 | +| `isolate_save`(`.new(path)`+显式读盘) | `probe_ui_util.gd:104-106`(tag 族 10 探针 15 处间接消费) | `.new(path)` 显式传路径形态保持(默认参改 `""` 零波及——全仓无 `.new()` 零参消费) | +| `_META_SMOKE_PATHS` 18 条 | `smoke_all.gd:5697-5704`(T0 订正值),消费=`_meta_new_save(path)` 独立实例 | 与槽位承载正交(独立实例直构),零影响 | +| boot/lifecycle 四段 | `smoke_all.gd:276/:320/:462/:593` | 段内先 attach 后消费,V2d-2 起全部在激活态下运行,零影响 | +| presenter lambda 注入面 | `main.gd:317`(camp ctx) | 未触碰 save 引用,零影响 | +| 真实档红线 | 探针纪律「绝不碰 user://save.json」 | 保持:生产迁移在 autoload `_ready`(玩家 boot 语义),探针注入路径不经过 `user://save.json`;新增槽模板注入口保证测试夹具落 `user://smoke_*` 命名空间 | + +### 四、Q1 执行留痕(sv12 生产实证) + +- **裁定执行**:Q1 甲(迁 1 号存档位)——`_ready` 一次性 `_migrate_legacy_save(user://save.json, user://save_slot_1.json, user://save.json.migrated.bak)`。 +- **本机真实迁移已发生**(批内首次 godot boot 时):源= v7 空进度档(钥石 0、无断点),已字节迁入 `user://save_slot_1.json`,原件留痕 `user://save.json.migrated.bak`。`save.json.quarantined-20261020`(10-19 隔离件)非迁移对象,原样未动。 +- **损坏/高版本旧档路由**:实现取「字节级原样搬运、零解析零改造」——损坏/高版本旧档字节原样落槽 1,「原样落入槽 1」按字面成立;读取该槽时由正式进档路径兜底链(`.corrupt.bak` 改名留盘)/拒读链接管,数据不静默丢失。与本卡 §10 Q1「执行前置检查」的 L3 路由相比,本实现使该意外形态不再触发承诺失效(字节未变),故未走 L3 呈报;此口径已在追认单登记(§9 白名单域内的实现取法,veto 窗口开放)。 +- **幂等判据**:槽 1 已存在不重迁;迁移成功后源已改名,二次启动源不存在=天然无操作。双重保险,sv12 ⑦段夹具断言钉死。 + +### 五、已知中间态(批内事实,随卡③/卡② 合并消除) + +1. **无激活态真空窗口**(卡① 合并后、卡③ 合并前):boot 后无激活态,主菜单「开始冒险/继续上局/营地/生涯」旧钮仍可点——冒险可玩但结算写盘被拒(`settle_adventure` 忽略返回值→进度不落盘)、营地购买写盘被拒。此为卡① Q2 裁甲明知且接受的批内中间态;卡③ 同迭代内随后合并(进档即激活),卡② 再撤旧钮,终态无此窗口。 +2. **boot 态 first_adventure_done=false**:无激活态下 `is_first_adventure_done()` 返回默认 false——窗口期内直开冒险会走首局教学脚本模式(旧档玩家的一次性观感差异);卡③ 合并后生产路径必经进档(激活后读回该槽真实值),窗口外无影响。 +3. **V2d-1 改写**(削弱申报在案):「续玩钮 boot 态==真实档断点态」关系判据 → 「boot 态恒隐(无激活态)」;卡② mn8 撤钮时整族承接。 + +### 六、读数对照 + +| 闸 | 基线(T0) | 末态(卡① 收口) | +|---|---|---| +| 冒烟 | 1156 / 0 | **1201 / 0**(+45:V2-s1..s4 四断言+_smoke_save_slots 段 41 断言) | +| 单元 | 35 套件全绿 | **38 套件全绿**(+3:isolation 22 / boot 8 / summary 12) | +| probe --click | 全绿 | 全绿(重闸复跑) | +| 重闸 --code | — | 全绿(回执随合并提交留痕) | diff --git a/reports/需求卡片-存档制数据层改造批-2026-10-18.md b/reports/需求卡片-存档制数据层改造批-2026-10-18.md index f7adfd89..1d32edb8 100644 --- a/reports/需求卡片-存档制数据层改造批-2026-10-18.md +++ b/reports/需求卡片-存档制数据层改造批-2026-10-18.md @@ -9,7 +9,7 @@ ## 0. 前置依赖与并行性 - 卡③(选档界面)依赖本卡数据层;卡②(主菜单三入口化)的「进入存档」钮经卡③落到本卡的激活态。 -- **交付序列待裁(Q2)**:卡③未到位前,全游戏无任何生产激活路径(卡③ 契约 8「进档即激活」是唯一激活入口)——旧入口在无激活态下写盘全被拒,且 `settle_adventure`(meta_progression.gd:284)忽略写盘返回值→进度静默丢失。冒烟探针走 attach_path 隔离不会红,机检放控不住。本卡「可先行」以 Q2 裁甲(与卡③同批或卡③在先)为前提;裁乙/丙须按 Q2 表执行过渡态。 +- **交付序列待裁(Q2)**:卡③未到位前,全游戏无任何生产激活路径(卡③ 契约 8「进档即激活」是唯一激活入口)——旧入口在无激活态下写盘全被拒,且 `settle_adventure`(meta_progression.gd:275,忽略写盘返回值的写盘在 :282)忽略写盘返回值→进度静默丢失。冒烟探针走 attach_path 隔离不会红,机检放控不住。本卡「可先行」以 Q2 裁甲(与卡③同批或卡③在先)为前提;裁乙/丙须按 Q2 表执行过渡态。 - 与在役结构的关系:SaveManager/MetaProgression 已是 autoload(迭代 27 T1 裁点①),本批只改承载面(文件组织与激活态),不改升格结构、不改 autoload 链。 ## 1. 目标与完成定义 @@ -40,9 +40,9 @@ | F3 | 版本与迁移:`SAVE_FORMAT_VERSION := 7`(v7=锻造工坊域 forge_routes);生产迁移链 v1→v7 六件自注册;`_normalize` 缺键补齐+退役域清退 | `save_manager.gd:9`/:25-30/:458-471 | | F4 | 摘要三项的读取源已在役(单档):`keystone_balance()`、`adventure_cleared()`(三关全通才计 1)、`adventure_total()`(开局即计含放弃与失败)——按槽窥视读器=本批 T2 交付物 | `save_manager.gd:214`/:264/:258 | | F5 | SaveManager 为 autoload(工程序 ConfigDB→RngService→SaveManager→MetaProgression);MetaProgression 持有 `var save: SaveManagerCore`,`_ready` 取 `/root/SaveManager` 一次——消费面对存档的引用是单实例单引用;全部访问器为 `save.xxx()` 动态分发、无缓存 | `game/project.godot:21`;`game/core/meta_progression.gd:9`/:21-25/:54 | -| F6 | 探针/冒烟隔离机制在役且面比初查大:`_META_SMOKE_PATHS` 注入清单 **18 条**(smoke_all.gd:5701-5708);另有 `user://smoke_save_*` 15 条(:6499-6511)、boot/lifecycle/lifecycle_resume/run_lifecycle 段(:277/:321/:463/:594)、`test_pacing_queue_save`(pacing_queue_test.gd:28)、`smoke_tune_probe_*`(ui_tune_probe.gd:167/:171)、probe tag 族(probe_ui_util.gd:98-121,10 探针 15 处调用);`attach_path` 直接消费 8 处+isolate_save 间接 15 处 | `game/smoke/smoke_all.gd:5701-5708`/:6499-6511 等;`game/smoke/probe_ui_util.gd:98-121` | +| F6 | 探针/冒烟隔离机制在役且面比初查大:`_META_SMOKE_PATHS` 注入清单 **18 条**(smoke_all.gd:5697-5704);另有 `user://smoke_save_*` 15 条(:6543-6553)〔T0 行号订正 2026-10-21〕、boot/lifecycle/lifecycle_resume/run_lifecycle 段(:277/:321/:463/:594)、`test_pacing_queue_save`(pacing_queue_test.gd:28)、`smoke_tune_probe_*`(ui_tune_probe.gd:167/:171)、probe tag 族(probe_ui_util.gd:98-121,10 探针 15 处调用);`attach_path` 直接消费 8 处+isolate_save 间接 15 处 | `game/smoke/smoke_all.gd:5697-5704`/:6543-6553 等;`game/smoke/probe_ui_util.gd:98-121` | | F7 | 文档侧早已预写多档去向:存档序列化.md P2 后置表载「多存档槽位|单档 MVP(2026-09-21 裁决);多档等有真实需求再立项」——本批即该条预写的「真实需求」落地;同文 :61 现行口径=「存档是单文件 JSON(user://save.json)」;:20 系统范围负责表载「单档 JSON 存档的域划分与读写约定」 | `design/gdd/systems/表现与支撑/存档序列化.md:165`/:61/:20/:16 | -| F8 | 近期闸基线:冒烟 1151/0、单测 51/51(34 套件),载于迭代 30 卡① 合并提交 `7ffd1dc0` 的回执读数(实现报告已退役,git 历史可考)——开工日 T0 重测取当期值 | git 提交 `7ffd1dc0`(迭代 30 卡① 合并) | +| F8 | 近期闸基线:冒烟 1151/0、单测 51/51(34 套件),载于迭代 30 卡① 合并提交 `7ffd1dc0` 的回执读数(实现报告已退役,git 历史可考);T0 复测(2026-10-21):冒烟 1156/0、单测 35 套件全绿、probe --click 全绿 | git 提交 `7ffd1dc0`(迭代 30 卡① 合并) | ## 3. 设计契约(红线,违反 = L3 停机呈报) @@ -169,7 +169,7 @@ |---|---|---|---|---|---| | sv1 三槽承载+位间隔离(契约1/2、V1、T1) | `game/core/save_manager.gd:15` `var _path` 单路径 → 三槽路径派生+当前激活位字段(机制取 §9 第 1 条) | 冒烟 `save_slot_roundtrip`〔sv1〕落 `game/smoke/smoke_all.gd` + 单元 `game/tests/unit/core/save_slot_isolation_test.gd`〔sv1〕 | 接口 | `SaveManagerCore` 实例的按槽 API 读写往返,不经 UI | 变异=三槽路径派生写成同一文件 → 三槽往返一致断言红(T3 变异①) | | sv2 启动态不读盘(完成定义3、V1、T1) | `save_manager.gd:21` `_init` 默认参 `"user://save.json"` 取消+初态 `_data`=`_default_data` | 单元 `game/tests/unit/core/save_boot_no_activate_test.gd` boot 三断言〔sv2〕 | 单元 | 组合根 boot 链起的 `/root/SaveManager`(`game/project.godot:21` autoload 序) | 变异=`_ready` 内偷调 `load_from_disk` → boot 断言红(T3 变异④) | -| sv3 无激活态写盘一律拒绝并返回 false(契约6、V1、T1/T2) | `save_manager.gd:188-202` `write_to_disk` 入口加激活态判定 | 单元〔sv3〕「拒写+返 false+盘面无新文件」三断言,同上落点 | 接口 | `MetaProgression.settle_adventure()`(`meta_progression.gd:284`)无激活态触达写盘 | 撤销=去掉判定放行旧入口 → 拒写断言红 | +| sv3 无激活态写盘一律拒绝并返回 false(契约6、V1、T1/T2) | `save_manager.gd:188-202` `write_to_disk` 入口加激活态判定 | 单元〔sv3〕「拒写+返 false+盘面无新文件」三断言,同上落点 | 接口 | `MetaProgression.settle_adventure()`(`meta_progression.gd:275`,写盘 :282)无激活态触达写盘 | 撤销=去掉判定放行旧入口 → 拒写断言红 | | sv4 新档原语(完成定义2、V2、T2) | 新增空槽创建,初值取 `_default_data` 全零可玩纪律 | 冒烟段〔sv4〕落 `smoke_all.gd` 新档子段 | 接口 | 空槽调新档 API 后按域读回 | 变异=新档写成读旧档 → 全零可玩断言红 | | sv5 进档原语(V3、T2) | 进档=激活+读盘,复用 `load_from_disk`(`save_manager.gd:150-183`)按槽成立 | 冒烟段〔sv5〕 | 接口 | 槽 N 有档 → 进档 API → 读回=该槽档案内容 | 变异=进档只置激活指针不读盘 → 读回断言红 | | sv6 删档原语(契约8、V4、T2) | 新增删除:文件删除+该槽回合法空态+激活指针连清除+无 `.tmp` 残留 | 冒烟段〔sv6〕含文件系统存在性断言 | 集成 | 激活槽调删档 API,再经访问器读该槽+盘面核查 | 变异=漏删文件 → 在盘断言红(T3 变异②) | @@ -177,7 +177,7 @@ | sv8 窥视三态零副作用(契约9、V5、T2) | 窥视对损坏/高版本槽返回显式损坏标记,禁触发 `_corrupt_fallback`(`:443-450`)改名 | 冒烟段〔sv8〕窥视前后文件字节与 mtime 双比对 | 接口 | 盘上坏槽夹具 → 窥视 API | 变异=窥视改走正式 load 路径 → 字节/mtime 断言红 | | sv9 迁移链各槽成立(契约3、V6、T1/T3) | v1→v7 六件迁移链(`:25-30` 自注册)不动,执行面按槽独立成立 | 冒烟段〔sv9〕三槽各喂旧版本档→各读回相等 | 接口 | 各槽 `load_from_disk` | 变异=迁移只跑激活槽 → 他槽读回断言红 | | sv10 切槽=同实例整体换 `_data` 与路径(契约6、V1、T1) | 激活切换取 `attach_path` 先例(`:42-46`);`MetaProgression.save` 引用(`meta_progression.gd:21-25`/`:54`)禁换实例 | 单元〔sv10〕切槽前后引用同一性+写盘落当前槽 | 集成 | `MetaProgression` 真实宿主链驱动切槽 | 变异=切槽新建实例 → 同一性断言红;写成落错槽 → 位间隔离断言红(T3 变异⑤) | -| sv11 测试隔离面保留(契约7、V7、T0/T3) | `attach_path` 与注入路径语义钉为「注入=激活槽文件,无激活态时注入即激活并重读」——`smoke_all.gd:5701-5708` 18 条、`:6499-6511` 15 条、`probe_ui_util.gd:98-121` tag 族行为零变化 | 既有冒烟/探针段全数复跑〔sv11〕+T0 隔离面清点成文件核 | 集成 | 各探针 `isolate_save()` 与 `attach_path` 现有调用面 | 撤销=注入路径改成忽略 → 依赖隔离的既有断言成片红(成片红即本行证明) | +| sv11 测试隔离面保留(契约7、V7、T0/T3) | `attach_path` 与注入路径语义钉为「注入=激活槽文件,无激活态时注入即激活并重读」——`smoke_all.gd:5697-5704` 18 条、`:6543-6553` 15 条、`probe_ui_util.gd:98-121` tag 族行为零变化 | 既有冒烟/探针段全数复跑〔sv11〕+T0 隔离面清点成文件核 | 集成 | 各探针 `isolate_save()` 与 `attach_path` 现有调用面 | 撤销=注入路径改成忽略 → 依赖隔离的既有断言成片红(成片红即本行证明) | | sv12 旧档处置 Q1 甲(契约4、V8、T0) | 首次启动把 `user://save.json` 原样迁入槽 1;幂等=槽 1 已存在不重迁;旧档处置按 §10 Q1「执行前置检查」列逐项 | 冒烟段〔sv12〕迁移前后逐域相等+槽 2/3 空+处置字节留痕件核 | 接口 | 盘上放旧档夹具跑启动链迁移 | 变异=去掉幂等判据 → 二次启动重复迁移断言红;损坏+高版本叠加等意外形态路由 §8 L3 呈报 | | sv13 不升 schema 版本(契约3、完成定义4)——非行为改动 | `SAVE_FORMAT_VERSION := 7`(`:9`)与 `_normalize`(`:458-471`)零改动;确需新键按「v6 域后扩列不升版」先例登记 | 既有版本与高版本拒读断言复跑〔sv13〕 | 单元 | 既有冒烟版本段 | 判断依据=本行不改任何行为;撤销它=升版即撞既有高版本拒读断言(该断言已由 sv9 面钉住) | | sv14 反哺点清单(V9)——非行为改动 | §5 所列文档陈述(存档序列化.md `:61`/`:20`/`:165` 等)随实现换代,本批只列点不落文 | 件核对表〔sv14〕落 `reports/iter33-反哺点-<完成日>.md` | 件核 | 收口前逐条指认落点 | 判断依据=产出物件核不产码;§5 任一条缺落点即 T4 收口不过 | @@ -218,7 +218,7 @@ ### Q2 · 交付序列:本卡可否先于卡③落地(必裁,未裁不定排期) -**背景**:见 §0。卡③未到位前无生产激活入口;旧入口在无激活态下写盘全被拒且 `settle_adventure` 忽略写盘返回值(meta_progression.gd:284)→进度静默丢失;冒烟探针走隔离路径不会红,机检放控不住。 +**背景**:见 §0。卡③未到位前无生产激活入口;旧入口在无激活态下写盘全被拒且 `settle_adventure` 忽略写盘返回值(meta_progression.gd:275,写盘 :282)→进度静默丢失;冒烟探针走隔离路径不会红,机检放控不住。 | 案 | 内容 | 代价 | |---|---|---| diff --git a/reports/需求卡片-选档界面新建批-2026-10-18.md b/reports/需求卡片-选档界面新建批-2026-10-18.md index ffb05aab..dd2309e1 100644 --- a/reports/需求卡片-选档界面新建批-2026-10-18.md +++ b/reports/需求卡片-选档界面新建批-2026-10-18.md @@ -40,6 +40,8 @@ | F7 | 摘要三项读源:现役单档访问器(`keystone_balance`/`adventure_total`/`adventure_cleared`,save_manager.gd:214/258/264)+**按槽窥视读器=卡① T2 交付物(未实现)**;「通关次数」口径=**通关局数 `adventure_cleared`**(同生涯页行,卡① F4 已锁——禁与「各关通关次数 clear_count」混义) | `save_manager.gd:214`/:258/:264;卡① §6 T2 | | F8 | UI 公共件纪律先例(迭代 17 卡③):「复用现行公共件与色板,零新素材零新色值」——本卡同类批,照此执行 | `roadmap.md` 迭代 17 ③行 | +**T0 对齐结论(2026-10-21,卡① 实测交付形态,消契约 9 落空项)**:窥视读器=`peek_slot_summary(slot: int) -> Dictionary`(另有路径注入口 `peek_summary_at(path)` 供探针隔离,契约 7 同款);返回结构=`{"readable": bool, "keystone": int, "cleared": int, "total": int}`;空槽(主文件不在盘,含仅 .tmp 残留)=`readable=true`+三项零值;不可读态(损坏/高版本/缺版本字段/越界槽号)=`readable=false`(Q2 裁甲:UI 一律按空槽呈现,禁自设第三态)。激活与档操作 API:`active_slot() -> int`(0=无激活态)、`create_slot(slot)`/`activate_slot(slot) -> bool`(失败自动恢复切换前三态)/`delete_slot(slot) -> bool`;位数常量=`SaveManagerCore.SLOT_COUNT=3`;槽路径唯一派生口=`slot_path(slot)`,视图/组合根禁自拼槽文件名;测试夹具隔离走 `attach_slot_template(template)` 注入口。 + ## 3. 设计契约(红线,违反 = L3 停机呈报) 🔴 **契约 1 · 三存档位且仅三位**:界面呈现 3 槽(裁决 2);禁参数化槽位列表、禁滚动长列表形态。