fix(09): add A-5-1 to 09-13 non-ALT connected-elimination list

This commit is contained in:
unanmed 2026-10-11 20:06:34 +08:00
parent bbd8674455
commit a91fb7a165
4 changed files with 1418 additions and 0 deletions

View File

@ -0,0 +1,377 @@
---
phase: 09-minimum-browser-runnable
plan: 13
type: execute
wave: 10
depends_on: [09-12]
files_modified:
- packages-user/client-modules/src/fallback/index.ts
- packages-user/client-modules/src/fallback/audio.ts
- packages-user/client-modules/src/fallback/ui.ts
- packages-user/client-modules/src/fallback/weather.ts
- packages-user/client-modules/src/fallback/load.ts
- packages-user/client-modules/src/index.ts
- packages-user/client-modules/src/ui/save.tsx
- packages-user/client-modules/src/ui/settings.tsx
- .planning/phases/09-minimum-browser-runnable/09-13-SUMMARY.md
autonomous: false
requirements: []
estimate:
tokens: 42000
raw_tokens: 47000
tasks: 3
confidence: low
must_haves:
truths:
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03)**:Task 0 必须先汇报「本计划 = 任务 7 · 第一批改造(ALT 条目)· 4 个计划中的第 1 个(回退层去除 + 其调用点)」的要做什么 / 为什么 / 大致怎么做 / 影响范围,**必须列明:本批 = `09-LEGACY-REPLACEMENT-AUDIT.md` 中 40 条 `有替代方案`(ALT);本计划 = 拆分后的第 1 组(12 条);逐条 ALT 条目表(ID / 原接口成员 / 替代方案 / 涉及文件:行 / 编辑要点);逐文件影响表;编辑策略;命名影响(是否新增命名);开放项 OI-1..OI-4**,然后必须**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行本计划任何一步、不得派发执行子代理,且不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行。"
- "**D-50(范围):** 本计划实现 `09-LEGACY-REPLACEMENT-AUDIT.md` 中判定为 `有替代方案`(ALT,共 40 条)的**第一批改造**之**第 1 组**(12 条:A-7-1 / A-7-3 / A-7-4 / A-7-5 / A-1-2 / A-1-4 / A-1-5 / A-4-1 / A-4-2 / A-4-3 / C:drawTip / C:playSound),依据文档已给出的替代方案实施**真实代码改造**。"
- "**D-51(文件清单):** 计划与执行中**准确记录涉及的源文件清单**(改动了哪些文件 / 位置),便于后续审阅——本计划 `files_modified` 精确列举全部被改 / 删的源文件,并给出逐文件影响表。"
- "**D-52(依据):** 严格依据 `09-LEGACY-REPLACEMENT-AUDIT.md` 的 ALT 条目及其替代方案 / `file:line` 锚点;**不 invent 替代方案**;若执行日审计锚点已失效,不得猜测,须记录并按真实代码调整。"
- "**D-53(其它分类不在本批):** `可直接替换` / `有替代但需适配` / `无替代` / `待定` 分类的条目**一律不在**本计划**主动**改动范围;本计划只**主动改** ALT 条目。"
- "**D-53 批次边界可审计(整文件删除的连带消除):** 删除 `fallback/audio.ts` 与 `fallback/load.ts` **整文件**会**必然连带消除**其中的非 ALT 分类用法——`audio.ts`:A-7-2(ADAPT,回退本体文件)/ A-1-3(TBD,`mainSetting`)/ A-3-2(ADAPT,`client` 导入)/ A-3-3(ADAPT,`client` 解构)/ A-5-1(TBD,`mainSetting.getValue('audio.bgmEnabled')` / `fallback/audio.ts:31`)/ C:getMappedName(NONE);`load.ts`:C:maps / C:material / C:tilesets / C:floorIds / C:floors(ADAPT)、C:icons(TBD)。这些**不是**「顺手改」,而是整文件删除的**必然后果**,须在 Task 0 逐条汇报并由用户在关卡**批准**(或据用户意见退出修订),执行后逐条写入 `09-13-SUMMARY.md`,使 D-53 的批次边界可审计。"
- "**回退层去除(A-7 / A-1 / A-4):** 删除 `packages-user/client-modules/src/fallback/`(`index.ts` / `audio.ts` / `ui.ts` / `weather.ts` / `load.ts`),并从 `client-modules/src/index.ts` 移除对回退层的导入与 `export * from './fallback'`;`create()` 不再调用 `patchAll()`。"
- "**调用点改直连(C:drawTip / C:playSound):** `core.drawTip` 与 `core.playSound` 的调用点在删除回退层**之前或同一计划内**被替换为新接口(`TipStore.drawTip` / `SoundPlayer.play`),保证删除 patch 定义后不存在悬空调用。"
- "**范围零外溢 + 依赖单向 + 静态门禁:** 只改 `files_modified` 列举的文件;不改 09-01..09-12 任何文件;不改任何既有注释 / JSDoc;源码行尾为 CRLF;验证一律为**静态断言**(legacy 用法消失 + 替代符号出现 + 文件级作用域 + CRLF + `git status` 只读守卫),**不使用** `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数作门禁。"
artifacts:
- path: packages-user/client-modules/src/index.ts
provides: "去掉回退层接线后的客户端顶层入口(`create()` 只保留 `createRender()`)。"
- path: packages-user/client-modules/src/fallback
provides: "删除:legacy patch 回退层(audio / ui / weather / load / index)。"
- path: packages-user/client-modules/src/ui/save.tsx
provides: "`core.drawTip` 调用点改写为 `TipStore.drawTip`。"
- path: packages-user/client-modules/src/ui/settings.tsx
provides: "`core.playSound` 调用点改写为 `SoundPlayer.play`。"
- path: .planning/phases/09-minimum-browser-runnable/09-13-SUMMARY.md
provides: "本计划执行摘要:本组 ALT 条目、逐文件改动、**非 ALT 分类连带消除清单(整文件删除的后果,经关卡批准)**、命名影响、门禁结果。"
key_links:
- "`fallback/audio.ts`(`new Patch(PatchClass.Control)` 挂载 `playBgm` / `playSound` / …)↔ `packages/audio/src/sound.ts`(`SoundPlayer` / `BGMPlayer`)—— A-1-2 / A-4-1"
- "`fallback/ui.ts`(`new Patch(PatchClass.UI)` 挂载 `drawTip`)↔ `packages-user/client-base/src/components/tip.tsx:148`(`TipStore`)—— A-1-4 / A-4-2 / A-7-3 / C:drawTip"
- "`fallback/weather.ts`(`new Patch(PatchClass.Control)` 挂载 `setWeather`)↔ `packages-user/client-modules/src/render/weather/controller.ts:11`(`WeatherController`)—— A-1-5 / A-4-3 / A-7-4"
- "`fallback/load.ts`(`fallbackLoad`,无调用者)↔ `packages-user/client-base/src/material/manager.ts:37`(`TextureManager`)—— A-7-5"
- "`client-modules/src/index.ts:4-7`(`create()` / `patchAll()`)↔ 移除回退层 —— A-7-1"
- "`ui/save.tsx:531,540`(`core.drawTip`)↔ `tip.tsx:158`(`TipStore.drawTip`)—— C:drawTip"
- "`ui/settings.tsx:431,444`(`core.playSound`)↔ `packages/audio/src/sound.ts:74`(`SoundPlayer.play`)—— C:playSound"
assumptions:
- "**执行日行号可能漂移**:本计划内引用行号为 2026-10-11 规划日实测;门禁以**执行日实测**为准,按「文件存在 + 无 legacy 用法 + 有替代符号」校验,不硬编码行号。"
- "**回退层可去除的前提**:规划日实测 `patchAudio` / `patchWeather` / `patchUI` 仅经 `patchAll()` 在 `create()` 内调用;`fallbackLoad` 无调用者;`core.playBgm` / `core.setWeather` 等 patch 方法在 `client-base` / `client-modules` 内**无调用者**,仅 `core.drawTip`(`save.tsx`)与 `core.playSound`(`settings.tsx`)有调用者(本计划一并替换)。执行日若发现新增调用者,须在 Task 0 汇报中说明并停下。"
- "**用户并发改动(D-56)**:执行期存在用户的 `packages-user/data-state/**` 并发改动(非本计划产物);`git status` 守卫据此豁免这些既有改动。"
- "**Markdown 使用 LF,源码使用 CRLF**:仓库既有 phase 文档为裸 LF;本计划新增文档随此约定;被改源码保持 CRLF(门禁校验)。"
prohibitions:
- "不得在用户明确回复「可以执行」四个字之前执行本计划任何一步、或派发任何执行子代理。"
- "**不得触碰非 ALT 条目**:只改本计划 `files_modified` 列举的 ALT 相关改动;不得顺手改 `可直接替换` / `有替代但需适配` / `无替代` / `待定` 相关的任何用法。"
- "**不得 invent 替代方案 / 不得新增未批准命名**:严格用审计的替代方案(`TipStore` / `SoundPlayer` / `WeatherController` / `TextureManager` / `create()`);若不得不引入新的公共 / 受保护 / 私有命名,须先在 Task 0 呈报并获批准(D-04),否则不得使用。"
- "**不得修改任何既有注释 / JSDoc**(AGENTS.md);不得使用 `import type`;不得新增 `?.`(除 dev.md 允许的两类场景);不得引入 `client` 单例。"
- "不得把门禁建立在「全仓 TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上;不得为「验证」改动源码。"
- "不得改 09-01..09-12 任何文件;不得改 `09-CONTEXT.md`;不得改 `09-LEGACY-REPLACEMENT-AUDIT.md`。"
open_items:
- "**OI-1(批次拆分,D-50/D-51):** 任务 7 的 40 条 ALT 条目按「回退层 + 调用点」「跨包 legacy 依赖」「存档 + UI 打开/控制」「音频/输入/绘制/素材/杂项」拆为 4 个计划(09-13 / 09-14 / 09-15 / 09-16),仍属**同一批次**。**待用户确认**(若用户要求合并为一个计划,须相应上调 context 预算评估)。"
- "**OI-2(回退层去除的连带影响):** 规划日实测 `fallback` 的 patch 方法(`playBgm` / `setWeather` / `getPlayingSounds` 等)在 `client-base` / `client-modules` 内无调用者;执行日若发现调用者(含 `entry-*` / 数据端),须记录并停在 Task 0 请示,**不得**强行删除造成悬空调用。另,删除整文件 `fallback/audio.ts` / `fallback/load.ts` 会**连带消除**其中的非 ALT 分类用法(见 Task 0 ④「非 ALT 分类连带消除清单」),须在关卡逐条呈报并经用户批准,执行后写入 `09-13-SUMMARY.md`。**待用户确认处置原则。**"
- "**OI-3(命名影响):** 本组替换预期**不新增任何公共命名**(复用 `TipStore` / `SoundPlayer` / `create()` 等既有符号)。若执行日不可避免需新增命名,须呈报批准。**待用户确认。**"
- "**OI-4(`SoundPlayer` 获取方式):** C:playSound 的替代 `SoundPlayer.play` 需要实例;规划日 `settings.tsx` 按 D-11 从 props / 主对象取得(不引入 `client` 单例)。具体接入点在执行时按真实代码确定。**待用户确认接入口径。**"
---
<objective>
Phase 9 · 任务 7 · 第一批改造(ALT 条目)· **第 1 组 / 共 4 组** —— **回退层去除 + 其唯一调用点替换**(12 条 ALT)。
**要解决的问题(D-50..D-53):** `09-LEGACY-REPLACEMENT-AUDIT.md` 判定 40 条 legacy 用法为 `有替代方案`(ALT)。本批按审计给出的替代方案实施**真实代码改造**。本计划(09-13)负责其中的**回退层 cluster**:`packages-user/client-modules/src/fallback/**` 是 legacy patch 回退层(把 `core.playBgm` / `core.drawTip` / `core.setWeather` 等挂回 legacy `core`),审计判定其替代为直连新系统(`SoundPlayer` / `TipStore` / `WeatherController` / `TextureManager`),整层可去除;其仅有的两个有调用者的 patch 方法(`drawTip` / `playSound`)的调用点一并改直连。
**大致解决方案(本计划 = 09-13):**
- **Task 0(`blocking-human`)= 预执行汇报关卡**:汇报本组范围、逐条 ALT 表、逐文件影响表、编辑策略、命名影响、OI-1..OI-4,等待「可以执行」。
- **Task 1 = 删除回退层 + 断线**:删除 `fallback/**`(5 文件);`client-modules/src/index.ts` 去掉 `patchAll` 导入、`patchAll()` 调用、`export * from './fallback'`;连带 A-7-1 / A-7-3 / A-7-4 / A-7-5 / A-1-2 / A-1-4 / A-1-5 / A-4-1 / A-4-2 / A-4-3(10 条)。
- **Task 2 = 调用点改直连**:`ui/save.tsx`(`core.drawTip` → `TipStore.drawTip`)、`ui/settings.tsx`(`core.playSound` → `SoundPlayer.play`);连带 C:drawTip / C:playSound(2 条)。
**为何先改调用点再删层:** 删除 patch 定义后若调用点未替换会悬空;Task 1 与 Task 2 同属本计划、同批次提交,保证「删定义」与「换调用」成对落地。
**API coverage:** `detected:false`。**`No external API integration: 本计划仅为源码改造,不涉及任何外部 API。`**
</objective>
<execution_context>
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
@.planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md
@.planning/phases/09-minimum-browser-runnable/09-LEGACY-INTERFACES-AUDIT.md
@AGENTS.md
@dev.md
</context>
## ALT 条目清单与影响表(本组 12 条)
| ID | 原接口/成员(锚点) | 替代方案(锚点) | 涉及文件:行 | 编辑要点 |
| --- | --- | --- | --- | --- |
| A-7-1 | `fallback/index.ts` `patchAll()`(fallback/index.ts:5) | `create()`(client-modules/src/index.ts:4) | client-modules/src/index.ts:1,5,10;fallback/index.ts:1-9 | 去掉 `patchAll` 导入与调用、`export * from './fallback'`;删除 fallback/index.ts |
| A-1-2 | `Patch`/`PatchClass`(fallback/audio.ts:1) | `Hookable`/`HookController`(packages/common/src/hook.ts:3) | fallback/audio.ts:1,10 | 删除该文件(legacy patch 机制随回退层去除) |
| A-4-1 | `new Patch(PatchClass.Control)`(fallback/audio.ts:10) | `SoundPlayer`/`BGMPlayer`(packages/audio/src/sound.ts:6) | fallback/audio.ts:10-70 | 删除 patch 注册(`playBgm`/`playSound`/… 由新音频对象承担) |
| A-1-4 | `Patch`/`PatchClass`(fallback/ui.ts:1) | `TipStore`(tip.tsx:148) | fallback/ui.ts:1,5 | 删除该文件 |
| A-4-2 | `new Patch(PatchClass.UI)`(fallback/ui.ts:5) | `TipStore`(tip.tsx:148) | fallback/ui.ts:5-10 | 删除 patch 注册(`drawTip` 改由 `TipStore` 直连,见 C:drawTip) |
| A-7-3 | `fallback/ui.ts` | `TipStore`(tip.tsx:148) | fallback/ui.ts:1-11 | 删除该文件 |
| A-1-5 | `Patch`/`PatchClass`(fallback/weather.ts:1) | `WeatherController`(controller.ts:11) | fallback/weather.ts:1,8 | 删除该文件 |
| A-4-3 | `new Patch(PatchClass.Control)`(fallback/weather.ts:8) | `WeatherController`(controller.ts:11) | fallback/weather.ts:8-22 | 删除 patch 注册(`setWeather` 改由 `WeatherController` 直连) |
| A-7-4 | `fallback/weather.ts` | `WeatherController`(controller.ts:11) | fallback/weather.ts:1-23 | 删除该文件 |
| A-7-5 | `fallback/load.ts` | `TextureManager`(manager.ts:37) | fallback/load.ts:1-137 | 删除该文件(`fallbackLoad` 无调用者) |
| C:drawTip | `core.drawTip`(save.tsx:531) | `TipStore.drawTip`(tip.tsx:158) | ui/save.tsx:531,540 | 改直连 `TipStore` 实例的 `drawTip` |
| C:playSound | `core.playSound`(settings.tsx:431) | `SoundPlayer.play`(packages/audio/src/sound.ts:74) | ui/settings.tsx:431,444 | 改经主对象取得的声音播放器 `play` |
## 逐文件影响表(本组)
| 文件 | 动作 | 改动内容 | 关联 ALT |
| --- | --- | --- | --- |
| `packages-user/client-modules/src/fallback/index.ts` | 删除 | 删除 `patchAll()` 及三个 patch 导入 | A-7-1 |
| `packages-user/client-modules/src/fallback/audio.ts` | 删除 | 删除 legacy 音频 patch 层(整文件删除**连带消除** A-7-2 / A-1-3 / A-3-2 / A-3-3 / A-5-1 / C:getMappedName,见下「非 ALT 分类连带消除清单」) | A-1-2 / A-4-1 |
| `packages-user/client-modules/src/fallback/ui.ts` | 删除 | 删除 legacy UI(drawTip)patch 层 | A-1-4 / A-4-2 / A-7-3 |
| `packages-user/client-modules/src/fallback/weather.ts` | 删除 | 删除 legacy 天气 patch 层 | A-1-5 / A-4-3 / A-7-4 |
| `packages-user/client-modules/src/fallback/load.ts` | 删除 | 删除 legacy 素材加载适配层(整文件删除**连带消除** C:maps / C:icons / C:material / C:tilesets / C:floorIds / C:floors,见下「非 ALT 分类连带消除清单」) | A-7-5 |
| `packages-user/client-modules/src/index.ts` | 修改 | 去掉 `patchAll` 导入 / 调用 / `export * from './fallback'` | A-7-1 |
| `packages-user/client-modules/src/ui/save.tsx` | 修改 | `core.drawTip` → `TipStore.drawTip` | C:drawTip |
| `packages-user/client-modules/src/ui/settings.tsx` | 修改 | `core.playSound` → `SoundPlayer.play` | C:playSound |
## 非 ALT 分类连带消除清单(整文件删除的后果,须用户批准)
> **与 D-53 的关系:** 本计划只**主动改** ALT 条目;但删除 `fallback/audio.ts` 与 `fallback/load.ts` 这两个**整文件**,会**连带消除**两个文件内**非 ALT 分类**(`有替代但需适配` / `无替代` / `待定`)的用法。这些**不是**「顺手改」(并非在 ALT 之外另行编辑),而是整文件删除的**必然后果**;故在 Task 0 汇报、逐条列明,由用户在关卡**批准**;执行后逐条写入 `09-13-SUMMARY.md` 以备审计。若用户不接受某条连带消除,须在 Task 0 回复中指明(退出本计划修订)。
| 连带消除项 | 分类 | 所在被删文件:行 | 审计锚点 / 备注 |
| --- | --- | --- | --- |
| A-7-2(`fallback/audio.ts` 音频回退本体) | 有替代但需适配 | fallback/audio.ts(整文件) | A-7-2(替代 `ClientCore` 音频成员 client.ts:44) |
| A-1-3 `mainSetting` | 待定 | fallback/audio.ts:2 | A-1-3(无替代) |
| A-3-2 `client` 导入使用 | 有替代但需适配 | fallback/audio.ts:5 | A-3-2(→ 经 `IClientCore` 参数注入) |
| A-3-3 `client` 解构 `{ bgmPlayer, soundPlayer, audioContext }` | 有替代但需适配 | fallback/audio.ts:11 | A-3-3(→ `ClientCore` 成员 client.ts:44) |
| A-5-1 `mainSetting.getValue('audio.bgmEnabled')` | 待定 | fallback/audio.ts:31 | A-5-1(无替代 / 待定) |
| C:getMappedName | 无替代 | fallback/audio.ts:49 | C:getMappedName(无替代) |
| C:maps | 有替代但需适配 | fallback/load.ts:44,59 | C:maps(→ `MapState` mapState.ts:17) |
| C:icons | 待定 | fallback/load.ts:55 | C:icons(无替代 / 待定) |
| C:material | 有替代但需适配 | fallback/load.ts:56 | C:material(→ `TextureManager` manager.ts:37) |
| C:tilesets | 有替代但需适配 | fallback/load.ts:99 | C:tilesets(→ `TextureManager.tilesetStore`) |
| C:floorIds | 有替代但需适配 | fallback/load.ts:111 | C:floorIds(→ `MapStore` mapStore.ts:3) |
| C:floors | 有替代但需适配 | fallback/load.ts:112 | C:floors(→ `MapState` mapState.ts:17) |
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 7 · 第一批改造(ALT)· 第 1 组:回退层去除 + 其调用点」的范围、逐条 ALT 表、逐文件影响表、命名影响与 OI,等待用户回复「可以执行」</name>
<precondition>`packages-user/client-modules/src/fallback/{index,audio,ui,weather,load}.ts` 存在;`packages-user/client-modules/src/ui/{save,settings}.tsx` 存在;审计文档 `09-LEGACY-REPLACEMENT-AUDIT.md` 已存在(含 `ALT-COUNT: 40`)。</precondition>
<files>(只读汇报,不修改任何文件)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-03 / D-04 / **D-50 / D-51 / D-52 / D-53**)
- .planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md(A 节 ALT 条目、§C 的 `C:drawTip` / `C:playSound`、`## 汇总`)
- .planning/phases/09-minimum-browser-runnable/09-13-PLAN.md(本计划全文,含「ALT 条目清单与影响表」与「逐文件影响表」)
- AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明);dev.md(命名规则、注释规范、依赖单向、CRLF)
</read_first>
<action>
**停下并向用户汇报,等待明确回复「可以执行」后才继续;汇报本身只读。**
① **要解决的问题:** 任务 7 · 第一批改造(ALT 条目)。本批 = 审计判定 `有替代方案` 的 40 条;按替代方案实施真实代码改造。**本计划(09-13)= 拆分后的第 1 组:回退层去除 + 其调用点**(12 条)。
② **输入(D-52):** `.planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md`(`ALT-COUNT: 40`,`## 汇总` / `## 改造顺序建议`)。
③ **逐条 ALT 条目表:** 原样呈现本计划「ALT 条目清单与影响表」的 12 行(ID / 原接口成员 / 替代方案 / 涉及文件:行 / 编辑要点)。
④ **逐文件影响表 + 非 ALT 分类连带消除清单:** 原样呈现本计划「逐文件影响表」的 8 行(文件 / 动作 / 改动内容 / 关联 ALT),并说明 `files_modified` 精确为这 8 个源文件;**并原样呈现「非 ALT 分类连带消除清单」的 12 行**——删除整文件 `fallback/audio.ts` / `fallback/load.ts` 会**连带消除**其中的非 ALT 分类用法(`audio.ts`:A-7-2 / A-1-3 / A-3-2 / A-3-3 / A-5-1 / C:getMappedName;`load.ts`:C:maps / C:icons / C:material / C:tilesets / C:floorIds / C:floors)。**这是整文件删除的必然后果(并非「顺手改」)**;须逐条列明并请用户在本关卡批准;执行后逐条写入 `09-13-SUMMARY.md`。若用户不接受某条连带消除,请在其回复中指明(退出本计划修订)。
⑤ **编辑策略:** Task 1 删除 `fallback/**`(5 文件)并断开 `client-modules/src/index.ts` 的接线(A-7-1/3/4/5、A-1-2/4/5、A-4-1/2/3);Task 2 将 `core.drawTip`(`save.tsx`)与 `core.playSound`(`settings.tsx`)调用点改直连新接口(C:drawTip / C:playSound)。两任务同计划同批次提交,保证「删定义」与「换调用」成对落地。
⑥ **命名影响(D-04):** 预期**不新增任何公共 / 受保护 / 私有命名**(复用 `TipStore` / `SoundPlayer` / `WeatherController` / `TextureManager` / `create()`);若执行日不可避免需新增,须先呈报批准。**OI-3。**
⑦ **范围与禁止:** 只**主动改** ALT 条目相关用法;不**主动**改动 `可直接替换` / `有替代但需适配` / `无替代` / `待定`;但删除 `fallback/audio.ts` / `fallback/load.ts` **整文件**会**连带消除**其中的非 ALT 用法(见 ④「非 ALT 分类连带消除清单」),此为整文件删除的必然后果而非「顺手改」,须经本关卡批准;不改 09-01..09-12;不改既有注释 / JSDoc;不用 `import type`;不新增 `?.`(dev.md 允许的两类场景除外);不引入 `client` 单例;源码保持 CRLF。
⑧ **验证方式:** 静态断言——`fallback/**` 已删除;`client-modules/src/index.ts` 无 `patchAll` 与 `./fallback` 引用;`save.tsx` 无 `core.drawTip` 且出现 `TipStore`;`settings.tsx` 无 `core.playSound` 且出现 `SoundPlayer`;文件级作用域(仅 `files_modified` 被改);CRLF;`git status` 只读守卫。**不设全仓类型 / 构建门禁。**
⑨ **开放项:** OI-1(批次拆分为 09-13..09-16,本批 4 计划,仍属同一批次);OI-2(回退层 patch 方法在 `client-base`/`client-modules` 内无调用者,执行日若发现调用者须停并请示);OI-3(命名影响:预期无新增);OI-4(`SoundPlayer` 获取按 D-11 经 props / 主对象,具体接入点执行时定)。**无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户明确回复「可以执行」四个字**;回复其它内容视为未通过;若用户给出修订意见,退出本计划、修订后重新汇报。
</action>
<decision>是否按本计划(09-13)执行任务 7 · 第一批改造 · 第 1 组:删除 `client-modules/src/fallback/**` 回退层、断开 `index.ts` 接线、并替换 `core.drawTip` / `core.playSound` 调用点为直连新接口(共 12 条 ALT)</decision>
<context>
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = ALT 范围确认(仅 ALT,不碰其它分类)+ 回退层可去除性确认(OI-2)+ 命名影响确认 + 批次拆分确认(OI-1)。行号为规划日实测、可能漂移,以执行日实测为准。
</context>
<options>
<option id="execute-default">
<name>可以执行(采纳全部默认处置)</name>
<pros>回退层整体去除、收益清晰;调用点成对替换避免悬空;预期不新增命名;静态门禁可验证</pros>
<cons>无(回退层 patch 方法无 in-scope 调用者)</cons>
</option>
<option id="execute-keep-callsites">
<name>可以执行 + 仅替换调用点、暂不删除回退层</name>
<pros>更保守</pros>
<cons>回退层保留则 A-7/A-1/A-4 共 10 条无法收口,与 D-50/D-52 不符</cons>
</option>
<option id="revise">
<name>修订分组范围 / 编辑策略后重新汇报</name>
<pros>避免带着错误范围改造</pros>
<cons>须修订 PLAN.md 后重新汇报</cons>
</option>
</options>
<resume-signal>回复「可以执行」四个字(如需调整 OI-1..OI-4 的处置,请在回复中指明;修订意见请按 action ⑨ 处理)</resume-signal>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['packages-user/client-modules/src/fallback/index.ts','packages-user/client-modules/src/fallback/audio.ts','packages-user/client-modules/src/fallback/ui.ts','packages-user/client-modules/src/fallback/weather.ts','packages-user/client-modules/src/fallback/load.ts','packages-user/client-modules/src/index.ts','packages-user/client-modules/src/ui/save.tsx','packages-user/client-modules/src/ui/settings.tsx']){if(!fs.existsSync(p))bad.push('missing: '+p)}const audit='.planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md';if(!fs.existsSync(audit))bad.push('missing audit');else if(!/ALT-COUNT:\s*40/.test(fs.readFileSync(audit,'utf8')))bad.push('audit ALT-COUNT != 40');if(bad.length){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state intact (fallback layer + callsites + audit present, ALT-COUNT=40)')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(回退层 / 调用点 / 审计文档缺失,或审计 ALT-COUNT 非 40)</fails_when>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定非默认的分组 / 编辑策略 / 命名,须在汇报与后续任务中据其指定执行)</human-check>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」四个字
- 汇报内容涵盖:D-50..D-53、本组 12 条 ALT 条目表、逐文件影响表、**非 ALT 分类连带消除清单(整文件删除的后果)**、编辑策略、命名影响(无新增,OI-3)、验证方式(静态断言,无全仓门禁)、OI-1..OI-4
- 汇报中已原样告知:只**主动改** ALT 条目相关用法、回退层整文件删除的连带影响(含非 ALT 连带消除清单,OI-2)、CRLF 与范围零外溢
- 用户已在关卡就「非 ALT 分类连带消除清单」给出批准(或据其意见退出修订)
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
</acceptance_criteria>
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
</task>
<task type="auto">
<name>Task 1: 删除回退层 `client-modules/src/fallback/**` 并断开 `index.ts` 接线(A-7-1/3/4/5、A-1-2/4/5、A-4-1/2/3)</name>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 OI-1..OI-4 另有指定,按指定执行)。</precondition>
<files>packages-user/client-modules/src/fallback/index.ts, packages-user/client-modules/src/fallback/audio.ts, packages-user/client-modules/src/fallback/ui.ts, packages-user/client-modules/src/fallback/weather.ts, packages-user/client-modules/src/fallback/load.ts, packages-user/client-modules/src/index.ts</files>
<read_first>
- packages-user/client-modules/src/fallback/index.ts(`patchAll()` 与三 patch 导入)
- packages-user/client-modules/src/fallback/audio.ts、ui.ts、weather.ts、load.ts(回退层全部内容)
- packages-user/client-modules/src/index.ts(`create()` 调用 `patchAll()` 与 `export * from './fallback'`)
- packages/audio/src/sound.ts(`SoundPlayer` / `BGMPlayer`,替代方案来源,只读参考)
- packages-user/client-base/src/components/tip.tsx(`TipStore`,只读参考)
- packages-user/client-modules/src/render/weather/controller.ts(`WeatherController`,只读参考)
- packages-user/client-base/src/material/manager.ts(`TextureManager`,只读参考)
- .planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md(A-7 / A-1 / A-4 小节)
</read_first>
<action>
(1) **删除回退层文件:** 删除 `packages-user/client-modules/src/fallback/index.ts`、`audio.ts`、`ui.ts`、`weather.ts`、`load.ts`(整目录 `fallback/` 移除)。关联 A-7-1(index)、A-7-3(ui)、A-7-4(weather)、A-7-5(load)、A-1-2 / A-4-1(audio)、A-1-4 / A-4-2(ui)、A-1-5 / A-4-3(weather)。删除 `audio.ts` / `load.ts` **整文件**将**连带消除**其中的非 ALT 分类用法(见下 (5))。
(2) **断开接线(`client-modules/src/index.ts`):** 移除 `import { patchAll } from './fallback'`;在 `create()` 中移除 `patchAll()` 调用(保留 `createRender()`);移除 `export * from './fallback'`。关联 A-7-1(审计替代方案为 `create()`,即新入口不再经 patchAll)。
(3) **不引入新命名:** 复用既有符号;若因删除导致需要对 `create()` 作最小可见性调整,**不得**新增公共命名(D-04);如确有必要,停下并回到 Task 0。
(4) **行尾 / 注释:** 被改文件保持 CRLF;不改任何既有注释 / JSDoc;不使用 `import type`。
(5) **记录连带消除(写入 SUMMARY):** 删除整文件 `fallback/audio.ts` / `fallback/load.ts` 会**连带消除**其中的非 ALT 分类用法(`audio.ts`:A-7-2 / A-1-3 / A-3-2 / A-3-3 / A-5-1 / C:getMappedName;`load.ts`:C:maps / C:icons / C:material / C:tilesets / C:floorIds / C:floors)。**这些不是主动改动,而是整文件删除的必然后果**(D-53 边界);须逐条写入 `09-13-SUMMARY.md` 供审计,并注明已在本计划 Task 0 关卡经用户批准(若用户对该清单另有指定,按指定记录)。
</action>
<reversibility rating="reversible">纯删除 + 入口断线;`git revert` 可整体回退。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];const del=['packages-user/client-modules/src/fallback/index.ts','packages-user/client-modules/src/fallback/audio.ts','packages-user/client-modules/src/fallback/ui.ts','packages-user/client-modules/src/fallback/weather.ts','packages-user/client-modules/src/fallback/load.ts'];for(const p of del){if(fs.existsSync(p))bad.push('still exists: '+p)}const idx='packages-user/client-modules/src/index.ts';const t=fs.readFileSync(idx,'utf8').split(/\r?\n/).filter(l=>!/^\s*\/\//.test(l)).join('\n');if(/patchAll/.test(t))bad.push('index.ts still references patchAll');if(/from '\''\.\/fallback'\''/.test(t))bad.push('index.ts still imports ./fallback');if(!/createRender\s*\(/.test(t))bad.push('index.ts lost createRender call');if(!/\bcreate\s*\(/.test(t))bad.push('index.ts lost create() export');if(bad.length){console.error('FALLBACK GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK fallback layer removed and index.ts de-wired')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "FALLBACK GATE FAILED"(回退层文件仍存在 / `index.ts` 仍引用 `patchAll` 或 `./fallback` / 丢失 `create()` 或 `createRender()`)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['packages-user/client-modules/src/index.ts']){const b=fs.readFileSync(p,'utf8');if(b.indexOf('\r\n')<0)bad.push('not CRLF: '+p)}if(bad.length){console.error('CRLF FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK edited files CRLF')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF FAILED"(被改源码非 CRLF)</fails_when>
<automated>git status --porcelain</automated>
<fails_when>输出包含本计划 `files_modified` 之外、且非用户既有并发改动(`packages-user/data-state/**`)的生产文件改动</fails_when>
</verify>
<acceptance_criteria>
- `packages-user/client-modules/src/fallback/` 下 5 个文件全部删除
- `client-modules/src/index.ts` 不再引用 `patchAll` 或 `./fallback`,`create()` 仍保留并调用 `createRender()`
- 未新增任何公共 / 受保护 / 私有命名;未改既有注释;被改文件为 CRLF
- 整文件删除**连带消除**的非 ALT 用法(A-7-2 / A-1-3 / A-3-2 / A-3-3 / A-5-1 / C:getMappedName / C:maps / C:icons / C:material / C:tilesets / C:floorIds / C:floors)已逐条写入 `09-13-SUMMARY.md`,并注明经 Task 0 关卡批准
- 未改 `files_modified` 外文件(用户并发改动除外)
</acceptance_criteria>
<done>回退层 5 文件删除、`index.ts` 断线完成;非 ALT 连带消除清单写入 SUMMARY;FALLBACK / CRLF / scope 门禁全绿</done>
</task>
<task type="auto">
<name>Task 2: 替换 `core.drawTip` / `core.playSound` 调用点为直连新接口(C:drawTip / C:playSound)</name>
<precondition>Task 1 已完成(回退层已删除);Task 0 已获批。</precondition>
<files>packages-user/client-modules/src/ui/save.tsx, packages-user/client-modules/src/ui/settings.tsx</files>
<read_first>
- packages-user/client-modules/src/ui/save.tsx:520-545(`core.drawTip(validation.message)` 两处)
- packages-user/client-modules/src/ui/settings.tsx:418-455(`core.playSound('confirm.opus')` 两处)
- packages-user/client-base/src/components/tip.tsx:148-170(`TipStore` / `TipStore.drawTip` / `TipStore.get`)
- packages/audio/src/sound.ts:1-90(`SoundPlayer.play`)
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-11 主对象经 props 获取、不得用 `client` 单例)
</read_first>
<action>
(1) **`ui/save.tsx`(C:drawTip):** 将 `core.drawTip(validation.message)`(约 531、540 行)改为经 `TipStore` 的等价提示调用(审计替代方案 = `TipStore.drawTip`,`tip.tsx:158`)。按 `fallback/ui.ts` 原实现 `TipStore.get('main-tip')` 获取实例后调用其 `drawTip`;按 D-11 组件经 props / 主对象取得所需对象,**不引入 `client` 单例**。非空判断遵循 dev.md / R-6(对象用 `!`、字面量用 `isNil`)。
(2) **`ui/settings.tsx`(C:playSound):** 将 `core.playSound('confirm.opus')`(约 431、444 行)改为经主对象取得的声音播放器的 `play`(审计替代方案 = `SoundPlayer.play`,`sound.ts:74`)。按 D-11 从 props / 主对象取得播放器,**不引入 `client` 单例**;保留原传入的音效名 `'confirm.opus'`。
(3) **不 invent 替代:** 严格用审计替代方案;若任一调用点缺少取得目标的合理途径(如 props 未暴露),**停下并回到 Task 0 请示**,不得凭空造接口(D-11 / D-52)。
(4) **不触碰其它分类:** 同文件内的 `core.doSL` / `core.saves` / `core.firstData` / `core.formatDate2` / `core.download` / `core.syncSave` 等属其它分类或其它组,**本任务不改**。
(5) **行尾 / 注释:** 保持 CRLF;不改既有注释 / JSDoc;不使用 `import type`;不新增 `?.`(dev.md 允许场景除外)。
</action>
<reversibility rating="reversible">两处调用点替换;`git revert` 可回退。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];const save='packages-user/client-modules/src/ui/save.tsx';const set='packages-user/client-modules/src/ui/settings.tsx';const code=p=>fs.readFileSync(p,'utf8').split(/\r?\n/).filter(l=>!/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');const s=code(save);const t=code(set);if(/core\.drawTip\s*\(/.test(s))bad.push('save.tsx still calls core.drawTip');if(!/TipStore/.test(s))bad.push('save.tsx: no TipStore usage');if(/core\.playSound\s*\(/.test(t))bad.push('settings.tsx still calls core.playSound');if(!/play\s*\(/.test(t))bad.push('settings.tsx: no play() usage');if(bad.length){console.error('CALLSITE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK drawTip/playSound callsites replaced with new interfaces')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CALLSITE GATE FAILED"(`save.tsx` 仍调用 `core.drawTip` 或无 `TipStore`;`settings.tsx` 仍调用 `core.playSound` 或无 `play(`)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['packages-user/client-modules/src/ui/save.tsx','packages-user/client-modules/src/ui/settings.tsx']){const b=fs.readFileSync(p,'utf8');if(b.indexOf('\r\n')<0)bad.push('not CRLF: '+p);if(/\bimport\s+type\b/.test(b))bad.push('import type found: '+p)}if(bad.length){console.error('CRLF/IMPORT FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK callsite files CRLF and no import type')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF/IMPORT FAILED"(被改源码非 CRLF,或出现 `import type`)</fails_when>
<automated>git status --porcelain</automated>
<fails_when>输出包含本计划 `files_modified` 之外、且非用户既有并发改动(`packages-user/data-state/**`)的生产文件改动</fails_when>
</verify>
<acceptance_criteria>
- `ui/save.tsx` 两处 `core.drawTip` 改为 `TipStore` 直连(无残留 `core.drawTip`)
- `ui/settings.tsx` 两处 `core.playSound` 改为声音播放器 `play`(无残留 `core.playSound`)
- 未引入 `client` 单例、未新增未批准命名;未改其它分类用法
- 被改文件为 CRLF、无 `import type`;未改 `files_modified` 外文件(用户并发改动除外)
</acceptance_criteria>
<done>drawTip / playSound 调用点替换完成;CALLSITE / CRLF / scope 门禁全绿;12 条 ALT 全部落地</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 范围边界 | 只**主动改** ALT 条目相关用法;不主动触碰 DIRECT / ADAPT / NONE / TBD(D-53);整文件删除的**非 ALT 连带消除**须在 Task 0 呈报获批(见「非 ALT 分类连带消除清单」) |
| 替代依据边界 | 替代方案只取自审计,且来自非 legacy 包;不 invent 接口(D-52) |
| 命名边界 | 未经批准不得新增公共 / 受保护 / 私有命名(D-04) |
| 文件边界 | 仅允许改 `files_modified` 的源文件 + 写 `09-13-SUMMARY.md` |
| 提交边界 | 不得把用户并发改动(`data-state/**`)纳入暂存 / 提交 |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-13-01 | Tampering | 删除回退层后遗留悬空调用(`core.drawTip` / `core.playSound`) | high | mitigate | Task 1 + Task 2 同计划成对落地;CALLSITE 门禁校验 legacy 调用消失 |
| T-09-13-02 | Elevation of Privilege | 未经批准即执行 / 使用未批准命名 | high | mitigate | Task 0 `blocking-human`;prohibitions 禁未获批命名(D-04) |
| T-09-13-03 | Tampering | 主动越界改动非 ALT 分类用法(顺手改 ADAPT / TBD) | high | mitigate | 范围门禁(仅 `files_modified`)+ prohibitions + `git status` 守卫;连带消除与主动改严格区分 |
| T-09-13-08 | Repudiation | 整文件删除**连带消除**非 ALT 用法但未呈报 / 未写入 SUMMARY,致 D-53 批次边界不可审计 | high | mitigate | Task 0 ⑦ 明列「非 ALT 分类连带消除清单」并请用户批准;Task 1 (5) 要求逐条写入 `09-13-SUMMARY.md` |
| T-09-13-04 | Repudiation | 误删仍有调用者的 patch 方法 | high | mitigate | Task 0 OI-2 明示前置(无 in-scope 调用者);precondition 校验;执行日发现调用者须停并请示 |
| T-09-13-05 | Tampering | 修改既有注释 / JSDoc 或引入 `import type` | medium | mitigate | prohibitions + CRLF/IMPORT 门禁 |
| T-09-13-06 | Information Disclosure | 把用户并发改动(`data-state/**`)误纳入提交 | medium | mitigate | `git status` 守卫豁免既有并发改动;提交仅暂存 `files_modified` |
| T-09-13-07 | Repudiation | 未跑静态门禁即提交 | medium | mitigate | Task 1/2 各有自动化门禁 + SUMMARY;未通过不得提交 |
| T-09-13-SC | Tampering | npm/pip/cargo installs | low | accept | 无第三方包安装、无依赖安装 |
</threat_model>
<verification>
- **Task 1**:`fallback/**` 5 文件删除;`client-modules/src/index.ts` 无 `patchAll` / `./fallback`,保留 `create()` / `createRender()`;非 ALT 连带消除清单写入 `09-13-SUMMARY.md`;CRLF;范围门禁。
- **Task 2**:`save.tsx` 无 `core.drawTip` 且有 `TipStore`;`settings.tsx` 无 `core.playSound` 且有 `play(`;CRLF;无 `import type`;范围门禁。
- **范围零外溢**:不**主动**触碰非 ALT 分类用法(整文件删除的**连带消除**除外,须经 Task 0 关卡批准并写入 SUMMARY);不改 09-01..09-12;不改既有注释。
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
- **人工复核**:结论写入 `09-13-SUMMARY.md`(含逐文件改动、**非 ALT 分类连带消除清单**、命名影响、门禁结果)。
</verification>
<success_criteria>
- 本组 12 条 ALT 全部落地:回退层 `fallback/**` 删除、`index.ts` 断线(10 条)+ `core.drawTip` / `core.playSound` 调用点改直连(2 条)。
- 替代方案严格取自审计(`TipStore` / `SoundPlayer` / `WeatherController` / `TextureManager` / `create()`),无 invent。
- 零越界:不**主动**触碰其它分类、不新增未批准命名、不改既有注释;整文件删除的**非 ALT 连带消除**已在 Task 0 呈报获批并写入 SUMMARY。
- 静态门禁(回退层 / 调用点 / CRLF / 范围)全绿。
</success_criteria>
## Artifacts this phase produces
| 符号 / 路径 | 类型 | 说明 |
|---|---|---|
| `packages-user/client-modules/src/fallback/**`(5 文件) | 删除 | legacy patch 回退层 |
| `packages-user/client-modules/src/index.ts`(修改) | 源码 | 去回退层接线后的客户端入口 |
| `packages-user/client-modules/src/ui/save.tsx`(修改) | 源码 | `core.drawTip` → `TipStore.drawTip` |
| `packages-user/client-modules/src/ui/settings.tsx`(修改) | 源码 | `core.playSound` → `SoundPlayer.play` |
| `.planning/phases/09-minimum-browser-runnable/09-13-SUMMARY.md`(新增) | 执行摘要 | 本组 ALT、逐文件改动、**非 ALT 分类连带消除清单**、命名影响、门禁结果 |
**命名说明:** 本计划**不新增任何源码命名**(复用既有符号);如执行日不可避免需新增,须先获用户批准。
## Source Coverage Audit
| 来源 | 项 | 处置 |
|------|----|------|
| GOAL | Phase 9(ROADMAP)「完成渲染端框架,产出可在浏览器运行并验证的最小版本」 | COVERED(增量·任务 7 只读判定的落地子切片:去 legacy 回退层) |
| REQ | Phase 9 ROADMAP「Requirements: TBD」,无需求 ID | N/A(`requirements: []` 如实标注) |
| RESEARCH | 无 RESEARCH.md | N/A |
| CONTEXT | D-50(第一批 = 40 条 ALT) | COVERED(本组 12/40;其余见 09-14..09-16) |
| CONTEXT | D-51(准确记录源文件清单) | COVERED(`files_modified` + 逐文件影响表) |
| CONTEXT | D-52(依据审计替代方案,不 invent) | COVERED(prohibitions + `<action>`) |
| CONTEXT | D-53(其它分类不在本批) | COVERED(范围门禁 + prohibitions) |
| CONTEXT | D-03 / D-04(执行前获批 / 命名获批) | COVERED(Task 0 + prohibitions) |
| 缺口 | 09-14 / 09-15 / 09-16 承接其余 28 条 ALT | DEFERRED(同批次后续计划) |
<output>
Create `.planning/phases/09-minimum-browser-runnable/09-13-SUMMARY.md` when done
</output>

View File

@ -0,0 +1,327 @@
---
phase: 09-minimum-browser-runnable
plan: 14
type: execute
wave: 11
depends_on: [09-13]
files_modified:
- packages-user/client-modules/src/action/hotkey.ts
- packages-user/client-base/src/elements/cache.ts
- packages-user/client-base/src/elements/misc.ts
- .planning/phases/09-minimum-browser-runnable/09-14-SUMMARY.md
autonomous: false
requirements: []
estimate:
tokens: 30000
raw_tokens: 34000
tasks: 3
confidence: low
must_haves:
truths:
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03)**:Task 0 必须先汇报「本计划 = 任务 7 · 第一批改造(ALT)· 4 个计划中的第 2 个(跨包 legacy 依赖:GameStorage / Mota.require / 全局 core 收口)」的要做什么 / 为什么 / 大致怎么做 / 影响范围,**必须列明:逐条 ALT 条目表(ID / 原接口成员 / 替代方案 / 涉及文件:行 / 编辑要点);逐文件影响表;两个执行日必须核实的风险点(R1:`@user/data-base` 当前未导出 `loading`,审计 A-6 替代锚点疑似失效;R2:`GameStorage` → `SaveSystem` 的实例获取路径与语义差异);命名影响;开放项 OI-1..OI-4**,然后必须**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行本计划任何一步、不得派发执行子代理,且不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行。"
- "**D-50 / D-52(范围 / 依据):** 实现本批 ALT 之**第 2 组**(A-1-1 / A-6-1 / A-6-2 / A-6-3 / A-2-1,共 5 条)的替代改造;严格依据 `09-LEGACY-REPLACEMENT-AUDIT.md` 的替代方案与 `file:line` 锚点,**不 invent**。"
- "**D-53(其它分类不在本批):** 不触碰 `可直接替换` / `有替代但需适配` / `无替代` / `待定`;本组只改 ALT 条目。"
- "**R1(审计锚点核实):** 规划日实测 `packages-user/data-base` 的 barrel(`src/index.ts`:`./enemy` `./flag` `./hero` `./map` `./shop` `./types`)**未导出 `loading`**;`Mota.require('@user/data-base').loading` 的 `loading`(`once('loaded'|'coreInit', …)` / `loaded` 形态)在新 data-base 中**无当前承载**。执行日必须**先核实** `loading` 的真实承载(如新加载器 `packages-user/data-state/src/loader/loader.ts` 的 `MotaDataLoader` 的 `loadedPromise` / 加载 hook);若确实无等价替代,**记录锚点失效并 STOP 请示**,不得猜测造接口。"
- "**R2(替代可行性核实):** A-1-1 审计替代为 `SaveSystem`(`save/system.ts:41`);`GameStorage` 为 localStorage 键值存储、`SaveSystem` 为 Dexie 存档 / 全局存储(语义与实例获取路径不同)。执行日必须核实 `hotkey.ts` 取得 `SaveSystem` 实例的**合规路径**(D-11:不得引入 `client` 单例);若无法合规取得,**记录并 STOP 请示**。"
- "**A-2-1(umbrella)部分收口:** 全局 `core`(`src/types/declaration/core.d.ts:1302`)的替代为 `CoreState`(`packages-user/data-state/src/core.ts:76`);本计划只收口本组 ALT 用法,**不删除 `core` 全局声明**;全量收口须待其余分类(ADAPT / NONE / TBD)完成后另行规划。**`09-14-SUMMARY.md` 必须逐条列明本计划 REPLACED(已收口)的对象与 DEFERRED(未收口 / 待其它批次)的对象**——含 `client-modules/src/core.ts:6` 的 `core` 单例定义如何处置(本计划未编辑该文件、未改其定义)——使 40 条 ALT 的整体覆盖可审计。"
- "**范围零外溢 + 静态门禁:** 只改 `files_modified`;不改既有注释 / JSDoc;不用 `import type`;源码 CRLF;验证一律静态断言(legacy 用法消失 + 替代符号出现 + CRLF + 范围 / `git status` 守卫),**不使用** `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。"
artifacts:
- path: packages-user/client-base/src/elements/cache.ts
provides: "`createCache()` 的 `Mota.require('@user/data-base')` 改为静态模块导入(`loading` 承载按执行日核实)。"
- path: packages-user/client-base/src/elements/misc.ts
provides: "两处 `Mota.require('@user/data-base')` 改为静态模块导入(`loading` 承载按执行日核实)。"
- path: packages-user/client-modules/src/action/hotkey.ts
provides: "`GameStorage` 用法替换为 `SaveSystem` 全局存储(实例路径按执行日核实)。"
- path: .planning/phases/09-minimum-browser-runnable/09-14-SUMMARY.md
provides: "本计划执行摘要:本组 ALT 条目、逐文件改动、R1/R2 核实结论、**A-2-1 收口覆盖(REPLACED vs DEFERRED 逐条,含 `core.ts:6` 单例定义处置)**、命名影响、门禁结果。"
key_links:
- "`packages-user/client-base/src/elements/cache.ts:491` / `misc.ts:89,219`(`Mota.require('@user/data-base')`)↔ `packages-user/data-base/src/index.ts:1`(静态导入)—— A-6-1/2/3"
- "`packages-user/data-base/src/index.ts`(未导出 `loading`)↔ `packages-user/data-state/src/loader/loader.ts`(`MotaDataLoader` 加载信号)—— R1 核实点"
- "`packages-user/client-modules/src/action/hotkey.ts:3,518-522`(`GameStorage`)↔ `packages-user/client-base/src/save/system.ts:41`(`SaveSystem` 全局存储)—— A-1-1"
- "`src/types/declaration/core.d.ts:1302`(全局 `core`)↔ `packages-user/data-state/src/core.ts:76`(`CoreState`)—— A-2-1"
assumptions:
- "**执行日行号可能漂移**:引用行号为 2026-10-11 规划日实测;门禁以执行日实测为准。"
- "**R1 为规划日已发现的风险**:`@user/data-base` 未导出 `loading`(规划日 grep 全仓确认);审计 A-6 替代锚点疑似失效,执行日须核实并按真实代码调整,必要时 STOP。"
- "**用户并发改动(D-56)**:执行期存在用户 `packages-user/data-state/**` 并发改动;`git status` 守卫豁免这些既有改动。"
- "**Markdown 使用 LF,源码使用 CRLF**。"
prohibitions:
- "不得在用户明确回复「可以执行」四个字之前执行本计划任何一步、或派发任何执行子代理。"
- "**不得 invent 替代 / 不得猜测 `loading` 承载**:若 `loading` 无当前等价替代,记录锚点失效并 STOP,不得造出未存在的符号。"
- "**不得引入 `client` 单例**;不得为取得实例而新增未批准命名(D-04)。"
- "不得触碰非 ALT 分类用法;不得改 09-01..09-13 任何文件;不得改既有注释 / JSDoc;不得用 `import type`;不得新增 `?.`(dev.md 允许场景除外)。"
- "不得把门禁建立在「全仓 TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上;不得为「验证」改动源码。"
open_items:
- "**OI-1(批次拆分):** 本计划为任务 7 批次 4 计划中的第 2 个;**待用户确认**(可合并 / 重排)。"
- "**OI-2(A-6 锚点失效,R1):** `@user/data-base` 未导出 `loading`;审计 A-6 替代锚点疑似失效。处置选项:(a) 执行日核实到真实承载(新加载器)后按真实的写;(b) 若无可替代,记录失效并**先补设计**。**待用户裁定。**"
- "**OI-3(A-1-1 实例路径,R2):** `hotkey.ts` 为模块级(非组件),取得 `SaveSystem` 实例的合规路径未定(D-11 禁 `client` 单例)。**待用户裁定接入口径**;若无法合规取得则本条目暂缓。"
- "**OI-4(命名影响):** 预期不新增公共命名;若执行日不可避免需新增,须呈报批准。**待用户确认。**"
---
<objective>
Phase 9 · 任务 7 · 第一批改造(ALT 条目)· **第 2 组 / 共 4 组** —— **跨包 legacy 依赖:`GameStorage` / `Mota.require` / 全局 `core` 收口**(5 条 ALT)。
**要解决的问题(D-50..D-53):** 本组处理 `client-base` / `client-modules` 中跨包 legacy 依赖:① `@motajs/legacy-system` 的 `GameStorage`(`hotkey.ts` 热键配置持久化);② 三处 `Mota.require('@user/data-base')`(`cache.ts` / `misc.ts`);③ 全局 `core` 的 ALT 用法收口(umbrella)。
**大致解决方案(本计划 = 09-14):**
- **Task 0(`blocking-human`)= 预执行汇报关卡**:汇报本组范围、逐条 ALT 表、逐文件影响表、**R1(A-6 锚点失效风险)** 与 **R2(`SaveSystem` 实例路径风险)**、命名影响、OI-1..OI-4,等待「可以执行」。
- **Task 1 = `Mota.require` → 静态导入(A-6-1/2/3)**:改 `cache.ts` / `misc.ts` 为顶部静态导入;执行日核实 `loading` 承载(R1);无替代则 STOP。
- **Task 2 = `GameStorage` → `SaveSystem`(A-1-1)+ A-2-1 umbrella 记录**:`hotkey.ts` 热键配置持久化改用新存档 / 全局存储;执行日核实实例路径(R2);无法合规取得则 STOP;A-2-1 作为 umbrella 在 SUMMARY 记录部分收口。
**API coverage:** `detected:false`。**`No external API integration: 本计划仅为源码改造,不涉及任何外部 API。`**
</objective>
<execution_context>
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
@.planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md
@.planning/phases/09-minimum-browser-runnable/09-LEGACY-INTERFACES-AUDIT.md
@AGENTS.md
@dev.md
</context>
## ALT 条目清单与影响表(本组 5 条)
| ID | 原接口/成员(锚点) | 替代方案(锚点) | 涉及文件:行 | 编辑要点 |
| --- | --- | --- | --- | --- |
| A-6-1 | `Mota.require('@user/data-base')`(cache.ts:491) | 直接 ESM 导入(data-base/index.ts:1) | client-base/src/elements/cache.ts:491 | 改顶部静态导入;核实 `loading` 承载(R1) |
| A-6-2 | `Mota.require('@user/data-base')`(misc.ts:89) | 直接 ESM 导入 | client-base/src/elements/misc.ts:89 | 改顶部静态导入;核实 `loading` 承载(R1) |
| A-6-3 | `Mota.require('@user/data-base')`(misc.ts:219) | 直接 ESM 导入 | client-base/src/elements/misc.ts:219 | 改顶部静态导入;核实 `loading` 承载(R1) |
| A-1-1 | `GameStorage`(hotkey.ts:3) | `SaveSystem`(save/system.ts:41) | client-modules/src/action/hotkey.ts:3,518-522 | 热键配置持久化改新存档 / 全局存储;核实实例路径(R2) |
| A-2-1 | 全局 `core`(core.d.ts:1302) | `CoreState`(data-state/core.ts:76) | client-modules/src/core.ts:6(umbrella) | 只收口本组 ALT 用法;不删 `core` 全局声明 |
## 逐文件影响表(本组)
| 文件 | 动作 | 改动内容 | 关联 ALT |
| --- | --- | --- | --- |
| `packages-user/client-base/src/elements/cache.ts` | 修改 | `createCache()` 内 `Mota.require('@user/data-base')` → 顶部静态导入 | A-6-1 |
| `packages-user/client-base/src/elements/misc.ts` | 修改 | 两处 `Mota.require('@user/data-base')` → 顶部静态导入 | A-6-2 / A-6-3 |
| `packages-user/client-modules/src/action/hotkey.ts` | 修改 | `GameStorage` → 新存档 / 全局存储(`SaveSystem`) | A-1-1 |
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 7 · 第一批改造(ALT)· 第 2 组:跨包 legacy 依赖」的范围、逐条 ALT 表、逐文件影响表、R1/R2 风险与 OI,等待用户回复「可以执行」</name>
<precondition>`packages-user/client-base/src/elements/{cache,misc}.ts` 与 `packages-user/client-modules/src/action/hotkey.ts` 存在;`packages-user/data-base/src/index.ts` 存在;审计文档 `09-LEGACY-REPLACEMENT-AUDIT.md` 已存在。</precondition>
<files>(只读汇报,不修改任何文件)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-03 / D-04 / **D-50 / D-52 / D-53**)
- .planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md(A-1-1 / A-6-* / A-2-1)
- .planning/phases/09-minimum-browser-runnable/09-14-PLAN.md(本计划全文,含「ALT 条目清单与影响表」「逐文件影响表」与 R1/R2)
- packages-user/data-base/src/index.ts(核实 `loading` 是否导出,R1)
- packages-user/client-base/src/save/system.ts(`SaveSystem` 全局存储 `getGlobal` / `setGlobal`,R2)
- AGENTS.md(执行前须获批、命名须先反馈、问题须先确认);dev.md(命名、注释、依赖单向、CRLF)
</read_first>
<action>
**停下并向用户汇报,等待明确回复「可以执行」后才继续;汇报本身只读。**
① **要解决的问题:** 任务 7 · 第一批改造(ALT)第 2 组:`client-base` / `client-modules` 中跨包 legacy 依赖(`GameStorage` / `Mota.require` / 全局 `core` 收口),共 5 条。
② **逐条 ALT 条目表:** 原样呈现本计划「ALT 条目清单与影响表」的 5 行。
③ **逐文件影响表:** 原样呈现本计划「逐文件影响表」的 3 行,并说明 `files_modified` 精确为 3 个源文件。
④ **R1(A-6 锚点疑似失效,须用户知悉):** 规划日实测 `packages-user/data-base` 的 barrel **未导出 `loading`**;`Mota.require('@user/data-base').loading` 所依赖的 `loading`(`once('loaded'|'coreInit', …)` / `loaded`)在新 data-base 中无当前承载。执行日先核实真实承载(如新加载器 `packages-user/data-state/src/loader/loader.ts` 的 `MotaDataLoader`);**若无等价替代,记录锚点失效并 STOP 请示**,不猜测。**OI-2。**
⑤ **R2(A-1-1 实例路径,须用户知悉):** `hotkey.ts` 为模块级,替代 `SaveSystem` 需要实例;`GameStorage`(localStorage 键值)与 `SaveSystem`(Dexie 存档 / 全局存储)语义不同。执行日核实合规取得路径(D-11 禁 `client` 单例);无法合规取得则记录并 STOP。**OI-3。**
⑥ **A-2-1(umbrella):** 全局 `core` → `CoreState`;本计划只收口本组 ALT 用法,**不删 `core` 全局声明**;全量收口待其它分类批次。
⑦ **编辑策略:** Task 1 改 `cache.ts` / `misc.ts` 的 `Mota.require` 为顶部静态导入(A-6-1/2/3);Task 2 改 `hotkey.ts` 的 `GameStorage`(A-1-1)+ 记录 A-2-1 umbrella。
⑧ **范围与禁止:** 只改 ALT 条目相关用法;不触碰其它分类;不改 09-01..09-13;不改既有注释;不用 `import type`;不引入 `client` 单例;源码 CRLF。
⑨ **验证方式:** 静态断言——`cache.ts` / `misc.ts` 无 `Mota.require('@user/data-base')` 且出现静态 `import`;`hotkey.ts` 无 `GameStorage` 且出现替代符号;CRLF;范围 / `git status` 守卫。**不设全仓类型 / 构建门禁。** 若 R1/R2 触发 STOP,则相应条目标记为「待裁定」,门禁对未改文件不作强制。
⑩ **开放项:** OI-1(批次拆分)、OI-2(A-6 锚点失效)、OI-3(A-1-1 实例路径)、OI-4(命名影响)。**无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户明确回复「可以执行」四个字**;回复其它内容视为未通过。
</action>
<decision>是否按本计划(09-14)执行任务 7 · 第一批改造 · 第 2 组:跨包 legacy 依赖替换(`Mota.require` → 静态导入、`GameStorage` → `SaveSystem`、A-2-1 部分收口),并在 R1/R2 无法满足时按 STOP 处置</decision>
<context>
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = ALT 范围确认 + R1(A-6 锚点失效)+ R2(A-1-1 实例路径)+ 命名影响确认。行号与 `loading` 导出现状为规划日实测,以执行日为准。
</context>
<options>
<option id="execute-default">
<name>可以执行(采纳全部默认处置;R1/R2 触发时按 STOP 处置)</name>
<pros>如实落实可替代部分;对疑似失效锚点不猜测;保留全部 ALT 追踪</pros>
<cons>若 R1/R2 成立,A-6 / A-1-1 可能暂缓,本组部分条目延后</cons>
</option>
<option id="execute-defer">
<name>可以执行 + 先将 A-6 / A-1-1 标记为「待裁定」,仅执行可确定部分</name>
<pros>避免中途 STOP</pros>
<cons>本组实际改动减少</cons>
</option>
<option id="revise">
<name>修订分组范围 / R1/R2 处置后重新汇报</name>
<pros>避免带着未定锚点改造</pros>
<cons>须修订 PLAN.md 后重新汇报</cons>
</option>
</options>
<resume-signal>回复「可以执行」四个字(如需指定 R1/R2 的处置或调整 OI,请在回复中指明)</resume-signal>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['packages-user/client-base/src/elements/cache.ts','packages-user/client-base/src/elements/misc.ts','packages-user/client-modules/src/action/hotkey.ts','packages-user/data-base/src/index.ts']){if(!fs.existsSync(p))bad.push('missing: '+p)}const audit='.planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md';if(!fs.existsSync(audit))bad.push('missing audit');if(bad.length){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}const db=fs.readFileSync('packages-user/data-base/src/index.ts','utf8');console.log('OK prestate intact; data-base barrel exports loading? '+(/loading/.test(db)?'YES':'NO (R1 confirmed: audit anchor likely stale)'))"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(输入文件或审计文档缺失)</fails_when>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 R1/R2 的处置,须在汇报与后续任务中据其指定执行)</human-check>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」四个字
- 汇报内容涵盖:D-50 / D-52 / D-53、本组 5 条 ALT 条目表、逐文件影响表、R1(A-6 锚点失效)、R2(A-1-1 实例路径)、编辑策略、命名影响、OI-1..OI-4
- 汇报中已原样告知:不猜 `loading` 承载、不引入 `client` 单例、只改 ALT、CRLF 与范围零外溢
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
</acceptance_criteria>
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
</task>
<task type="auto">
<name>Task 1: 将 `Mota.require('@user/data-base')` 改为静态导入(A-6-1 / A-6-2 / A-6-3),并核实 `loading` 承载(R1)</name>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 R1 / OI 另有指定,按指定执行)。</precondition>
<files>packages-user/client-base/src/elements/cache.ts, packages-user/client-base/src/elements/misc.ts</files>
<read_first>
- packages-user/client-base/src/elements/cache.ts:485-505(`createCache()` 中 `Mota.require('@user/data-base')` 的用法)
- packages-user/client-base/src/elements/misc.ts:80-100 与 214-232(两处 `Mota.require('@user/data-base')`)
- packages-user/data-base/src/index.ts(核实 `loading` 是否导出,R1)
- packages-user/data-state/src/loader/loader.ts(新加载器 `MotaDataLoader` 的加载信号,潜在的 `loading` 承载,只读参考)
- packages/legacy-ui/src/preset/ui.ts:222(legacy 中 `loading.once('coreInit', …)` 的同族用法,只读参考)
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-11 主对象经 props 获取、禁 `client` 单例)
</read_first>
<action>
(1) **核实 `loading` 承载(先于编辑,R1):** 读 `@user/data-base` barrel 与全包检索 `loading` 的导出 / 等价物。规划日实测:`@user/data-base` **未导出 `loading`**;执行日须进一步确认 `loading`(`once('loaded'|'coreInit', …)` / `loaded`)在新架构的真实承载。
- 若定位到等价替代(例如新加载器 `MotaDataLoader` 的 `loadedPromise` / 加载 hook,或某个静态可导入的信号对象),**按真实符号**改写三处用法,并在 SUMMARY 记录「审计锚点 `@user/data-base` 不精确,实为 X」。
- 若确认**无等价替代**:**记录审计锚点失效 + STOP 请示**(回到 Task 0 / 用户裁决),**不得**造出未存在的符号,也**不得**保留 `Mota.require` 视作完成。
(2) **静态导入改造(A-6-1/2/3,在 R1 满足前提下):** 将 `cache.ts`(约 491 行)与 `misc.ts`(约 89、219 行)中的 `Mota.require('@user/data-base')` 改为文件顶部标准 ESM 导入(`import { … } from '@user/data-base'`;导入位置遵循 dev.md——首行起、无注释夹杂、无空行)。保持原行为(`loading` 的 `once('loaded', …)` / `loaded` 语义按真实承载等价实现)。
(3) **不 invent / 不越界:** 只改 ALT 条目相关用法;`cache.ts` 中 `core.material`(ADAPT)、`core.getAnimateFrames`(TBD)、`core.getTilesetOffset`(ALT,属 09-16)等**本任务不改**。
(4) **行尾 / 注释:** 保持 CRLF;不改既有注释 / JSDoc;不使用 `import type`;不新增 `?.`(dev.md 允许场景除外)。
</action>
<reversibility rating="reversible">三处调用点替换;`git revert` 可回退。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];const code=p=>fs.readFileSync(p,'utf8').split(/\r?\n/).filter(l=>!/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');for(const p of ['packages-user/client-base/src/elements/cache.ts','packages-user/client-base/src/elements/misc.ts']){const t=code(p);if(/Mota\.require\(\s*'@user\/data-base'/.test(t))bad.push(p+' still has Mota.require(@user/data-base)');if(!/@user\/data-base'/.test(t))bad.push(p+' no static @user/data-base import');}if(bad.length){console.error('MOTA-REQUIRE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK Mota.require(@user/data-base) replaced by static import')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "MOTA-REQUIRE GATE FAILED"(仍存在 `Mota.require('@user/data-base')` 或缺少静态导入)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['packages-user/client-base/src/elements/cache.ts','packages-user/client-base/src/elements/misc.ts']){const b=fs.readFileSync(p,'utf8');if(b.indexOf('\r\n')<0)bad.push('not CRLF: '+p);if(/\bimport\s+type\b/.test(b))bad.push('import type: '+p)}if(bad.length){console.error('CRLF/IMPORT FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK files CRLF and no import type')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF/IMPORT FAILED"(非 CRLF 或出现 `import type`)</fails_when>
<automated>git status --porcelain</automated>
<fails_when>输出包含本计划 `files_modified` 之外、且非用户既有并发改动(`packages-user/data-state/**`)的生产文件改动</fails_when>
</verify>
<acceptance_criteria>
- `loading` 承载已核实:要么按真实符号改写并记录,要么记录锚点失效并 STOP(二选一,均须写入 SUMMARY)
- 若执行改写:`cache.ts` / `misc.ts` 无 `Mota.require('@user/data-base')`、有静态导入;行为语义保持
- 未触碰非 ALT 用法(`core.material` / `core.getAnimateFrames` / `core.getTilesetOffset` 等)
- 被改文件 CRLF、无 `import type`;未改 `files_modified` 外文件(用户并发改动除外)
</acceptance_criteria>
<done>`Mota.require` 三处按真实承载改写;或记录 A-6 锚点失效并 STOP;门禁(若改写)全绿</done>
</task>
<task type="auto">
<name>Task 2: 替换 `GameStorage` 为 `SaveSystem` 全局存储(A-1-1)并记录 A-2-1 umbrella(部分收口)</name>
<precondition>Task 1 已完成(或已按 R1 STOP 记录);Task 0 已获批。</precondition>
<files>packages-user/client-modules/src/action/hotkey.ts</files>
<read_first>
- packages-user/client-modules/src/action/hotkey.ts:1-6,510-526(`GameStorage` 导入与热键配置持久化)
- packages-user/client-base/src/save/system.ts:28-45,197-222(`SaveSystem` / `GlobalTransaction` / `getGlobal` / `setGlobal`)
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-11 主对象经 props 获取、禁 `client` 单例)
- .planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md(A-1-1 / A-2-1)
</read_first>
<action>
(1) **核实实例路径(先于编辑,R2):** `hotkey.ts` 为模块级(非组件)。确认取得 `SaveSystem`(或等价的全局存储)实例的**合规路径**:不得引入 `client` 单例;若只能经未批准的机制取得,则本条目**记录并 STOP 请示**(OI-3)。
- 若存在合规路径:将热键配置持久化从 `GameStorage`(`keyStorage.data` / `read()` / `setValue()` / `toJSON()`)改为经 `SaveSystem` 的全局存储(`getGlobal` / `setGlobal` 或审计等价方案),保持「读入 → 反序列化到 `gameKey` → 变更时写回」的行为等价。
- 若无法合规取得:**记录 R2 阻断 + STOP**,不得引入 `client` 单例,也不得保留 `GameStorage` 视作完成。
(2) **A-2-1(umbrella,部分收口):** 不删 `src/types/declaration/core.d.ts` 的全局 `core` 声明,也不改 `client-modules/src/core.ts` 的单例定义(其属 ADAPT 分类 / 其它批次)。在 `09-14-SUMMARY.md` 记录 A-2-1 的**部分收口**状态(本组 ALT 用法已改;全量收口待其余分类批次),并**逐条列明 REPLACED(已收口)与 DEFERRED(未收口 / 待其它批次)的对象**:至少含 `GameStorage`(已 REPLACED,`hotkey.ts`)、`Mota.require('@user/data-base')` 三处(状态按 R1 结论 REPLACED 或 STOP)、以及 DEFERRED 的 `client-modules/src/core.ts:6` 单例定义 / 全局 `core` 声明与其它分类(ADAPT / NONE / TBD)成员,使 40 条 ALT 的整体覆盖可审计。
(3) **不 invent / 不越界:** 只改 `hotkey.ts` 中与 A-1-1 相关的用法;`hotkey.ts` 内 `core.*` 其它用法(`core.status` / `core.isMoving` / `core.isReplaying` / `core.doSL` / `core.turnHero` / `core.getNextItem` / `core.confirmRestart` / `core.actions` / `core.openBook` 等)属其它分类或其它组,**本任务不改**(`core.debug` 属 09-16)。
(4) **命名 / 行尾 / 注释:** 预期不新增命名;如必须新增须呈报批准(D-04)。保持 CRLF;不改既有注释 / JSDoc;不使用 `import type`;不新增 `?.`(dev.md 允许场景除外)。
</action>
<reversibility rating="reversible">单文件替换;`git revert` 可回退。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];const p='packages-user/client-modules/src/action/hotkey.ts';const t=fs.readFileSync(p,'utf8').split(/\r?\n/).filter(l=>!/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');if(/GameStorage/.test(t))bad.push('hotkey.ts still uses GameStorage');if(!/\bSaveSystem\b/.test(t))bad.push('hotkey.ts: no SaveSystem replacement (expected audit symbol)');if(/\bfrom '@motajs\/legacy-system'/.test(t))bad.push('hotkey.ts still imports @motajs/legacy-system');if(bad.length){console.error('GAMESTORAGE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK GameStorage replaced (or recorded STOP) in hotkey.ts')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "GAMESTORAGE GATE FAILED"(仍用 `GameStorage`、无替代符号,或仍导入 `@motajs/legacy-system`)</fails_when>
<automated>node -e "const fs=require('fs');const b=fs.readFileSync('packages-user/client-modules/src/action/hotkey.ts','utf8');const bad=[];if(b.indexOf('\r\n')<0)bad.push('not CRLF');if(/\bimport\s+type\b/.test(b))bad.push('import type');if(/\bclient\b\s*\./.test(b)&&/from '\.\.\/core'/.test(b)&&!/^\s*\/\//m.test(b)){}if(bad.length){console.error('CRLF/IMPORT FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK hotkey.ts CRLF and no import type')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF/IMPORT FAILED"(非 CRLF 或出现 `import type`)</fails_when>
<automated>git status --porcelain</automated>
<fails_when>输出包含本计划 `files_modified` 之外、且非用户既有并发改动(`packages-user/data-state/**`)的生产文件改动</fails_when>
</verify>
<acceptance_criteria>
- `hotkey.ts` 已去除 `GameStorage`(及 `@motajs/legacy-system` 导入)并改用合规的 `SaveSystem` / 全局存储;或记录 R2 阻断并 STOP
- 未引入 `client` 单例;未新增未批准命名;未触碰其它分类用法
- A-2-1 部分收口状态已写入 SUMMARY(不删 `core` 全局声明),且 SUMMARY **逐条列明 REPLACED vs DEFERRED 对象**(含 `client-modules/src/core.ts:6` 单例定义未编辑/未改的处置),使 40 条 ALT 覆盖可审计
- 被改文件 CRLF、无 `import type`;未改 `files_modified` 外文件(用户并发改动除外)
</acceptance_criteria>
<done>`GameStorage` 替换到位(或记录阻断 STOP);A-2-1 部分收口记录完成;门禁全绿</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 范围边界 | 只改 ALT 条目相关用法;不触碰 DIRECT / ADAPT / NONE / TBD(D-53) |
| 替代依据边界 | 替代方案只取自审计;锚点失效时记录并 STOP,**不猜**(D-52 / R1) |
| 单例边界 | 不得引入 `client` 单例取得实例(D-11 / R2) |
| 命名边界 | 未经批准不得新增命名(D-04) |
| 文件 / 提交边界 | 仅改 `files_modified`;不把用户并发改动(`data-state/**`)纳入提交 |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-14-01 | Tampering | 臆造 `loading` 替代 / 编造不存在的符号 | high | mitigate | R1 前置核实;prohibitions 禁猜;无替代则 STOP + 记录 |
| T-09-14-02 | Elevation of Privilege | 为取得实例而引入 `client` 单例 / 使用未批准命名 | high | mitigate | R2 前置核实;prohibitions;D-04 / D-11 |
| T-09-14-03 | Tampering | 越界改动非 ALT 用法(`core.status` 等) | high | mitigate | 范围门禁 + `<action>` 明列不改项 + `git status` 守卫 |
| T-09-14-04 | Repudiation | 保留 `Mota.require` / `GameStorage` 却视作完成 | medium | mitigate | MOTA-REQUIRE / GAMESTORAGE 门禁强制 legacy 用法消失 |
| T-09-14-05 | Information Disclosure | 把用户并发改动(`data-state/**`)误纳入提交 | medium | mitigate | `git status` 守卫豁免既有并发改动 |
| T-09-14-06 | Repudiation | 未跑静态门禁即提交 | medium | mitigate | Task 1/2 各有自动化门禁 + SUMMARY |
| T-09-14-SC | Tampering | npm/pip/cargo installs | low | accept | 无第三方包安装 |
</threat_model>
<verification>
- **Task 1**:`cache.ts` / `misc.ts` 无 `Mota.require('@user/data-base')` 且有静态导入;R1 结论写入 SUMMARY;CRLF / import type 检查;范围门禁。
- **Task 2**:`hotkey.ts` 无 `GameStorage` / `@motajs/legacy-system`,有 `SaveSystem`;A-2-1 部分收口记录(含 REPLACED vs DEFERRED 逐条、`core.ts:6` 单例定义处置);CRLF / import type 检查;范围门禁。
- **R1/R2 触发时**:记录锚点失效 / 阻断并 STOP,相应条目标记「待裁定」。
- **无全仓门禁**:不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
- **人工复核**:结论写入 `09-14-SUMMARY.md`。
</verification>
<success_criteria>
- 本组 5 条 ALT:A-6-1/2/3(`Mota.require` → 静态导入)、A-1-1(`GameStorage` → `SaveSystem`)按审计落地;A-2-1 umbrella 部分收口并记录(SUMMARY 含 REPLACED vs DEFERRED 逐条,覆盖可审计)。
- R1 / R2 若成立,**如实记录并 STOP**,不猜测、不保留 legacy 用法冒充完成。
- 零越界:不触碰其它分类、不引入 `client` 单例、不新增未批准命名。
- 静态门禁(MOTA-REQUIRE / GAMESTORAGE / CRLF / 范围)全绿(或按 STOP 分支记录)。
</success_criteria>
## Artifacts this phase produces
| 符号 / 路径 | 类型 | 说明 |
|---|---|---|
| `packages-user/client-base/src/elements/cache.ts`(修改) | 源码 | `Mota.require` → 静态导入 |
| `packages-user/client-base/src/elements/misc.ts`(修改) | 源码 | 两处 `Mota.require` → 静态导入 |
| `packages-user/client-modules/src/action/hotkey.ts`(修改) | 源码 | `GameStorage` → `SaveSystem` 全局存储 |
| `.planning/phases/09-minimum-browser-runnable/09-14-SUMMARY.md`(新增) | 执行摘要 | 本组 ALT、逐文件改动、R1/R2 结论、**A-2-1 REPLACED vs DEFERRED 逐条**、命名影响、门禁结果 |
**命名说明:** 预期**不新增源码命名**;如需新增须先获用户批准。
## Source Coverage Audit
| 来源 | 项 | 处置 |
|------|----|------|
| GOAL | Phase 9(ROADMAP) | COVERED(去 legacy 跨包依赖子切片) |
| REQ | 「Requirements: TBD」 | N/A(`requirements: []`) |
| RESEARCH | 无 RESEARCH.md | N/A |
| CONTEXT | D-50(第一批 = 40 条 ALT) | COVERED(本组 5/40) |
| CONTEXT | D-51(准确记录源文件清单) | COVERED(`files_modified` + 逐文件影响表) |
| CONTEXT | D-52(依据审计,不 invent) | COVERED(prohibitions + R1 STOP) |
| CONTEXT | D-53(其它分类不在本批) | COVERED(范围门禁) |
| CONTEXT | D-03 / D-04(执行前获批 / 命名获批) | COVERED(Task 0 + prohibitions) |
| 缺口 | R1(A-6 锚点疑似失效) / R2(A-1-1 实例路径) | OPEN(Task 0 OI-2 / OI-3,待用户裁定) |
<output>
Create `.planning/phases/09-minimum-browser-runnable/09-14-SUMMARY.md` when done
</output>

View File

@ -0,0 +1,364 @@
---
phase: 09-minimum-browser-runnable
plan: 15
type: execute
wave: 12
depends_on: [09-14]
files_modified:
- packages-user/client-modules/src/render/utils/saves.ts
- packages-user/client-modules/src/ui/save.tsx
- packages-user/client-modules/src/ui/settings.tsx
- packages-user/client-modules/src/action/hotkey.ts
- packages-user/client-modules/src/ui/toolbar.tsx
- packages-user/client-modules/src/ui/viewmap.tsx
- .planning/phases/09-minimum-browser-runnable/09-15-SUMMARY.md
autonomous: false
requirements: []
estimate:
tokens: 48000
raw_tokens: 53000
tasks: 3
confidence: low
must_haves:
truths:
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03)**:Task 0 必须先汇报「本计划 = 任务 7 · 第一批改造(ALT)· 4 个计划中的第 3 个(存档体系替换 + UI 打开 / 控制替换)」的要做什么 / 为什么 / 大致怎么做 / 影响范围,**必须列明:逐条 ALT 条目表(14 条);逐文件影响表(6 文件);两个须核实的点(R1:`core.saves.*` 结构与 `SaveSystem` API 不等价,逐字段映射口径;R2:`core.openBook` 等打开 UI 的传参与 `UIController.open` / props 接入);命名影响;开放项 OI-1..OI-4**,然后必须**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行本计划任何一步、不得派发执行子代理,且不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行。"
- "**D-50 / D-52(范围 / 依据):** 实现本批 ALT 之**第 3 组**(14 条:C:saves / C:getSave / C:getAllSaves / C:setLocalForage / C:removeLocalForage / C:removeSave / C:clearLocalForage / C:openBook / C:openToolbox / C:openEquipbox / C:openQuickShop / C:ui / A-1-6 / A-5-2),严格依据审计替代方案,**不 invent**。"
- "**D-53(其它分类不在本批):** 不触碰 `可直接替换` / `有替代但需适配` / `无替代` / `待定`;本组只改 ALT 条目。"
- "**R1(`SaveSystem` 映射口径):** 审计将 `core.saves` / `core.getSave` / `core.getAllSaves` / `core.setLocalForage` / `core.removeLocalForage` / `core.removeSave` / `core.clearLocalForage` 的替代定位到 `SaveSystem`(`save/system.ts:41` 等)。`core.saves`(`ids` / `saveIndex` / `autosave` / `favorite` …)与 `SaveSystem`(`load` / `deleteSave` / `getGlobal` / `setGlobal` / `startGlobalTransaction` …)**结构不等价**;执行日须逐字段映射(或经主对象取得存档系统实例),无法等价时**记录并 STOP**,不得猜测。**特别地,`C:getAllSaves` 的审计替代锚点(`getAllSaves`,`saves.ts:36`)即为该调用点所在的包装函数本身**(其内部 `saves.ts:38` 仍调用 `core.getAllSaves`)——属**自指锚点**;`SaveSystem`(`save/system.ts:41`)**无存档枚举方法**;执行日须核实是否存在真正的枚举来源(`SaveSystem` 索引 / 全局存储或新存档存储的列表 API),若确认无等价枚举路径则**记录锚点问题并 STOP**(与 09-16 `C:drawThumbnail` 同处置)。"
- "**R2(UI 打开 / 控制的传参与接入):** 审计将 `core.openBook` / `core.openToolbox` / `core.openEquipbox` / `core.openQuickShop` / `core.ui` / `mainUi` 的替代定位到 `BookUI` / `ToolBoxUI` / `EquipBoxUI` / `ShopUI`(`ui/*.tsx`)、`mainUIController`(`ui/controller.tsx:5`)与 `UIController.open`(`packages/system/src/ui/controller.ts:190`)。执行日须核实打开这些 UI 所需 props 的取得路径(D-11:经 props / 主对象,不得引入 `client` 单例)与 `core.ui.closePanel()` / `core.ui._drawViewMaps()` 的等价方法(审计述 `closePanel` 以 `closeAll` 等价;`_drawViewMaps` 等价物须核实);无法等价时**记录并 STOP**。"
- "**范围零外溢 + 静态门禁:** 只改 `files_modified`;不改既有注释 / JSDoc;不用 `import type`;源码 CRLF;验证一律静态断言(legacy 用法消失 + 替代符号出现 + CRLF + 范围 / `git status` 守卫),**不使用** `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。"
artifacts:
- path: packages-user/client-modules/src/render/utils/saves.ts
provides: "存档工具改为经 `SaveSystem`(`load` / `deleteSave` / `getGlobal` / `setGlobal` 等)。"
- path: packages-user/client-modules/src/ui/save.tsx
provides: "`core.removeSave` → `SaveSystem.deleteSave`。"
- path: packages-user/client-modules/src/ui/settings.tsx
provides: "`core.clearLocalForage` → `SaveSystem` 事务;`mainUi` / `mainUi.open` → `mainUIController` / `UIController.open`;`core.ui` → `mainUIController`。"
- path: packages-user/client-modules/src/action/hotkey.ts
provides: "`core.openBook` / `core.openToolbox` / `core.openEquipbox` / `core.openQuickShop` → 对应 UI + `UIController.open`。"
- path: packages-user/client-modules/src/ui/toolbar.tsx
provides: "`core.open*` / `core.ui` → 对应 UI + `mainUIController`。"
- path: packages-user/client-modules/src/ui/viewmap.tsx
provides: "`core.openBook` → `BookUI` + `UIController.open`。"
- path: .planning/phases/09-minimum-browser-runnable/09-15-SUMMARY.md
provides: "本计划执行摘要:本组 ALT、逐文件改动、R1/R2 结论、命名影响、门禁结果。"
key_links:
- "`render/utils/saves.ts:19-51,172-187`(`core.getSave` / `core.getAllSaves` / `core.saves` / `core.setLocalForage` / `core.removeLocalForage`)↔ `packages-user/client-base/src/save/system.ts:41,185,192,209,214`(`SaveSystem`)—— C:saves / C:getSave / C:getAllSaves / C:setLocalForage / C:removeLocalForage"
- "`render/utils/saves.ts:36,38`(`getAllSaves` 包装 ↔ 其内 `core.getAllSaves`)—— C:getAllSaves **自指锚点**(审计替代即调用点本身;`SaveSystem` 无枚举方法,STOP 点)"
- "`ui/save.tsx:538`(`core.removeSave`)↔ `save/system.ts:192`(`SaveSystem.deleteSave`)—— C:removeSave"
- "`ui/settings.tsx:573`(`core.clearLocalForage`)↔ `save/system.ts:214`(`startGlobalTransaction`)—— C:clearLocalForage"
- "`action/hotkey.ts:481,484,487,493` 与 `ui/toolbar.tsx:89,90,98,99,192` 与 `ui/viewmap.tsx:130`(`core.open*`)↔ `ui/book.tsx:251` / `ui/toolbox.tsx:278` / `ui/equipbox.tsx:551` / `ui/shop.tsx:491`(`*UI`)—— C:openBook / C:openToolbox / C:openEquipbox / C:openQuickShop"
- "`ui/settings.tsx:160` 与 `ui/toolbar.tsx:198`(`core.ui`)↔ `ui/controller.tsx:5`(`mainUIController`)—— C:ui"
- "`ui/settings.tsx:13,67`(`mainUi` / `mainUi.open('settings')`)↔ `ui/controller.tsx:5`(`mainUIController`)/ `packages/system/src/ui/controller.ts:190`(`UIController.open`)—— A-1-6 / A-5-2"
assumptions:
- "**执行日行号可能漂移**:引用行号为 2026-10-11 规划日实测;门禁以执行日实测为准。"
- "**`mainUIController` 已为既有合法入口**:`ui/toolbar.tsx` 已导入并使用 `mainUIController`(`ui/controller.tsx:5`);本组沿用该模式,不引入 `client` 单例。"
- "**用户并发改动(D-56)**:执行期存在用户 `packages-user/data-state/**` 并发改动;`git status` 守卫豁免这些既有改动。"
- "**Markdown 使用 LF,源码使用 CRLF**。"
prohibitions:
- "不得在用户明确回复「可以执行」四个字之前执行本计划任何一步、或派发任何执行子代理。"
- "**不得 invent 存档 / UI 替代**:`SaveSystem` 无法等价映射的 `core.saves.*` 字段、或 `UIController` 无等价方法的 `core.ui.*`,须记录并 STOP,不得造符号或猜语义。"
- "**不得引入 `client` 单例**;不得为接入而新增未批准命名(D-04)。"
- "不得触碰非 ALT 分类用法(如 `core.doSL` / `core.startGame` / `core.chooseReplayFile` / `core.download` / `core.firstData` / `core.status` 等)—— 它们属其它分类或其它组。"
- "不得改 09-01..09-14 任何文件;不得改既有注释 / JSDoc;不得用 `import type`;不得新增 `?.`(dev.md 允许场景除外)。"
- "不得把门禁建立在「全仓 TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上。"
open_items:
- "**OI-1(批次拆分):** 本计划为任务 7 批次 4 计划中的第 3 个;**待用户确认**。"
- "**OI-2(`SaveSystem` 映射口径,R1):** `core.saves.*` 与 `SaveSystem` 结构不等价;逐字段映射(或经主对象取存档系统)口径待定。**另,`C:getAllSaves`(`saves.ts:36/38`)为自指锚点**——审计替代即调用点所在包装函数本身,`SaveSystem` 无枚举方法,真实枚举来源待核实或 STOP。**待用户裁定。**"
- "**OI-3(UI 打开传参 / `core.ui` 等价物,R2):** 打开 `*UI` 所需 props 路径与 `core.ui._drawViewMaps()` 的等价物待核实。**待用户裁定。**"
- "**OI-4(命名影响):** 预期不新增公共命名;如执行日不可避免须呈报批准。**待用户确认。**"
---
<objective>
Phase 9 · 任务 7 · 第一批改造(ALT 条目)· **第 3 组 / 共 4 组** —— **存档体系替换 + UI 打开 / 控制替换**(14 条 ALT)。
**要解决的问题(D-50..D-53):** 本组替换 `core` 的存档体系调用(→ `SaveSystem`)与 UI 打开 / 控制调用(→ `BookUI` / `ToolBoxUI` / `EquipBoxUI` / `ShopUI` / `mainUIController` / `UIController.open`)。
**大致解决方案(本计划 = 09-15):**
- **Task 0(`blocking-human`)= 预执行汇报关卡**:汇报本组范围、逐条 ALT 表、逐文件影响表、R1(`SaveSystem` 映射口径)、R2(UI 打开传参 / `core.ui` 等价物)、命名影响、OI-1..OI-4,等待「可以执行」。
- **Task 1 = 存档体系替换(7 条)**:`render/utils/saves.ts`(C:saves / getSave / getAllSaves / setLocalForage / removeLocalForage)、`ui/save.tsx`(C:removeSave)、`ui/settings.tsx`(C:clearLocalForage)改经 `SaveSystem`;R1 逐字段映射,无法等价则 STOP。
- **Task 2 = UI 打开 / 控制替换(7 条)**:`action/hotkey.ts` 与 `ui/toolbar.tsx` 与 `ui/viewmap.tsx`(C:openBook / openToolbox / openEquipbox / openQuickShop)、`ui/settings.tsx` 与 `ui/toolbar.tsx`(C:ui)、`ui/settings.tsx`(A-1-6 / A-5-2);R2 核实 props / 等价方法,无法等价则 STOP。
**API coverage:** `detected:false`。**`No external API integration: 本计划仅为源码改造,不涉及任何外部 API。`**
</objective>
<execution_context>
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
@.planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md
@.planning/phases/09-minimum-browser-runnable/09-LEGACY-INTERFACES-AUDIT.md
@AGENTS.md
@dev.md
</context>
## ALT 条目清单与影响表(本组 14 条)
| ID | 原接口/成员(锚点) | 替代方案(锚点) | 涉及文件:行 | 编辑要点 |
| --- | --- | --- | --- | --- |
| C:saves | `core.saves`(saves.ts:173,185) | `SaveSystem`(save/system.ts:41) | render/utils/saves.ts:173,185 | `core.saves.ids` / `saveIndex` → SaveSystem 等价(R1) |
| C:getSave | `core.getSave`(saves.ts:21) | `SaveSystem.load`(save/system.ts:185) | render/utils/saves.ts:19-34 | 改经 `SaveSystem.load`(异步契约) |
| C:getAllSaves | `core.getAllSaves`(saves.ts:38) | `getAllSaves`(saves.ts:36)→ `SaveSystem` | render/utils/saves.ts:36-51 | **自指锚点**(审计替代即调用点所在包装函数本身);核实真实枚举来源,无等价则 STOP(R1) |
| C:setLocalForage | `core.setLocalForage`(saves.ts:172,187) | `SaveSystem.setGlobal`(save/system.ts:209) | render/utils/saves.ts:172,187 | 改经 `SaveSystem.setGlobal` |
| C:removeLocalForage | `core.removeLocalForage`(saves.ts:174) | `SaveSystem.deleteSave`(save/system.ts:192) | render/utils/saves.ts:174 | 改经 `SaveSystem.deleteSave` |
| C:removeSave | `core.removeSave`(save.tsx:538) | `SaveSystem.deleteSave`(save/system.ts:192) | ui/save.tsx:538 | 改经 `SaveSystem.deleteSave` |
| C:clearLocalForage | `core.clearLocalForage`(settings.tsx:573) | `SaveSystem.startGlobalTransaction`(save/system.ts:214) | ui/settings.tsx:573 | 改经事务清空全局存储 |
| C:openBook | `core.openBook`(hotkey.ts:481) | `BookUI`(book.tsx:251) | hotkey.ts:481;toolbar.tsx:89,192;viewmap.tsx:130 | 改经 `mainUIController.open(BookUI, …)`(R2) |
| C:openToolbox | `core.openToolbox`(hotkey.ts:484) | `ToolBoxUI`(toolbox.tsx:278) | hotkey.ts:484;toolbar.tsx:90 | 改经 `mainUIController.open(ToolBoxUI, …)`(R2) |
| C:openEquipbox | `core.openEquipbox`(hotkey.ts:487) | `EquipBoxUI`(equipbox.tsx:551) | hotkey.ts:487;toolbar.tsx:98 | 改经 `mainUIController.open(EquipBoxUI, …)`(R2) |
| C:openQuickShop | `core.openQuickShop`(hotkey.ts:493) | `ShopUI`(shop.tsx:491) | hotkey.ts:493;toolbar.tsx:99 | 改经 `mainUIController.open(ShopUI, …)`(R2) |
| C:ui | `core.ui`(settings.tsx:160) | `mainUIController`(ui/controller.tsx:5) | settings.tsx:160;toolbar.tsx:198 | `closePanel` → `closeAll`;`_drawViewMaps` 等价物核实(R2) |
| A-1-6 | `mainUi`(settings.tsx:13) | `mainUIController`(ui/controller.tsx:5) | ui/settings.tsx:13,67 | 移除 `@motajs/legacy-ui` 的 `mainUi` 导入,改用 `mainUIController` |
| A-5-2 | `mainUi.open('settings')`(settings.tsx:67) | `UIController.open`(system/ui/controller.ts:190) | ui/settings.tsx:67 | 改 `mainUIController.open(MainSettingsUI, …)` |
## 逐文件影响表(本组)
| 文件 | 动作 | 改动内容 | 关联 ALT |
| --- | --- | --- | --- |
| `packages-user/client-modules/src/render/utils/saves.ts` | 修改 | 存档工具改经 `SaveSystem`(load / deleteSave / setGlobal 等) | C:saves / C:getSave / C:getAllSaves / C:setLocalForage / C:removeLocalForage |
| `packages-user/client-modules/src/ui/save.tsx` | 修改 | `core.removeSave` → `SaveSystem.deleteSave` | C:removeSave |
| `packages-user/client-modules/src/ui/settings.tsx` | 修改 | `core.clearLocalForage` → `SaveSystem`;`mainUi` / `mainUi.open` → `mainUIController` / `UIController.open`;`core.ui` → `mainUIController` | C:clearLocalForage / C:ui / A-1-6 / A-5-2 |
| `packages-user/client-modules/src/action/hotkey.ts` | 修改 | `core.open*` → 对应 `*UI` + `UIController.open` | C:openBook / C:openToolbox / C:openEquipbox / C:openQuickShop |
| `packages-user/client-modules/src/ui/toolbar.tsx` | 修改 | `core.open*` / `core.ui` → 对应 `*UI` + `mainUIController` | C:openBook / C:openToolbox / C:openEquipbox / C:openQuickShop / C:ui |
| `packages-user/client-modules/src/ui/viewmap.tsx` | 修改 | `core.openBook` → `BookUI` + `UIController.open` | C:openBook |
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 7 · 第一批改造(ALT)· 第 3 组:存档 + UI 打开 / 控制」的范围、逐条 ALT 表、逐文件影响表、R1/R2 风险与 OI,等待用户回复「可以执行」</name>
<precondition>`packages-user/client-modules/src/render/utils/saves.ts`、`ui/{save,settings,toolbar,viewmap}.tsx`、`action/hotkey.ts` 存在;`packages-user/client-base/src/save/system.ts` 存在;审计文档已存在。</precondition>
<files>(只读汇报,不修改任何文件)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-03 / D-04 / **D-50 / D-52 / D-53**)
- .planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md(C 节 `C:saves` / `C:getSave` / `C:getAllSaves` / `C:setLocalForage` / `C:removeLocalForage` / `C:removeSave` / `C:clearLocalForage` / `C:openBook` / `C:openToolbox` / `C:openEquipbox` / `C:openQuickShop` / `C:ui` 与 A-1-6 / A-5-2)
- .planning/phases/09-minimum-browser-runnable/09-15-PLAN.md(本计划全文,含两表与 R1/R2)
- packages-user/client-base/src/save/system.ts:41,185,192,209,214(`SaveSystem` API,R1)
- packages-user/client-modules/src/ui/controller.tsx:5 与 packages/system/src/ui/controller.ts(`mainUIController` / `UIController.open`,R2)
- AGENTS.md;dev.md(命名、注释、依赖单向、CRLF)
</read_first>
<action>
**停下并向用户汇报,等待明确回复「可以执行」后才继续;汇报本身只读。**
① **要解决的问题:** 任务 7 · 第一批改造(ALT)第 3 组:存档体系替换(`core.*` → `SaveSystem`)+ UI 打开 / 控制替换(`core.open*` / `core.ui` / `mainUi` → `*UI` / `mainUIController` / `UIController.open`),共 14 条。
② **逐条 ALT 条目表:** 原样呈现本计划「ALT 条目清单与影响表」的 14 行。
③ **逐文件影响表:** 原样呈现本计划「逐文件影响表」的 6 行,并说明 `files_modified` 精确为 6 个源文件。
④ **R1(`SaveSystem` 映射口径,须用户知悉):** `core.saves.*`(`ids` / `saveIndex` / `autosave` / `favorite` …)与 `SaveSystem`(`load` / `deleteSave` / `getGlobal` / `setGlobal` / `startGlobalTransaction` …)结构不等价;执行日逐字段映射,无法等价则记录并 STOP。**其中 `C:getAllSaves`(`saves.ts:36/38`)为自指锚点**——审计替代即调用点所在包装函数本身(内部仍调用 `core.getAllSaves`),`SaveSystem` 无枚举方法,须核实真实枚举来源,否则记录锚点问题并 STOP。**OI-2。**
⑤ **R2(UI 打开传参 / `core.ui` 等价物,须用户知悉):** 打开 `BookUI` / `ToolBoxUI` / `EquipBoxUI` / `ShopUI` 所需 props 的取得路径,与 `core.ui.closePanel()` / `core.ui._drawViewMaps()` 的等价方法(审计述 `closePanel` 以 `closeAll` 等价);无法等价则记录并 STOP。**OI-3。**
⑥ **编辑策略:** Task 1 存档(7 条,3 文件);Task 2 UI 打开 / 控制(7 条,4 文件,其中 `hotkey.ts` 与 `settings.tsx`、`toolbar.tsx` 与 Task 1 有文件重叠,**同计划内按文件隔离一次性完成**,避免中间态)。
⑦ **范围与禁止:** 只改 ALT 条目相关用法;不触碰其它分类(`core.doSL` / `core.startGame` / `core.status` 等);不改 09-01..09-14;不改既有注释;不用 `import type`;不引入 `client` 单例;源码 CRLF。
⑧ **验证方式:** 静态断言——`saves.ts` / `save.tsx` / `settings.tsx` 无本组 legacy 存档调用且有 `SaveSystem`;`hotkey.ts` / `toolbar.tsx` / `viewmap.tsx` 无 `core.open*` 且有 `*UI` / `UIController`;`settings.tsx` / `toolbar.tsx` 无 `core.ui` 且有 `mainUIController`;无 `@motajs/legacy-ui` 的 `mainUi` 导入;CRLF;范围 / `git status` 守卫。**不设全仓类型 / 构建门禁。**
⑨ **开放项:** OI-1(批次拆分)、OI-2(R1)、OI-3(R2)、OI-4(命名)。**无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户明确回复「可以执行」四个字**;回复其它内容视为未通过。
</action>
<decision>是否按本计划(09-15)执行任务 7 · 第一批改造 · 第 3 组:存档体系替换(→ `SaveSystem`)与 UI 打开 / 控制替换(→ `*UI` / `mainUIController` / `UIController.open`),R1/R2 无法满足时按 STOP 处置</decision>
<context>
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = ALT 范围确认 + R1(`SaveSystem` 映射)+ R2(UI 打开传参)+ 命名影响确认。行号为规划日实测,以执行日为准。
</context>
<options>
<option id="execute-default">
<name>可以执行(采纳全部默认处置;R1/R2 触发时按 STOP 处置)</name>
<pros>如实落实可替代部分;结构不等价处不猜测;保留全部 ALT 追踪</pros>
<cons>若 R1/R2 部分成立,个别条目可能暂缓</cons>
</option>
<option id="execute-saves-first">
<name>可以执行 + 先只做存档组(Task 1),UI 打开组另行计划</name>
<pros>范围更小、更稳</pros>
<cons>须再拆一计划,UI 组延后</cons>
</option>
<option id="revise">
<name>修订分组范围 / R1/R2 处置后重新汇报</name>
<pros>避免带着未定口径改造</pros>
<cons>须修订 PLAN.md 后重新汇报</cons>
</option>
</options>
<resume-signal>回复「可以执行」四个字(如需指定 R1/R2 的处置或调整 OI,请在回复中指明)</resume-signal>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['packages-user/client-modules/src/render/utils/saves.ts','packages-user/client-modules/src/ui/save.tsx','packages-user/client-modules/src/ui/settings.tsx','packages-user/client-modules/src/action/hotkey.ts','packages-user/client-modules/src/ui/toolbar.tsx','packages-user/client-modules/src/ui/viewmap.tsx','packages-user/client-base/src/save/system.ts','packages-user/client-modules/src/ui/controller.tsx','packages-user/client-modules/src/ui/book.tsx','packages-user/client-modules/src/ui/toolbox.tsx','packages-user/client-modules/src/ui/equipbox.tsx','packages-user/client-modules/src/ui/shop.tsx']){if(!fs.existsSync(p))bad.push('missing: '+p)}if(bad.length){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK prestate intact (saves + UI callsites + SaveSystem + UI defs present)')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(调用点 / `SaveSystem` / UI 定义文件缺失)</fails_when>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 R1/R2 处置,须据其指定执行)</human-check>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」四个字
- 汇报内容涵盖:D-50 / D-52 / D-53、本组 14 条 ALT 条目表、逐文件影响表、R1(`SaveSystem` 映射)、R2(UI 打开传参 / `core.ui` 等价物)、编辑策略、命名影响、OI-1..OI-4
- 汇报中已原样告知:结构不等价处不猜测、不引入 `client` 单例、只改 ALT、CRLF 与范围零外溢
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
</acceptance_criteria>
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
</task>
<task type="auto">
<name>Task 1: 存档体系替换 —— `core.saves` / `core.getSave` / `core.getAllSaves` / `core.setLocalForage` / `core.removeLocalForage` / `core.removeSave` / `core.clearLocalForage` → `SaveSystem`(7 条)</name>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 R1 / OI 另有指定,按指定执行)。</precondition>
<files>packages-user/client-modules/src/render/utils/saves.ts, packages-user/client-modules/src/ui/save.tsx, packages-user/client-modules/src/ui/settings.tsx</files>
<read_first>
- packages-user/client-modules/src/render/utils/saves.ts:19-51,124-196(`getSave` / `getAllSaves` / `core.saves` / `core.setLocalForage` / `core.removeLocalForage`)
- packages-user/client-modules/src/ui/save.tsx:245-255,525-545(`core.saves.saveIndex` / `core.removeSave`)
- packages-user/client-modules/src/ui/settings.tsx:548-660(`core.clearLocalForage` / `core.saves.*`)
- packages-user/client-base/src/save/system.ts:1-222(`SaveSystem` / `GlobalTransaction` / `load` / `deleteSave` / `getGlobal` / `setGlobal` / `startGlobalTransaction`)
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-11 主对象经 props 获取、禁 `client` 单例)
</read_first>
<action>
(1) **核实映射(先于编辑,R1):** 读 `SaveSystem` API 与各调用点,逐条建立 `core.*` → `SaveSystem` 的等价映射:
- `core.getSave(index, cb)` → `SaveSystem.load(id)`(异步契约差异:回调 → Promise,须等价改写);
- `core.getAllSaves(cb)` → 审计替代锚点 `getAllSaves`(`saves.ts:36`)**即为本调用点所在的包装函数本身**(其内部第 38 行仍调用 `core.getAllSaves`),属**自指锚点**;`SaveSystem`(`save/system.ts:41`)**无存档枚举方法**,故须核实是否存在真正的枚举来源(`SaveSystem` 索引 / 全局存储或新存档存储的列表 API);若确认无等价枚举路径,**记录锚点问题并 STOP**(与 09-16 `C:drawThumbnail` 同处置),不得猜测(不得把保留 `core.getAllSaves` 的包装函数视作完成);
- `core.setLocalForage(key, value[, cb])` → `SaveSystem.setGlobal(key, value)`;
- `core.removeLocalForage(key[, cb])` → `SaveSystem.deleteSave(...)`(按 key 语义核实,审计定位 `deleteSave`);
- `core.clearLocalForage(cb)` → `SaveSystem.startGlobalTransaction(...)`(清空全局存储);
- `core.saves.saveIndex` / `core.saves.ids` → `SaveSystem` 等价字段 / 方法;
- `core.removeSave(index)` → `SaveSystem.deleteSave(...)`。
若某条无法等价(字段 / 语义缺失),**记录并 STOP**,不得猜测。
(2) **改写(R1 满足前提下):** 按审计替代方案改写 `saves.ts`、`save.tsx`、`settings.tsx` 中相应调用;经主对象(props)取得存档系统实例(D-11,禁 `client` 单例);保持行为等价。
(3) **不 invent / 不越界:** 只改本组 ALT 存档调用;`core.doSL` / `core.download` / `core.syncSave` / `core.firstData` / `core.formatDate2` / `core.encodeRoute`(→09-16)/ `core.removeLocalStorage` 等属其它分类或其它组,**本任务不改**。
(4) **命名 / 行尾 / 注释:** 预期不新增命名;如必须新增须呈报批准(D-04)。保持 CRLF;不改既有注释 / JSDoc;不使用 `import type`。
</action>
<reversibility rating="reversible">三文件调用点替换;`git revert` 可回退。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];const code=p=>fs.readFileSync(p,'utf8').split(/\r?\n/).filter(l=>!/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');const s=code('packages-user/client-modules/src/render/utils/saves.ts');for(const m of ['core.getSave','core.getAllSaves','core.setLocalForage','core.removeLocalForage','core.saves']){if(new RegExp(m.replace('.','\\.')).test(s))bad.push('saves.ts still uses '+m)}if(!/SaveSystem|save\./.test(s))bad.push('saves.ts: no SaveSystem usage');const v=code('packages-user/client-modules/src/ui/save.tsx');if(/core\.removeSave\s*\(/.test(v))bad.push('save.tsx still uses core.removeSave');const st=code('packages-user/client-modules/src/ui/settings.tsx');if(/core\.clearLocalForage\s*\(/.test(st))bad.push('settings.tsx still uses core.clearLocalForage');if(bad.length){console.error('SAVES GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK saves callsites replaced with SaveSystem')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "SAVES GATE FAILED"(残留本组存档 legacy 调用,或缺少 `SaveSystem` 用法)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['packages-user/client-modules/src/render/utils/saves.ts','packages-user/client-modules/src/ui/save.tsx','packages-user/client-modules/src/ui/settings.tsx']){const b=fs.readFileSync(p,'utf8');if(b.indexOf('\r\n')<0)bad.push('not CRLF: '+p);if(/\bimport\s+type\b/.test(b))bad.push('import type: '+p)}if(bad.length){console.error('CRLF/IMPORT FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK files CRLF and no import type')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF/IMPORT FAILED"(非 CRLF 或出现 `import type`)</fails_when>
<automated>git status --porcelain</automated>
<fails_when>输出包含本计划 `files_modified` 之外、且非用户既有并发改动(`packages-user/data-state/**`)的生产文件改动</fails_when>
</verify>
<acceptance_criteria>
- 7 条存档 ALT:`saves.ts` / `save.tsx` / `settings.tsx` 中相应调用改为 `SaveSystem`;无残留本组 legacy 存档调用(除 STOP 分支记录者)
- `C:getAllSaves` 的自指锚点已按 R1 处置:或核实到真实枚举来源并按真实符号改写,或记录锚点问题并 STOP(不得保留 `core.getAllSaves` 包装视作完成)
- R1 映射结论写入 SUMMARY;无法等价的条目记录并 STOP,未猜测
- 未触碰非 ALT 用法;未引入 `client` 单例;未新增未批准命名
- 被改文件 CRLF、无 `import type`;未改 `files_modified` 外文件(用户并发改动除外)
</acceptance_criteria>
<done>存档体系 7 条替换到位(或记录 STOP);SAVES / CRLF / scope 门禁全绿</done>
</task>
<task type="auto">
<name>Task 2: UI 打开 / 控制替换 —— `core.open*` / `core.ui` / `mainUi` → `*UI` / `mainUIController` / `UIController.open`(7 条)</name>
<precondition>Task 1 已完成(或已按 R1 STOP 记录);Task 0 已获批。</precondition>
<files>packages-user/client-modules/src/action/hotkey.ts, packages-user/client-modules/src/ui/toolbar.tsx, packages-user/client-modules/src/ui/viewmap.tsx, packages-user/client-modules/src/ui/settings.tsx</files>
<read_first>
- packages-user/client-modules/src/action/hotkey.ts:475-515(`core.openBook` / `openToolbox` / `openEquipbox` / `openQuickShop`)
- packages-user/client-modules/src/ui/toolbar.tsx:1-30,85-205(`core.open*` / `core.ui._drawViewMaps` / `mainUIController` 既有用法)
- packages-user/client-modules/src/ui/viewmap.tsx:125-140(`core.openBook`)
- packages-user/client-modules/src/ui/settings.tsx:1-70,155-165,685-700(`mainUi` / `mainUi.open('settings')` / `core.ui.closePanel` / 既有 `controller.open(MainSettingsUI, …)`)
- packages-user/client-modules/src/ui/book.tsx:251、toolbox.tsx:278、equipbox.tsx:551、shop.tsx:491(`BookUI` / `ToolBoxUI` / `EquipBoxUI` / `ShopUI`)
- packages-user/client-modules/src/ui/controller.tsx:5(`mainUIController`)
- packages/system/src/ui/controller.ts(`UIController.open`)
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-11)
</read_first>
<action>
(1) **核实接入(先于编辑,R2):** 确认打开 `BookUI` / `ToolBoxUI` / `EquipBoxUI` / `ShopUI` 所需的 `UIController.open(ui, props)` 传参与 props 取得路径(D-11:经 props / 主对象;`mainUIController`(`ui/controller.tsx:5`)为既有合法入口,`ui/toolbar.tsx` 已沿用);核实 `core.ui.closePanel()` → `mainUIController` 的等价方法(审计述 `closeAll`)与 `core.ui._drawViewMaps()` 的等价物。无法等价则**记录并 STOP**。
(2) **改写(R2 满足前提下):**
- `action/hotkey.ts`:`core.openBook(true)` / `core.openToolbox(true)` / `core.openEquipbox(true)` / `core.openQuickShop(true)` → 对应 `*UI` 经 `mainUIController.open(…)`(参数按新契约;原 `true` 的语义按真实接口等价);
- `ui/toolbar.tsx`:两处 `core.openBook(true)` 与 `core.openToolbox(true)` / `core.openEquipbox(true)` / `core.openQuickShop(true)` 同上;`core.ui._drawViewMaps()` → 等价物;
- `ui/viewmap.tsx`:`core.openBook(true)` → `BookUI` + `mainUIController.open`;
- `ui/settings.tsx`:`mainUi.open('settings')` → `mainUIController.open(MainSettingsUI, …)`;`core.ui.closePanel()` → `mainUIController` 等价方法;移除 `import { mainUi } from '@motajs/legacy-ui'`(A-1-6 / A-5-2 / C:ui)。
(3) **不 invent / 不越界:** 只改本组 ALT UI 打开 / 控制调用;`core.book` 之类不存在的符号不得造;`ui/settings.tsx` 内 `core.restart` / `core.doSL` / `core.startGame` / `core.chooseReplayFile` / `core.download` / `core.encodeRoute`(→09-16)等**本任务不改**;`ui/viewmap.tsx` 内 `core.hasItem` / `core.flyTo`(→09-16 / TBD)不改。
(4) **命名 / 行尾 / 注释:** 预期不新增命名;如必须新增须呈报批准(D-04)。保持 CRLF;不改既有注释 / JSDoc;不使用 `import type`;不新增 `?.`(dev.md 允许场景除外)。
</action>
<reversibility rating="reversible">四文件调用点替换;`git revert` 可回退。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];const code=p=>fs.readFileSync(p,'utf8').split(/\r?\n/).filter(l=>!/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');const hk=code('packages-user/client-modules/src/action/hotkey.ts');for(const m of ['core.openBook','core.openToolbox','core.openEquipbox','core.openQuickShop']){if(new RegExp(m.replace('.','\\.')+'\\(',).test(hk))bad.push('hotkey.ts still uses '+m)}if(!/BookUI|ToolBoxUI|EquipBoxUI|ShopUI|UIController/.test(hk))bad.push('hotkey.ts: no UI replacement');const tb=code('packages-user/client-modules/src/ui/toolbar.tsx');for(const m of ['core.openBook','core.openToolbox','core.openEquipbox','core.openQuickShop','core.ui']){if(new RegExp(m.replace('.','\\.')).test(tb))bad.push('toolbar.tsx still uses '+m)}const vm=code('packages-user/client-modules/src/ui/viewmap.tsx');if(/core\.openBook\s*\(/.test(vm))bad.push('viewmap.tsx still uses core.openBook');const st=code('packages-user/client-modules/src/ui/settings.tsx');if(/core\.ui\./.test(st))bad.push('settings.tsx still uses core.ui');if(/from '@motajs\/legacy-ui'/.test(st)&&/mainUi/.test(st))bad.push('settings.tsx still imports mainUi');if(bad.length){console.error('UIOPEN GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK UI open/control callsites replaced')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "UIOPEN GATE FAILED"(残留本组 UI 打开 / 控制 legacy 调用或 `mainUi` 导入,或缺少替代符号)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['packages-user/client-modules/src/action/hotkey.ts','packages-user/client-modules/src/ui/toolbar.tsx','packages-user/client-modules/src/ui/viewmap.tsx','packages-user/client-modules/src/ui/settings.tsx']){const b=fs.readFileSync(p,'utf8');if(b.indexOf('\r\n')<0)bad.push('not CRLF: '+p);if(/\bimport\s+type\b/.test(b))bad.push('import type: '+p)}if(bad.length){console.error('CRLF/IMPORT FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK files CRLF and no import type')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF/IMPORT FAILED"(非 CRLF 或出现 `import type`)</fails_when>
<automated>git status --porcelain</automated>
<fails_when>输出包含本计划 `files_modified` 之外、且非用户既有并发改动(`packages-user/data-state/**`)的生产文件改动</fails_when>
</verify>
<acceptance_criteria>
- 7 条 UI ALT:`hotkey.ts` / `toolbar.tsx` / `viewmap.tsx` / `settings.tsx` 中 `core.open*` / `core.ui` / `mainUi` 改为 `*UI` / `mainUIController` / `UIController.open`;无残留本组 legacy 调用(除 STOP 分支记录者)
- R2 结论写入 SUMMARY;无法等价的条目记录并 STOP,未猜测
- 未触碰非 ALT 用法;未引入 `client` 单例;未新增未批准命名
- 被改文件 CRLF、无 `import type`;未改 `files_modified` 外文件(用户并发改动除外)
</acceptance_criteria>
<done>UI 打开 / 控制 7 条替换到位(或记录 STOP);UIOPEN / CRLF / scope 门禁全绿;14 条全部落地</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 范围边界 | 只改 ALT 条目相关用法;不触碰 DIRECT / ADAPT / NONE / TBD(D-53) |
| 替代依据边界 | 替代方案只取自审计;结构不等价处记录并 STOP,**不猜**(D-52 / R1 / R2) |
| 单例边界 | 不得引入 `client` 单例(D-11) |
| 命名边界 | 未经批准不得新增命名(D-04) |
| 文件 / 提交边界 | 仅改 `files_modified`;不把用户并发改动(`data-state/**`)纳入提交 |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-15-01 | Tampering | 臆造 `SaveSystem` 无对应能力的映射(含 `C:getAllSaves` 自指锚点)/ 编造 `core.ui` 等价方法 | high | mitigate | R1/R2 前置核实;prohibitions 禁猜;无法等价(含无枚举来源)则 STOP + 记录 |
| T-09-15-02 | Elevation of Privilege | 为接入而引入 `client` 单例 / 使用未批准命名 | high | mitigate | D-11 / D-04;prohibitions |
| T-09-15-03 | Tampering | 越界改动非 ALT 用法(`core.doSL` / `core.status` 等) | high | mitigate | 范围门禁 + `<action>` 明列不改项 + `git status` 守卫 |
| T-09-15-04 | Repudiation | 保留 `core.saves` / `core.open*` 却视作完成 | medium | mitigate | SAVES / UIOPEN 门禁强制 legacy 用法消失 |
| T-09-15-05 | Information Disclosure | 把用户并发改动(`data-state/**`)误纳入提交 | medium | mitigate | `git status` 守卫豁免既有并发改动 |
| T-09-15-06 | Repudiation | 未跑静态门禁即提交 | medium | mitigate | Task 1/2 各有自动化门禁 + SUMMARY |
| T-09-15-SC | Tampering | npm/pip/cargo installs | low | accept | 无第三方包安装 |
</threat_model>
<verification>
- **Task 1**:`saves.ts` / `save.tsx` / `settings.tsx` 无本组 legacy 存档调用且有 `SaveSystem`;`C:getAllSaves` 自指锚点按 R1 处置(改写或 STOP);R1 结论写入 SUMMARY;CRLF / import type 检查;范围门禁。
- **Task 2**:`hotkey.ts` / `toolbar.tsx` / `viewmap.tsx` 无 `core.open*` 且有 `*UI` / `UIController`;`settings.tsx` / `toolbar.tsx` 无 `core.ui` 且有 `mainUIController`;无 `mainUi` 导入;R2 结论写入 SUMMARY;CRLF / import type 检查;范围门禁。
- **R1/R2 触发时**:记录并 STOP,相应条目标记「待裁定」。
- **无全仓门禁**:不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
- **人工复核**:结论写入 `09-15-SUMMARY.md`。
</verification>
<success_criteria>
- 本组 14 条 ALT:存档 7 条(→ `SaveSystem`,含 `C:getAllSaves` 自指锚点按 R1 处置)+ UI 打开 / 控制 7 条(→ `*UI` / `mainUIController` / `UIController.open`)按审计落地。
- R1 / R2 若成立,**如实记录并 STOP**,不猜测、不保留 legacy 用法冒充完成。
- 零越界:不触碰其它分类、不引入 `client` 单例、不新增未批准命名。
- 静态门禁(SAVES / UIOPEN / CRLF / 范围)全绿(或按 STOP 分支记录)。
</success_criteria>
## Artifacts this phase produces
| 符号 / 路径 | 类型 | 说明 |
|---|---|---|
| `packages-user/client-modules/src/render/utils/saves.ts`(修改) | 源码 | 存档工具 → `SaveSystem` |
| `packages-user/client-modules/src/ui/save.tsx`(修改) | 源码 | `core.removeSave` → `SaveSystem.deleteSave` |
| `packages-user/client-modules/src/ui/settings.tsx`(修改) | 源码 | `core.clearLocalForage` / `core.ui` / `mainUi` → 新接口 |
| `packages-user/client-modules/src/action/hotkey.ts`(修改) | 源码 | `core.open*` → `*UI` + `UIController.open` |
| `packages-user/client-modules/src/ui/toolbar.tsx`(修改) | 源码 | `core.open*` / `core.ui` → `*UI` / `mainUIController` |
| `packages-user/client-modules/src/ui/viewmap.tsx`(修改) | 源码 | `core.openBook` → `BookUI` |
| `.planning/phases/09-minimum-browser-runnable/09-15-SUMMARY.md`(新增) | 执行摘要 | 本组 ALT、逐文件改动、R1/R2 结论、命名影响、门禁结果 |
**命名说明:** 预期**不新增源码命名**;如需新增须先获用户批准。
## Source Coverage Audit
| 来源 | 项 | 处置 |
|------|----|------|
| GOAL | Phase 9(ROADMAP) | COVERED(去 legacy 存档 / UI 打开子切片) |
| REQ | 「Requirements: TBD」 | N/A(`requirements: []`) |
| RESEARCH | 无 RESEARCH.md | N/A |
| CONTEXT | D-50(第一批 = 40 条 ALT) | COVERED(本组 14/40) |
| CONTEXT | D-51(准确记录源文件清单) | COVERED(`files_modified` + 逐文件影响表) |
| CONTEXT | D-52(依据审计,不 invent) | COVERED(prohibitions + R1/R2 STOP) |
| CONTEXT | D-53(其它分类不在本批) | COVERED(范围门禁) |
| CONTEXT | D-03 / D-04(执行前获批 / 命名获批) | COVERED(Task 0 + prohibitions) |
| 缺口 | R1(`SaveSystem` 映射) / R2(UI 打开传参) | OPEN(Task 0 OI-2 / OI-3,待用户裁定) |
<output>
Create `.planning/phases/09-minimum-browser-runnable/09-15-SUMMARY.md` when done
</output>

View File

@ -0,0 +1,350 @@
---
phase: 09-minimum-browser-runnable
plan: 16
type: execute
wave: 13
depends_on: [09-15]
files_modified:
- packages-user/client-modules/src/action/hotkey.ts
- packages-user/client-base/src/components/thumbnail.tsx
- packages-user/client-modules/src/ui/settings.tsx
- packages-user/client-modules/src/ui/viewmap.tsx
- packages-user/client-base/src/elements/cache.ts
- packages-user/client-modules/src/ui/toolbar.tsx
- .planning/phases/09-minimum-browser-runnable/09-16-SUMMARY.md
autonomous: false
requirements: []
estimate:
tokens: 44000
raw_tokens: 49000
tasks: 3
confidence: low
must_haves:
truths:
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03)**:Task 0 必须先汇报「本计划 = 任务 7 · 第一批改造(ALT)· 4 个计划中的第 4 个(输入 / 绘制 / 素材 / 杂项)」的要做什么 / 为什么 / 大致怎么做 / 影响范围,**必须列明:逐条 ALT 条目表(9 条);逐文件影响表(6 文件);须核实的点(R1:`getVitualKeyOnce` → `Keyboard` 的事件 / 契约差异;R2:`core.drawThumbnail` / `core.encodeRoute` / `core.hasItem` / `core.getTilesetOffset` / `core.debug` 的替代是否真为等价,`getTilesetOffset` 可能需新增访问器);命名影响;开放项 OI-1..OI-5**,然后必须**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行本计划任何一步、不得派发执行子代理,且不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行。"
- "**D-50 / D-52(范围 / 依据):** 实现本批 ALT 之**第 4 组**(9 条:C:debug / C:drawThumbnail / C:encodeRoute / C:hasItem / C:getTilesetOffset / A-1-7 / A-1-9 / A-5-3 / A-5-7),严格依据审计替代方案,**不 invent**。"
- "**D-53(其它分类不在本批):** 不触碰 `可直接替换` / `有替代但需适配` / `无替代` / `待定`;本组只改 ALT 条目。"
- "**R1(输入契约差异):** `getVitualKeyOnce()`(`@motajs/legacy-ui`,返回 `{ key, assist }` 的 Promise)在 `ui/settings.tsx:71` 与 `ui/toolbar.tsx:101` 用于「捕获下一次虚拟按键」;审计替代为 `Keyboard`(`packages/system/src/action/keyboard.ts:51`),获取方式 / 事件契约不同。执行日须核实等价捕获路径;无法等价则**记录并 STOP**。"
- "**R2(绘制 / 素材 / 杂项替代核实):** ① `core.drawThumbnail`(thumbnail.tsx:75)的审计替代锚点为组件内本地函数(thumbnail.tsx:50),但该函数**本身即调用 `core.drawThumbnail`**(第 75 行);须核实是否存在真正的本地绘制实现,若无则**记录锚点问题并 STOP**;② `core.getTilesetOffset`(cache.ts:131)→ `TextureManager`(manager.ts:37)为内部 `tilesetOffsetMap`,**未暴露同名公开方法**,可能需新增访问器(触发 D-04 命名批准)或改访问路径;③ `core.encodeRoute`(settings.tsx:228)→ `ReplaySystem`(replay/system.ts:21)、`core.hasItem`(viewmap.tsx:133)→ `HeroItems`(items.ts:18)、`core.debug`(hotkey.ts:514)→ `logger`(logger.ts:229)均须核实语义等价。"
- "**范围零外溢 + 静态门禁:** 只改 `files_modified`;不改既有注释 / JSDoc;不用 `import type`;源码 CRLF;验证一律静态断言(legacy 用法消失 + 替代符号出现 + CRLF + 范围 / `git status` 守卫),**不使用** `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。"
artifacts:
- path: packages-user/client-modules/src/action/hotkey.ts
provides: "`core.debug` → `logger`。"
- path: packages-user/client-base/src/components/thumbnail.tsx
provides: "`core.drawThumbnail` → 本地绘制(按 R2 核实)。"
- path: packages-user/client-modules/src/ui/settings.tsx
provides: "`core.encodeRoute` → `ReplaySystem`;`getVitualKeyOnce` → `Keyboard`。"
- path: packages-user/client-modules/src/ui/viewmap.tsx
provides: "`core.hasItem` → `HeroItems`。"
- path: packages-user/client-base/src/elements/cache.ts
provides: "`core.getTilesetOffset` → `TextureManager`(按 R2 核实访问路径)。"
- path: packages-user/client-modules/src/ui/toolbar.tsx
provides: "`getVitualKeyOnce` → `Keyboard`。"
- path: .planning/phases/09-minimum-browser-runnable/09-16-SUMMARY.md
provides: "本计划执行摘要:本组 ALT、逐文件改动、R1/R2 结论、命名影响、门禁结果。"
key_links:
- "`action/hotkey.ts:514`(`core.debug`)↔ `packages/common/src/logger.ts:229`(`logger`)—— C:debug"
- "`client-base/src/components/thumbnail.tsx:50,75`(本地 `drawThumbnail` ↔ `core.drawThumbnail`)—— C:drawThumbnail"
- "`ui/settings.tsx:228`(`core.encodeRoute`)↔ `packages-user/data-common/src/replay/system.ts:21`(`ReplaySystem`)—— C:encodeRoute"
- "`ui/viewmap.tsx:133`(`core.hasItem`)↔ `packages-user/data-base/src/hero/items.ts:18`(`HeroItems`)—— C:hasItem"
- "`client-base/src/elements/cache.ts:131`(`core.getTilesetOffset`)↔ `packages-user/client-base/src/material/manager.ts:37`(`TextureManager`)—— C:getTilesetOffset"
- "`ui/settings.tsx:15,71` 与 `ui/toolbar.tsx:14,101`(`getVitualKeyOnce`)↔ `packages/system/src/action/keyboard.ts:51`(`Keyboard`)—— A-1-7 / A-1-9 / A-5-3 / A-5-7"
assumptions:
- "**执行日行号可能漂移**:引用行号为 2026-10-11 规划日实测;门禁以执行日实测为准。"
- "**用户并发改动(D-56)**:执行期存在用户 `packages-user/data-state/**` 并发改动;`git status` 守卫豁免这些既有改动。"
- "**Markdown 使用 LF,源码使用 CRLF**。"
prohibitions:
- "不得在用户明确回复「可以执行」四个字之前执行本计划任何一步、或派发任何执行子代理。"
- "**不得 invent 替代 / 不得造访问器**:`getTilesetOffset` 若需新增公开访问器,须先呈报命名并获批准(D-04);`drawThumbnail` / `encodeRoute` / `hasItem` / `debug` 无法等价则记录并 STOP。"
- "**不得引入 `client` 单例**;不得为接入而新增未批准命名(D-04)。"
- "不得触碰非 ALT 分类用法(如 `core.flyTo` / `core.material` / `core.getAnimateFrames` / `core.formatBigNumber` / `core.doSL` 等)—— 它们属其它分类或其它组。"
- "不得改 09-01..09-15 任何文件;不得改既有注释 / JSDoc;不得用 `import type`;不得新增 `?.`(dev.md 允许场景除外)。"
- "不得把门禁建立在「全仓 TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上。"
open_items:
- "**OI-1(批次拆分):** 本计划为任务 7 批次 4 计划中的第 4 个;**待用户确认**。"
- "**OI-2(`getVitualKeyOnce` → `Keyboard` 契约,R1):** 捕获下一次虚拟按键的等价路径待核实。**待用户裁定。**"
- "**OI-3(`drawThumbnail` 锚点,R2①):** 审计替代锚点为组件内本地函数,但该函数仍调用 `core.drawThumbnail`;是否存在真正的本地绘制实现待核实。**待用户裁定**(若不存在则本条目暂缓 / 反对照)。"
- "**OI-4(`getTilesetOffset` 访问器,R2②):** `TextureManager` 未暴露同名公开方法;是否新增访问器(命名须批准)待裁定。**待用户裁定。**"
- "**OI-5(命名影响):** 预期除非 OI-4 需新增访问器,否则不新增命名;如新增须呈报批准。**待用户确认。**"
---
<objective>
Phase 9 · 任务 7 · 第一批改造(ALT 条目)· **第 4 组 / 共 4 组** —— **输入 / 绘制 / 素材 / 杂项**(9 条 ALT)。
**要解决的问题(D-50..D-53):** 本组处理剩余 ALT:调试(`core.debug`)、缩略图绘制(`core.drawThumbnail`)、录像编码(`core.encodeRoute`)、道具持有(`core.hasItem`)、素材偏移(`core.getTilesetOffset`),以及虚拟按键捕获(`getVitualKeyOnce` → `Keyboard`)。
**大致解决方案(本计划 = 09-16):**
- **Task 0(`blocking-human`)= 预执行汇报关卡**:汇报本组范围、逐条 ALT 表、逐文件影响表、R1(输入契约)、R2(绘制 / 素材 / 杂项替代核实)、命名影响、OI-1..OI-5,等待「可以执行」。
- **Task 1 = 输入替换(4 条)**:`ui/settings.tsx` 与 `ui/toolbar.tsx` 的 `getVitualKeyOnce` → `Keyboard`(A-1-7 / A-1-9 / A-5-3 / A-5-7);R1 核实契约,无法等价则 STOP。
- **Task 2 = 绘制 / 素材 / 杂项替换(5 条)**:`action/hotkey.ts`(C:debug)、`client-base/src/components/thumbnail.tsx`(C:drawThumbnail)、`ui/settings.tsx`(C:encodeRoute)、`ui/viewmap.tsx`(C:hasItem)、`client-base/src/elements/cache.ts`(C:getTilesetOffset);R2 核实等价,无法等价则 STOP。
**API coverage:** `detected:false`。**`No external API integration: 本计划仅为源码改造,不涉及任何外部 API。`**
</objective>
<execution_context>
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
@.planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md
@.planning/phases/09-minimum-browser-runnable/09-LEGACY-INTERFACES-AUDIT.md
@AGENTS.md
@dev.md
</context>
## ALT 条目清单与影响表(本组 9 条)
| ID | 原接口/成员(锚点) | 替代方案(锚点) | 涉及文件:行 | 编辑要点 |
| --- | --- | --- | --- | --- |
| C:debug | `core.debug`(hotkey.ts:514) | `logger`(packages/common/src/logger.ts:229) | action/hotkey.ts:514 | 调试改由 `logger`(R2③ 核实等价) |
| C:drawThumbnail | `core.drawThumbnail`(thumbnail.tsx:75) | 组件内 `drawThumbnail`(thumbnail.tsx:50) | client-base/src/components/thumbnail.tsx:50,75 | 本地绘制替代;R2① 核实 |
| C:encodeRoute | `core.encodeRoute`(settings.tsx:228) | `ReplaySystem`(replay/system.ts:21) | ui/settings.tsx:228 | 录像编码改由 `ReplaySystem`(R2③) |
| C:hasItem | `core.hasItem`(viewmap.tsx:133) | `HeroItems`(items.ts:18) | ui/viewmap.tsx:133 | 道具持有查询改由 `HeroItems`(R2③) |
| C:getTilesetOffset | `core.getTilesetOffset`(cache.ts:131) | `TextureManager`(manager.ts:37) | client-base/src/elements/cache.ts:131 | 改由 `TextureManager` 内部偏移图(R2②,或需访问器) |
| A-1-7 | `getVitualKeyOnce`(settings.tsx:15) | `Keyboard`(keyboard.ts:51) | ui/settings.tsx:15,71 | 移除 legacy 导入;改 `Keyboard`(R1) |
| A-1-9 | `getVitualKeyOnce`(toolbar.tsx:14) | `Keyboard`(keyboard.ts:51) | ui/toolbar.tsx:14,101 | 移除 legacy 导入;改 `Keyboard`(R1) |
| A-5-3 | `getVitualKeyOnce().then(...)`(settings.tsx:71) | `Keyboard`(keyboard.ts:51) | ui/settings.tsx:71 | 改经 `Keyboard` 捕获(R1) |
| A-5-7 | `getVitualKeyOnce().then(...)`(toolbar.tsx:101) | `Keyboard`(keyboard.ts:51) | ui/toolbar.tsx:101 | 改经 `Keyboard` 捕获(R1) |
## 逐文件影响表(本组)
| 文件 | 动作 | 改动内容 | 关联 ALT |
| --- | --- | --- | --- |
| `packages-user/client-modules/src/action/hotkey.ts` | 修改 | `core.debug` → `logger` | C:debug |
| `packages-user/client-base/src/components/thumbnail.tsx` | 修改 | `core.drawThumbnail` → 本地绘制 | C:drawThumbnail |
| `packages-user/client-modules/src/ui/settings.tsx` | 修改 | `getVitualKeyOnce` → `Keyboard`;`core.encodeRoute` → `ReplaySystem` | A-1-7 / A-5-3 / C:encodeRoute |
| `packages-user/client-modules/src/ui/viewmap.tsx` | 修改 | `core.hasItem` → `HeroItems` | C:hasItem |
| `packages-user/client-base/src/elements/cache.ts` | 修改 | `core.getTilesetOffset` → `TextureManager` | C:getTilesetOffset |
| `packages-user/client-modules/src/ui/toolbar.tsx` | 修改 | `getVitualKeyOnce` → `Keyboard` | A-1-9 / A-5-7 |
<tasks>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 7 · 第一批改造(ALT)· 第 4 组:输入 / 绘制 / 素材 / 杂项」的范围、逐条 ALT 表、逐文件影响表、R1/R2 风险与 OI,等待用户回复「可以执行」</name>
<precondition>`packages-user/client-modules/src/action/hotkey.ts`、`ui/{settings,viewmap,toolbar}.tsx`、`packages-user/client-base/src/components/thumbnail.tsx`、`packages-user/client-base/src/elements/cache.ts` 存在;`packages/system/src/action/keyboard.ts` 存在;审计文档已存在。</precondition>
<files>(只读汇报,不修改任何文件)</files>
<read_first>
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-03 / D-04 / **D-50 / D-52 / D-53**)
- .planning/phases/09-minimum-browser-runnable/09-LEGACY-REPLACEMENT-AUDIT.md(§C 的 C:debug / C:drawThumbnail / C:encodeRoute / C:hasItem / C:getTilesetOffset 与 §A 的 A-1-7 / A-1-9 / A-5-3 / A-5-7)
- .planning/phases/09-minimum-browser-runnable/09-16-PLAN.md(本计划全文,含两表与 R1/R2)
- packages/system/src/action/keyboard.ts(`Keyboard`,R1)
- packages-user/client-base/src/components/thumbnail.tsx(本地 `drawThumbnail` ↔ `core.drawThumbnail`,R2①)
- packages-user/client-base/src/material/manager.ts(`TextureManager` 偏移图,R2②)
- AGENTS.md;dev.md(命名、注释、依赖单向、CRLF)
</read_first>
<action>
**停下并向用户汇报,等待明确回复「可以执行」后才继续;汇报本身只读。**
① **要解决的问题:** 任务 7 · 第一批改造(ALT)第 4 组:输入(`getVitualKeyOnce` → `Keyboard`)+ 绘制 / 素材 / 杂项(`core.debug` / `core.drawThumbnail` / `core.encodeRoute` / `core.hasItem` / `core.getTilesetOffset`),共 9 条。
② **逐条 ALT 条目表:** 原样呈现本计划「ALT 条目清单与影响表」的 9 行。
③ **逐文件影响表:** 原样呈现本计划「逐文件影响表」的 6 行,并说明 `files_modified` 精确为 6 个源文件。
④ **R1(输入契约,须用户知悉):** `getVitualKeyOnce()` 返回 `{ key, assist }` 的 Promise 用于捕获下一次虚拟按键;替代 `Keyboard` 的事件 / 契约不同;执行日核实等价捕获路径,无法等价则记录并 STOP。**OI-2。**
⑤ **R2(绘制 / 素材 / 杂项核实,须用户知悉):** ① `core.drawThumbnail` 的替代锚点(组件内本地函数)**本身仍调用 `core.drawThumbnail`**,须核实是否有真正的本地绘制实现(**OI-3**);② `core.getTilesetOffset` → `TextureManager` 未暴露同名公开方法,可能需新增访问器(命名须批准,**OI-4**);③ `core.encodeRoute` / `core.hasItem` / `core.debug` 的替代须核实语义等价。
⑥ **编辑策略:** Task 1 输入(4 条,2 文件);Task 2 绘制 / 素材 / 杂项(5 条,5 文件);`settings.tsx` / `toolbar.tsx` 与 Task 1 有重叠,**同计划内按文件隔离一次性完成**,避免中间态。
⑦ **范围与禁止:** 只改 ALT 条目相关用法;不触碰其它分类(`core.flyTo` / `core.material` / `core.getAnimateFrames` / `core.formatBigNumber` 等);不改 09-01..09-15;不改既有注释;不用 `import type`;不引入 `client` 单例;源码 CRLF。
⑧ **验证方式:** 静态断言——`hotkey.ts` 无 `core.debug`;`thumbnail.tsx` 无 `core.drawThumbnail`;`settings.tsx` 无 `getVitualKeyOnce` / `core.encodeRoute`;`viewmap.tsx` 无 `core.hasItem`;`cache.ts` 无 `core.getTilesetOffset`;`toolbar.tsx` 无 `getVitualKeyOnce`;且有对应替代符号;CRLF;范围 / `git status` 守卫。**不设全仓类型 / 构建门禁。**
⑨ **开放项:** OI-1(批次拆分)、OI-2(R1)、OI-3(`drawThumbnail` 锚点)、OI-4(`getTilesetOffset` 访问器 / 命名)、OI-5(命名影响)。**无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户明确回复「可以执行」四个字**;回复其它内容视为未通过。
</action>
<decision>是否按本计划(09-16)执行任务 7 · 第一批改造 · 第 4 组:输入替换(`getVitualKeyOnce` → `Keyboard`)与绘制 / 素材 / 杂项替换(`core.debug` / `drawThumbnail` / `encodeRoute` / `hasItem` / `getTilesetOffset`),R1/R2 无法满足时按 STOP 处置</decision>
<context>
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心 = ALT 范围确认 + R1(输入契约)+ R2(绘制 / 素材 / 杂项等价)+ 命名影响确认。行号为规划日实测,以执行日为准。
</context>
<options>
<option id="execute-default">
<name>可以执行(采纳全部默认处置;R1/R2 触发时按 STOP 处置)</name>
<pros>如实落实可替代部分;锚点可疑处不猜测;保留全部 ALT 追踪</pros>
<cons>若 R1/R2 部分成立,个别条目可能暂缓</cons>
</option>
<option id="execute-input-only">
<name>可以执行 + 先只做输入组(Task 1),绘制 / 素材 / 杂项组另行计划</name>
<pros>范围更小、更稳</pros>
<cons>须再拆一计划,杂项组延后</cons>
</option>
<option id="revise">
<name>修订分组范围 / R1/R2 处置后重新汇报</name>
<pros>避免带着未定口径改造</pros>
<cons>须修订 PLAN.md 后重新汇报</cons>
</option>
</options>
<resume-signal>回复「可以执行」四个字(如需指定 R1/R2 的处置或调整 OI,请在回复中指明)</resume-signal>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['packages-user/client-modules/src/action/hotkey.ts','packages-user/client-base/src/components/thumbnail.tsx','packages-user/client-modules/src/ui/settings.tsx','packages-user/client-modules/src/ui/viewmap.tsx','packages-user/client-base/src/elements/cache.ts','packages-user/client-modules/src/ui/toolbar.tsx','packages/system/src/action/keyboard.ts','packages/common/src/logger.ts','packages-user/data-base/src/hero/items.ts','packages-user/data-common/src/replay/system.ts']){if(!fs.existsSync(p))bad.push('missing: '+p)}if(bad.length){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK prestate intact (input/draw/material callsites + replacement symbols present)')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(调用点或替代符号文件缺失)</fails_when>
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对回复原文;用户若指定 R1/R2 处置,须据其指定执行)</human-check>
</verify>
<acceptance_criteria>
- 用户回复中明确包含「可以执行」四个字
- 汇报内容涵盖:D-50 / D-52 / D-53、本组 9 条 ALT 条目表、逐文件影响表、R1(输入契约)、R2(绘制 / 素材 / 杂项等价)、编辑策略、命名影响、OI-1..OI-5
- 汇报中已原样告知:锚点可疑处(`drawThumbnail`)不猜测、`getTilesetOffset` 访问器命名须批准、不引入 `client` 单例、只改 ALT、CRLF 与范围零外溢
- 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
</acceptance_criteria>
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
</task>
<task type="auto">
<name>Task 1: 输入替换 —— `getVitualKeyOnce` → `Keyboard`(A-1-7 / A-1-9 / A-5-3 / A-5-7)</name>
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字(若用户就 R1 / OI 另有指定,按指定执行)。</precondition>
<files>packages-user/client-modules/src/ui/settings.tsx, packages-user/client-modules/src/ui/toolbar.tsx</files>
<read_first>
- packages-user/client-modules/src/ui/settings.tsx:13-16,60-90(`getVitualKeyOnce` 导入与「虚拟按键」分支)
- packages-user/client-modules/src/ui/toolbar.tsx:10-30,95-115(`getVitualKeyOnce` 导入与用法)
- packages/system/src/action/keyboard.ts:1-90(`Keyboard` / `KeyboardEmits` / 捕获下一次按键的接口)
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-11 主对象经 props 获取、禁 `client` 单例)
</read_first>
<action>
(1) **核实捕获路径(先于编辑,R1):** 确认经新 `Keyboard`(`packages/system/src/action/keyboard.ts:51`)实现「捕获下一次虚拟按键并按 `{ key, assist }` 语义处理」的等价路径(事件订阅 / Promise 化)。无法等价则**记录并 STOP**。
(2) **改写(R1 满足前提下):**
- `ui/settings.tsx`:移除 `import { getVitualKeyOnce } from '@motajs/legacy-ui'`;将 `MainChoice.VirtualKey` 分支(约 71 行)的 `getVitualKeyOnce().then(...)` 改为经 `Keyboard` 捕获下一次按键(等价 `gameKey.emitKey(...)` 行为);
- `ui/toolbar.tsx`:移除 `import { getVitualKeyOnce } from '@motajs/legacy-ui'`;将约 101 行用法改为经 `Keyboard` 捕获(等价行为)。
(3) **不 invent / 不越界:** 只改 `getVitualKeyOnce` 相关用法;`toolbar.tsx` 的 `generateBinary`(A-1-10,TBD,不在本批)与 `core.open*`(09-15 已处理)等**本任务不改**;`settings.tsx` 的 `core.encodeRoute` 属 Task 2。
(4) **命名 / 行尾 / 注释:** 预期不新增命名;如必须新增须呈报批准(D-04)。保持 CRLF;不改既有注释 / JSDoc;不使用 `import type`。
</action>
<reversibility rating="reversible">两文件调用点替换;`git revert` 可回退。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];const code=p=>fs.readFileSync(p,'utf8').split(/\r?\n/).filter(l=>!/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');for(const p of ['packages-user/client-modules/src/ui/settings.tsx','packages-user/client-modules/src/ui/toolbar.tsx']){const t=code(p);if(/getVitualKeyOnce/.test(t))bad.push(p+' still uses getVitualKeyOnce');if(/from '@motajs\/legacy-ui'/.test(t)&&/getVitualKeyOnce/.test(fs.readFileSync(p,'utf8')))bad.push(p+' still imports legacy-ui');if(!/Keyboard/.test(t))bad.push(p+' no Keyboard usage');}if(bad.length){console.error('INPUT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK getVitualKeyOnce replaced with Keyboard')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "INPUT GATE FAILED"(残留 `getVitualKeyOnce` / legacy-ui 导入,或缺少 `Keyboard`)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['packages-user/client-modules/src/ui/settings.tsx','packages-user/client-modules/src/ui/toolbar.tsx']){const b=fs.readFileSync(p,'utf8');if(b.indexOf('\r\n')<0)bad.push('not CRLF: '+p);if(/\bimport\s+type\b/.test(b))bad.push('import type: '+p)}if(bad.length){console.error('CRLF/IMPORT FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK files CRLF and no import type')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF/IMPORT FAILED"(非 CRLF 或出现 `import type`)</fails_when>
<automated>git status --porcelain</automated>
<fails_when>输出包含本计划 `files_modified` 之外、且非用户既有并发改动(`packages-user/data-state/**`)的生产文件改动</fails_when>
</verify>
<acceptance_criteria>
- 4 条输入 ALT:`settings.tsx` / `toolbar.tsx` 无 `getVitualKeyOnce`、无相应 legacy-ui 导入,且经 `Keyboard` 捕获(或按 STOP 分支记录)
- R1 契约结论写入 SUMMARY;无法等价的条目记录并 STOP,未猜测
- 未触碰非 ALT 用法;未引入 `client` 单例;未新增未批准命名
- 被改文件 CRLF、无 `import type`;未改 `files_modified` 外文件(用户并发改动除外)
</acceptance_criteria>
<done>输入 4 条替换到位(或记录 STOP);INPUT / CRLF / scope 门禁全绿</done>
</task>
<task type="auto">
<name>Task 2: 绘制 / 素材 / 杂项替换 —— `core.debug` / `core.drawThumbnail` / `core.encodeRoute` / `core.hasItem` / `core.getTilesetOffset`(5 条)</name>
<precondition>Task 1 已完成(或已按 R1 STOP 记录);Task 0 已获批。</precondition>
<files>packages-user/client-modules/src/action/hotkey.ts, packages-user/client-base/src/components/thumbnail.tsx, packages-user/client-modules/src/ui/settings.tsx, packages-user/client-modules/src/ui/viewmap.tsx, packages-user/client-base/src/elements/cache.ts</files>
<read_first>
- packages-user/client-modules/src/action/hotkey.ts:508-516(`core.debug`)
- packages-user/client-base/src/components/thumbnail.tsx:43-84(本地 `drawThumbnail` ↔ `core.drawThumbnail`)
- packages-user/client-modules/src/ui/settings.tsx:210-232(`core.encodeRoute`)
- packages-user/client-modules/src/ui/viewmap.tsx:125-140(`core.hasItem`;注意 `core.flyTo` 属 TBD、不改)
- packages-user/client-base/src/elements/cache.ts:125-135,200-210(`core.getTilesetOffset`;注意 `core.getAnimateFrames` 属 TBD、不改)
- packages/common/src/logger.ts:220-229(`logger`)、packages-user/data-common/src/replay/system.ts:21(`ReplaySystem`)、packages-user/data-base/src/hero/items.ts:18(`HeroItems`)、packages-user/client-base/src/material/manager.ts:37(`TextureManager`)
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-11)
</read_first>
<action>
(1) **核实等价(先于编辑,R2):** 逐条确认替代是否等价:
- `core.drawThumbnail`(thumbnail.tsx:75):审计替代锚点为本地函数(50 行),但该函数**即调用 `core.drawThumbnail`**;核实是否存在真正的本地绘制实现;若无则**记录锚点问题并 STOP**(OI-3);
- `core.getTilesetOffset`(cache.ts:131)→ `TextureManager`(manager.ts:37):核对访问路径;若需新增公开访问器,须先呈报命名并获批准(D-04,OI-4);
- `core.debug`(hotkey.ts:514)→ `logger`(logger.ts:229)、`core.encodeRoute`(settings.tsx:228)→ `ReplaySystem`(replay/system.ts:21)、`core.hasItem`(viewmap.tsx:133)→ `HeroItems`(items.ts:18):核对语义等价。
无法等价则**记录并 STOP**,不得猜测。
(2) **改写(R2 满足前提下):** 按审计替代方案逐一改写对应调用点:`hotkey.ts`(`core.debug`)、`thumbnail.tsx`(`core.drawThumbnail`)、`settings.tsx`(`core.encodeRoute`)、`viewmap.tsx`(`core.hasItem`)、`cache.ts`(`core.getTilesetOffset`);经主对象 / 组件 props 取得所需实例(D-11,禁 `client` 单例)。
(3) **不 invent / 不越界:** 只改本组 ALT;`core.flyTo`(viewmap.tsx,TBD)、`core.getAnimateFrames`(cache.ts,TBD)、`core.material`(ADAPT)、`core.formatBigNumber`(NONE)等**本任务不改**。
(4) **命名 / 行尾 / 注释:** 除 OI-4 所述可能的访问器外不新增命名;如必须新增须呈报批准(D-04)。保持 CRLF;不改既有注释 / JSDoc;不使用 `import type`;不新增 `?.`(dev.md 允许场景除外)。
</action>
<reversibility rating="reversible">五文件调用点替换;`git revert` 可回退。</reversibility>
<verify>
<automated>node -e "const fs=require('fs');const bad=[];const code=p=>fs.readFileSync(p,'utf8').split(/\r?\n/).filter(l=>!/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');const checks=[['packages-user/client-modules/src/action/hotkey.ts','core.debug','logger'],['packages-user/client-base/src/components/thumbnail.tsx','core.drawThumbnail','drawThumbnail'],['packages-user/client-modules/src/ui/settings.tsx','core.encodeRoute','ReplaySystem'],['packages-user/client-modules/src/ui/viewmap.tsx','core.hasItem','HeroItems'],['packages-user/client-base/src/elements/cache.ts','core.getTilesetOffset','TextureManager']];for(const [p,legacy,repl] of checks){const t=code(p);if(new RegExp(legacy.replace('.','\\.')+'\\s*\\(').test(t))bad.push(p+' still uses '+legacy);if(!t.includes(repl))bad.push(p+' missing '+repl+' (or STOP not recorded)');}if(bad.length){console.error('MISC GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK draw/material/misc callsites replaced (or STOP recorded)')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "MISC GATE FAILED"(残留本组 legacy 调用或缺少替代符号且未记录 STOP)</fails_when>
<automated>node -e "const fs=require('fs');const bad=[];for(const p of ['packages-user/client-modules/src/action/hotkey.ts','packages-user/client-base/src/components/thumbnail.tsx','packages-user/client-modules/src/ui/settings.tsx','packages-user/client-modules/src/ui/viewmap.tsx','packages-user/client-base/src/elements/cache.ts']){const b=fs.readFileSync(p,'utf8');if(b.indexOf('\r\n')<0)bad.push('not CRLF: '+p);if(/\bimport\s+type\b/.test(b))bad.push('import type: '+p)}if(bad.length){console.error('CRLF/IMPORT FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK files CRLF and no import type')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CRLF/IMPORT FAILED"(非 CRLF 或出现 `import type`)</fails_when>
<automated>git status --porcelain</automated>
<fails_when>输出包含本计划 `files_modified` 之外、且非用户既有并发改动(`packages-user/data-state/**`)的生产文件改动</fails_when>
</verify>
<acceptance_criteria>
- 5 条绘制 / 素材 / 杂项 ALT:相应文件无 legacy 调用且有替代符号(或按 STOP 分支记录)
- R2 结论(含 `drawThumbnail` 锚点、`getTilesetOffset` 访问路径、语义等价性)写入 SUMMARY;无法等价的条目记录并 STOP,未猜测
- 未触碰非 ALT 用法;未引入 `client` 单例;如新增访问器已获命名批准(D-04)
- 被改文件 CRLF、无 `import type`;未改 `files_modified` 外文件(用户并发改动除外)
</acceptance_criteria>
<done>绘制 / 素材 / 杂项 5 条替换到位(或记录 STOP);MISC / CRLF / scope 门禁全绿;9 条全部落地</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 范围边界 | 只改 ALT 条目相关用法;不触碰 DIRECT / ADAPT / NONE / TBD(D-53) |
| 替代依据边界 | 替代方案只取自审计;锚点可疑 / 不等价处记录并 STOP,**不猜**(D-52 / R1 / R2) |
| 单例边界 | 不得引入 `client` 单例(D-11) |
| 命名边界 | 新增访问器 / 命名须先获批(D-04) |
| 文件 / 提交边界 | 仅改 `files_modified`;不把用户并发改动(`data-state/**`)纳入提交 |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-16-01 | Tampering | 臆造 `drawThumbnail` / `getTilesetOffset` 等替代 / 编造访问器 | high | mitigate | R1/R2 前置核实;prohibitions 禁猜;无法等价则 STOP + 记录;命名须批准 |
| T-09-16-02 | Elevation of Privilege | 为接入而引入 `client` 单例 / 使用未批准命名 | high | mitigate | D-11 / D-04;prohibitions |
| T-09-16-03 | Tampering | 越界改动非 ALT 用法(`core.flyTo` / `core.material` 等) | high | mitigate | 范围门禁 + `<action>` 明列不改项 + `git status` 守卫 |
| T-09-16-04 | Repudiation | 保留 `core.debug` / `core.drawThumbnail` 等却视作完成 | medium | mitigate | INPUT / MISC 门禁强制 legacy 用法消失 |
| T-09-16-05 | Information Disclosure | 把用户并发改动(`data-state/**`)误纳入提交 | medium | mitigate | `git status` 守卫豁免既有并发改动 |
| T-09-16-06 | Repudiation | 未跑静态门禁即提交 | medium | mitigate | Task 1/2 各有自动化门禁 + SUMMARY |
| T-09-16-SC | Tampering | npm/pip/cargo installs | low | accept | 无第三方包安装 |
</threat_model>
<verification>
- **Task 1**:`settings.tsx` / `toolbar.tsx` 无 `getVitualKeyOnce`、无 legacy-ui 导入,有 `Keyboard`;R1 结论写入 SUMMARY;CRLF / import type 检查;范围门禁。
- **Task 2**:`hotkey.ts` / `thumbnail.tsx` / `settings.tsx` / `viewmap.tsx` / `cache.ts` 无本组 legacy 调用,有替代符号(或 STOP 记录);R2 结论写入 SUMMARY;CRLF / import type 检查;范围门禁。
- **R1/R2 触发时**:记录并 STOP,相应条目标记「待裁定」。
- **无全仓门禁**:不使用 `pnpm check:type` / `pnpm build` / 全仓 TS 诊断数。
- **人工复核**:结论写入 `09-16-SUMMARY.md`。
</verification>
<success_criteria>
- 本组 9 条 ALT:输入 4 条(→ `Keyboard`)+ 绘制 / 素材 / 杂项 5 条(→ `logger` / 本地绘制 / `ReplaySystem` / `HeroItems` / `TextureManager`)按审计落地。
- R1 / R2 若成立,**如实记录并 STOP**,不猜测、不保留 legacy 用法冒充完成。
- 零越界:不触碰其它分类、不引入 `client` 单例、命名先获批。
- 静态门禁(INPUT / MISC / CRLF / 范围)全绿(或按 STOP 分支记录)。
</success_criteria>
## Artifacts this phase produces
| 符号 / 路径 | 类型 | 说明 |
|---|---|---|
| `packages-user/client-modules/src/action/hotkey.ts`(修改) | 源码 | `core.debug` → `logger` |
| `packages-user/client-base/src/components/thumbnail.tsx`(修改) | 源码 | `core.drawThumbnail` → 本地绘制 |
| `packages-user/client-modules/src/ui/settings.tsx`(修改) | 源码 | `getVitualKeyOnce` → `Keyboard`;`core.encodeRoute` → `ReplaySystem` |
| `packages-user/client-modules/src/ui/viewmap.tsx`(修改) | 源码 | `core.hasItem` → `HeroItems` |
| `packages-user/client-base/src/elements/cache.ts`(修改) | 源码 | `core.getTilesetOffset` → `TextureManager` |
| `packages-user/client-modules/src/ui/toolbar.tsx`(修改) | 源码 | `getVitualKeyOnce` → `Keyboard` |
| `.planning/phases/09-minimum-browser-runnable/09-16-SUMMARY.md`(新增) | 执行摘要 | 本组 ALT、逐文件改动、R1/R2 结论、命名影响、门禁结果 |
**命名说明:** 预期除非 OI-4(`getTilesetOffset` 访问器)外**不新增源码命名**;如新增须先获用户批准。
## Source Coverage Audit
| 来源 | 项 | 处置 |
|------|----|------|
| GOAL | Phase 9(ROADMAP) | COVERED(去 legacy 输入 / 绘制 / 素材子切片) |
| REQ | 「Requirements: TBD」 | N/A(`requirements: []`) |
| RESEARCH | 无 RESEARCH.md | N/A |
| CONTEXT | D-50(第一批 = 40 条 ALT) | COVERED(本组 9/40;4 计划合计 40) |
| CONTEXT | D-51(准确记录源文件清单) | COVERED(`files_modified` + 逐文件影响表) |
| CONTEXT | D-52(依据审计,不 invent) | COVERED(prohibitions + R1/R2 STOP) |
| CONTEXT | D-53(其它分类不在本批) | COVERED(范围门禁) |
| CONTEXT | D-03 / D-04(执行前获批 / 命名获批) | COVERED(Task 0 + prohibitions) |
| 缺口 | R1(输入契约) / R2(绘制 / 素材 / 杂项等价) | OPEN(Task 0 OI-2..OI-4,待用户裁定) |
<output>
Create `.planning/phases/09-minimum-browser-runnable/09-16-SUMMARY.md` when done
</output>