diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 4ba03c6..d9684da 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -481,11 +481,14 @@ Plans:
**Goal**: 在已完成的数据端(可在 Node 环境独立运行/回放验证)之上完成渲染端框架,产出可在浏览器中运行并验证的最小版本
**Requirements**: TBD
**Depends on**: Phase 8
-**Plans**: TBD
+**Plans**: 4 plans (任务 1·UI 接口适配,D-10..D-14;本阶段增量派发,其余任务待用户再派发)
Plans:
-- [ ] TBD (run /gsd-plan-phase 9 to break down)
+- [ ] 09-01-PLAN.md — 任务 1·D-10(上):`render/ui/` → `src/ui/` 迁移叶子/基础模块(save/statistics/viewmap/controller 去重)+ barrel 骨架 + `load.tsx`/`title.tsx` props 基类型修正
+- [ ] 09-02-PLAN.md — 任务 1·D-10(收口):剩余模块(main/settings/statusBar/toolbar)迁移 + 跨目录引用改写 + 删尽 `render/ui/` + D-69 桶收口(公共面经包根 `./ui` 承接)+ `pnpm test:ci` 回归
+- [ ] 09-03-PLAN.md — 任务 1·D-11 + D-13:`client` 单例消除(改经 `props.state`)+ 旧→新映射表落地 + 嵌套 `state` 透传 + flags `// TODO`(模块单例 `using`/`mainUIController`/`sceneController` 与 `state`(data-state) 列为阻断项待裁定)
+- [ ] 09-04-PLAN.md — 任务 1·D-12:响应式勇士属性对象(命名待用户批准;`IHeroAttr` 不可用)+ 接入 `main.tsx`
## Progress
@@ -502,4 +505,4 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
| 6. 单元测试 | 18/18 | Obsolete(由 Phase 9 承接) | - |
| 7. 数据端缺陷修复 | 15/16 | Complete(07-16 superseded) | - |
| 8. 测试重构与接口对齐 | 11/11 | Complete | 2026-10-08 |
-| 9. 最小浏览器可运行版本 | 0/TBD | Not started | - |
+| 9. 最小浏览器可运行版本 | 0/4 | Not started | - |
diff --git a/.planning/phases/09-minimum-browser-runnable/09-01-PLAN.md b/.planning/phases/09-minimum-browser-runnable/09-01-PLAN.md
new file mode 100644
index 0000000..7f9d09c
--- /dev/null
+++ b/.planning/phases/09-minimum-browser-runnable/09-01-PLAN.md
@@ -0,0 +1,414 @@
+---
+phase: 09-minimum-browser-runnable
+plan: 1
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ # ---- 迁移目标:叶子 / 基础 UI 模块(D-10)----
+ - packages-user/client-modules/src/ui/save.tsx
+ - packages-user/client-modules/src/ui/statistics.tsx
+ - packages-user/client-modules/src/ui/viewmap.tsx
+ - packages-user/client-modules/src/ui/controller.tsx
+ # ---- 已迁移文件与接线文件修正 ----
+ - packages-user/client-modules/src/ui/background.tsx
+ - packages-user/client-modules/src/ui/load.tsx
+ - packages-user/client-modules/src/ui/title.tsx
+ - packages-user/client-modules/src/ui/index.ts
+ # ---- 产物 ----
+ - .planning/phases/09-minimum-browser-runnable/09-01-SUMMARY.md
+files_deleted:
+ - packages-user/client-modules/src/render/ui/save.tsx
+ - packages-user/client-modules/src/render/ui/statistics.tsx
+ - packages-user/client-modules/src/render/ui/viewmap.tsx
+ - packages-user/client-modules/src/render/ui/controller.tsx
+autonomous: false
+requirements: []
+estimate:
+ tokens: 62000
+ raw_tokens: 62000
+ tasks: 4
+ confidence: low
+must_haves:
+ truths:
+ - "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求)**:Task 0 必须先向用户汇报「要解决的问题(`packages-user/client-modules/src/render/ui/` 与 `src/ui/` 双份并存、`src/ui/` 已部分迁移但 `title.tsx`/`load.tsx` 存在 props 基类型断链、`src/ui/index.ts` 仅导出 `./types`)」「锁定裁定 D-10 / D-11 / D-12 / D-13 / D-14 原文要点」「任务 1 的四个计划(09-01..09-04)与旧接口→新接口映射表」「大致解决方案(先完成 D-10 目录移动与引用改写,再做 D-11 接口对齐,再单独实现 D-12 的 `IHeroAttr`;D-13 仅标 TODO)」「范围与硬边界」「验证方式」,然后必须**停下等待**用户明确回复「可以执行」四个字;收到该回复前不得执行任何任务、不得派发任何执行子代理。无论触发方式(`/gsd-execute-phase`、`/gsd-manager`、`/gsd-autonomous`、子代理/执行器派发)或配置模式(`.planning/config.json` 的 `mode: yolo`、`workflow.auto_advance`)如何,均不得自动放行。"
+ - "**D-10(纯移动 + 引用改写,不改变行为):本计划结束时,叶子 / 基础 UI 模块已落到 `packages-user/client-modules/src/ui/`,旧 `render/ui/` 下的对应源文件已删除(不得留双份)**:`src/ui/save.tsx`、`src/ui/statistics.tsx`、`src/ui/viewmap.tsx`、`src/ui/controller.tsx` 四个文件在场;`packages-user/client-modules/src/render/ui/{save,statistics,viewmap,controller}.tsx` 四个文件不存在。"
+ - "**迁移后相对路径逐条改写正确**:`src/ui/save.tsx` 用 `'../render'`(`useKey`)与 `'../render/utils'`(`getSave` / `SaveData`)与 `'../shared'`;`src/ui/viewmap.tsx` 用 `'../render'` 与 `'../shared'`;`src/ui/controller.tsx` 用 `'../shared'`;`src/ui/statistics.tsx` 无仓内相对导入(纯拷贝)。四个文件内**不得**残留 `'../use'` / `'../utils'` / `'../../shared'` / `'../../core'` 等旧层级说明符。"
+ - "**controller.tsx 与 background.tsx 去重(同一 `MainBackground` / `MainBackgroundUI` 不得重复定义)**:`MainBackground`、`MainBackgroundUI`、`MainBackgroundProps` 仅由 `src/ui/background.tsx` 定义;`src/ui/controller.tsx` 仅保留 `mainUIController` 与 `createMainController`,并从 `./background` 取 `MainBackgroundUI`;`src/ui/background.tsx` 不再从 `'../render'` 引入 `MainBackgroundProps`。"
+ - "**props 基类型断链修正**:`src/ui/load.tsx` 与 `src/ui/title.tsx` 不得再从 `./types` 引入不存在的 `IUIDefaultPropsBase`;二者改从 `./types` 引入 `IUIPropsBase`(`src/ui/types.ts:4` 已导出),且其 props 接口携带 `state`(经 `IUIPropsBase extends IClientBaseExtended`)。"
+ - "**D-69 桶导出硬约束(本计划新增的每一行)**:`src/ui/index.ts` 内每一条 `export * from` 的说明符都以 `./` 开头(只指向自身文件夹内的文件);`createUI` 为本文件内定义的函数(可保留,与旧 `render/ui/index.ts` 同构);本计划不新增任何指向文件夹之外的转发导出。"
+ - "**范围零外溢 + 无运行时要求**:验证一律为**静态**(文件存在 / 内容 / 说明符);**不得**出现任何以「`pnpm check:type` 通过」「`pnpm build` 成功」「TS 诊断数归零」为条件的门禁或验收项(沿用 Phase 4 D-68;Phase 9 CONTEXT 未另行要求运行)。`packages-user/client-base/src/material/**`(D-42 / D-48)、`packages/**`、`src/**`、`@motajs/legacy-ui`(D-14)、`core.*`(D-14)、用户并发改动(D-56)全部零触碰。"
+ artifacts:
+ - path: packages-user/client-modules/src/ui/save.tsx
+ provides: "D-10 迁移后的存读档 UI(叶子模块):相对路径改指 `'../render'` / `'../render/utils'` / `'../shared'`;`adjustGrid` / `IGridLayoutData` 仍自 `@user/client-base`"
+ - path: packages-user/client-modules/src/ui/statistics.tsx
+ provides: "D-10 迁移后的数据统计 UI:无仓内相对导入,纯拷贝(`@user/data-state` 的 `ItemState` 为 legacy,D-14 只报告)"
+ - path: packages-user/client-modules/src/ui/viewmap.tsx
+ provides: "D-10 迁移后的浏览地图 UI:`'./statistics'` 同目录保持,`'../use'`→`'../render'`、`'../../shared'`→`'../shared'`"
+ - path: packages-user/client-modules/src/ui/controller.tsx
+ provides: "去重后的主 UI 控制器:仅 `mainUIController`(模块级 `UIController`,Phase 4 遗留待裁定,本 run 不删不迁)+ `createMainController()`;`MainBackgroundUI` 自 `./background` 取"
+ - path: packages-user/client-modules/src/ui/background.tsx
+ provides: "`MainBackground` / `MainBackgroundUI` / `MainBackgroundProps` 的唯一归属(自旧 `render/ui/controller.tsx` 迁入接口)"
+ - path: packages-user/client-modules/src/ui/load.tsx
+ provides: "props 基类型修正后的加载 UI(`IUIPropsBase`);已使用 `props.state.loader`"
+ - path: packages-user/client-modules/src/ui/title.tsx
+ provides: "props 基类型修正后的标题 UI(`IUIPropsBase`);已使用 `props.state.materials`"
+ - path: packages-user/client-modules/src/ui/index.ts
+ provides: "新 UI barrel(D-69 合规):`createUI` + 逐步补齐的 `./` 同目录 `export *`"
+ key_links:
+ - "`packages-user/client-modules/src/ui/title.tsx:33`(`import { saveLoad } from './save'`)↔ `packages-user/client-modules/src/ui/save.tsx` —— 本计划 Task 1(tracer)完成 `save.tsx` 迁移后该断链闭合"
+ - "`packages-user/client-modules/src/ui/controller.tsx`(`import { MainBackgroundUI } from './background'`)↔ `packages-user/client-modules/src/ui/background.tsx` —— 去重后 `MainBackgroundUI` 的唯一来源"
+ - "`packages-user/client-modules/src/ui/background.tsx`(`MainBackgroundProps`)↔ 旧 `packages-user/client-modules/src/render/ui/controller.tsx:13` —— 接口归属迁移,消费者为零(本文件自身),无外部断链"
+ - "`packages-user/client-modules/src/ui/index.ts`(`export * from './save'` / `'./statistics'` / `'./viewmap'` / `'./controller'` / `'./background'` / `'./func'`)↔ D-69 桶边界 —— 全部同目录说明符"
+ assumptions:
+ - "**FLAGGED ASSUMPTION(D-11 阻断项·不发明):** `using`(模块单例 `render/renderer.ts:43`)、`mainUIController`(模块单例 `render/ui/controller.tsx:11`)、`sceneController`(模块单例 `render/scene.ts:3`)三者虽有 `IClientBase` 上的同名字段(`packages-user/client-base/src/types.ts:45,47,49`),但**模块单例与 `ClientCore` 构造器内新建的实例并非同一对象**(`packages-user/client-modules/src/client.ts:122,126,127`)。本 run **不替换**这些模块级单例读取(属 Phase 4 遗留未决项,须用户裁定),仅登记映射。`09-03` 亦同。"
+ - "**FLAGGED ASSUMPTION(`state` 单例语义):** 旧接口映射表把 `main.tsx:28,249` 的 `state`(`@user/data-state` 单例)映射为 `props.state`。规划日观察 `state`(`packages-user/data-state/src/ins.ts` 的 `CoreState` 实例)与 `client`(`ClientCore` 实例)**并非同一对象**;该替换的语义正确性须用户在 Task 0 确认,执行者不得自行判定。本计划(09-01)不触及该替换(归 `09-03`)。"
+ - "**controller/background 去重的默认方案待用户确认(Task 0 呈报):** 默认方案 = `MainBackground` / `MainBackgroundUI` / `MainBackgroundProps` 归 `background.tsx`,`controller.tsx` 减为 `mainUIController` + `createMainController`。若用户否决,须退出本计划、修订后重新汇报。"
+ - "**`src/ui/` 已部分迁移(用户既有工作,已提交,不得回退):** `src/ui/{load,title,background,func,types,index}.tsx/ts` 均已 tracked。本计划只做补齐性修正,不得重写既有已迁移文件的主体逻辑、不得改其既有注释与 jsDoc(AGENTS.md)。"
+ - "**执行日行号可能漂移:** 本计划引用的 `file:line` 是 2026-10-09 的规划日锚点;执行时一律按符号名 / 说明符文本检索定位,任一与规划日不同即在 SUMMARY 记录差异。"
+ - "**无运行时验证可得:** 渲染端无运行时测试设施;本计划承载体为静态门禁 + 人工复核。`pnpm test:ci` 的回归门禁落在 `09-02` 收口。"
+ prohibitions:
+ - "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理(AGENTS.md 的第一个 block 点约束;不因触发方式或 `yolo` / `auto_advance` 模式而放宽)。"
+ - "本计划**只做 D-10 的目录移动与引用改写**:不得在本计划内做 D-11 的 `client` / 单例消除、不得新建 `IHeroAttr`(D-12)、不得添加 flags 的 TODO(D-13);这些属 `09-03` / `09-04`。"
+ - "不得触碰本文 `files_modified` / `files_deleted` 之外的任何生产文件;尤其不得删除或改动 `packages-user/client-modules/src/render/ui/{main,settings,statusBar,toolbar,title,load,index}.tsx`(归 `09-02`)、不得改动 `render/{index.tsx,action.ts,scene.ts,renderer.ts,use.ts,utils/**}`(跨目录引用改写归 `09-02`)、不得改动 `packages-user/client-modules/src/client.ts` 与 `src/index.ts`(归 `09-02`)。"
+ - "不得改动 `packages-user/client-modules/src/core.ts` 的 `client` 单例本体;不得删除或改名任何模块级单例(`mainUIController` / `sceneController` / `using`)——属 Phase 4 遗留未决项。"
+ - "不得新增或改写任何 `@ts-expect-error`(`dev.md`:须说明原因);不得引入 `import type`(`dev.md:54`);不得使用 `?.`(`dev.md:107`);新增 / 改写的类须先 `interface` 声明再 `implements`(`dev.md:100`)。"
+ - "不得修改任何既有 jsDoc 与注释(AGENTS.md);迁移文件除相对路径说明符行外一律逐字保留;不得改任何文件 / 目录名(除 D-10 既定的目录位移)。"
+ - "不得把任何门禁建立在「全仓 / 范围内 TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上;不得因这类检查失败而阻塞交付。"
+ - "不得修复任何 legacy 报错(D-14 / Phase 4 D-55 / D-65):`core.*`、`@motajs/legacy-ui`(`mainSetting` / `triggerFullscreen` / `mainUi` / `getVitualKeyOnce`)、`@motajs/legacy-common`(`generateBinary`)、`@user/data-state` 的 `ItemState` 一律不管、不修、不登记为待办。"
+ - "不得触碰、暂存、提交用户的并发改动(D-56):提交前必须以 `git status` 逐条核对暂存面,只暂存本计划 `files_modified` / `files_deleted` 的条目。"
+---
+
+
+Phase 9 · 任务 1(UI 接口适配)· 四个计划中的第 1 个 —— **D-10:`packages-user/client-modules/src/render/ui/` → `packages-user/client-modules/src/ui/` 的目录移动(叶子 / 基础模块 + 目标 barrel 骨架)**。
+
+**要解决的问题:**
+
+用户已开始把 `render/ui/` 往 `src/ui/` 迁移(`src/ui/{load,title,background,func,types,index}` 已 tracked),但迁移尚未完成,`render/ui/` 仍留有 11 个旧文件与之双份并存。本计划完成其中**叶子 / 基础**部分的迁移,使 `src/ui/` 的目标侧骨架成形:
+
+1. `src/ui/title.tsx:33` 现 `import { saveLoad } from './save'` 指向尚不存在的 `src/ui/save` —— props 基类型(`:38` 从 `./types` 引入不存在的 `IUIDefaultPropsBase`)亦断链。
+2. `src/ui/load.tsx:23` 同款 props 基类型断链。
+3. `src/ui/background.tsx:3` 从 `'../render'` 取 `MainBackgroundProps`(该接口定义在旧 `render/ui/controller.tsx:13`);且旧 `controller.tsx` 与 `background.tsx` **同时**定义 `MainBackground` / `MainBackgroundUI`,迁移后必然重复导出。
+4. `src/ui/index.ts` 仅 `export * from './types'`,尚未承载新桶。
+
+**锁定裁定(D-10 / D-14):** D-10 = 把 `render/ui/` 移动到 `src/ui/` 并同步改写全部引用,**纯移动 + 引用改写,不改变行为**;D-14 = 本任务不引入 UI 形态 / 视觉改动、不做 `@motajs/legacy-ui` 移植。
+
+**大致解决方案(本计划 = 09-01):** 按依赖顺序(叶子先行)迁移 `save` / `statistics` / `viewmap` / `controller` 四个模块到 `src/ui/`,逐文件改写相对路径;对 `controller.tsx` 与 `background.tsx` 去重(`MainBackground` 一族归 `background.tsx`);修正 `load.tsx` / `title.tsx` 的 props 基类型断链;逐步补齐 `src/ui/index.ts` 的 `./` 同目录 `export *`(D-69 合规)。
+
+**任务切分(Task 0 汇报关卡 + tracer + 2 个铺开任务):** Task 1(tracer)= `save.tsx` 叶子模块端到端迁移(移动 + barrel + 删除旧文件 + 证明 `title.tsx` 的 `./save` 断链闭合);Task 2 = `statistics.tsx` + `viewmap.tsx` 迁移 + barrel + 删旧;Task 3 = `controller.tsx` 去重 + `background.tsx` 接口归属 + `load.tsx` / `title.tsx` props 修正 + barrel 收口 + 删旧。
+
+## Source Audit(多源覆盖审计)
+
+| SOURCE | ID | Feature/Requirement | Plan | Status | Notes |
+|--------|----|---------------------|------|--------|-------|
+| GOAL | — | Phase 9(ROADMAP:22)「在已完成的数据端之上完成渲染端框架,并验证可在浏览器上运行」 | 09-01 | COVERED(增量·D-10 子切片) | 本 run 只规划「任务 1(UI 接口适配)」;D-10 是任务 1 的第一步 |
+| REQ | — | Phase 9 ROADMAP 无 `**Requirements:**` 行,`phase_req_ids` 为 null | — | N/A | 本 run 无需求 ID 覆盖门禁;`requirements: []` 如实标注 |
+| RESEARCH | — | 无 `09-RESEARCH.md`(`has_research: false`) | 09-01 | N/A | 等价输入 = `09-PATTERNS.md`(pattern map,老→新接口映射与 analog) |
+| CONTEXT | D-10 | 将 `render/ui/` 移动到 `src/ui/` 并改写全部引用;纯移动 + 引用改写,不改变行为 | 09-01 | COVERED(叶子 / 基础部分) | Task 1..3 迁移 4 个模块 + 2 个已迁移文件的修正;其余 4 模块归 `09-02` |
+| CONTEXT | D-11 | UI 对齐新 UI 系统(`props` 取 `IClientBase`,不用 `client` 单例);给出旧→新映射表;无对等项先反馈不造接口 | 09-03 | COVERED(登记于本计划 Task 0 汇报) | 本计划 Task 0 须呈报完整映射表(见 `09-03` objective 表) |
+| CONTEXT | D-12 | 单独实现一个 `IHeroAttr` 响应式对象;命名待批准 | 09-04 | COVERED(登记于本计划 Task 0 汇报) | 命名与字段集须用户批准;本计划 Task 0 先行呈报以便一次把关 |
+| CONTEXT | D-13 | flags 无监听系统,仅标 TODO | 09-03 | COVERED(登记于本计划 Task 0 汇报) | 本计划不实现 |
+| CONTEXT | D-14 | 纯适配:不改 UI 形态、不移植 `@motajs/legacy-ui` | 09-01 | COVERED(排除) | 无视觉 / legacy 任务;prohibitions 明列 |
+| CONTEXT | D-05 / D-06 / D-07 | 默认不逐阶段验证;范围由用户界定;UI 形态由用户提供 | 09-01 | COVERED(边界) | 本 run 不引入任何 UI 形态判断;验证为静态 |
+| CONTEXT | Deferred Ideas | Phase 9 其余杂项对齐工作由用户后续增量派发 | — | DEFERRED | 未写入任何任务 |
+| PATTERN | `09-PATTERNS.md` §2/§3/§6/§8 | `render/ui` → `src/ui` 的逐文件相对路径改写规则、`background.tsx` 的 `../render` 断链、`load.tsx`/`title.tsx` props 类型断链、旧 `controller.tsx` 的 `MainBackground*` 归属 | 09-01 | COVERED | Task 1..3 逐条落地 |
+
+## Estimate Basis(估算依据)
+
+`estimate-calibration` 无历史样本(`sample_count: 0`)⇒ `factor: 1`、`tokens == raw_tokens`、`confidence: low`。按「执行者实际触及量」估算(`estimate.tasks: 4` = Task 0 汇报关卡 + 3 个工作任务):Task 0 只读汇报并等待用户确认(约 6k,不写文件);Task 1(tracer)`save.tsx` 迁移 + barrel + 删旧 + 门禁(约 13k);Task 2 `statistics.tsx` + `viewmap.tsx` 迁移 + barrel + 删旧 + 门禁(约 13k);Task 3 `controller.tsx` 去重 + `background.tsx` + `load.tsx`/`title.tsx` props 修正 + barrel 收口 + 删旧 + 门禁(约 18k);固定开销约 12k;合计约 62k。
+
+**API coverage(贡献检测器):** 对 Phase 9 §9 + 本计划正文运行 `api-coverage.cjs` 得 `detected:false`。**`No external API integration: Phase 9 任务 1 是渲染端 UI 目录移动与 `props.state` 接口适配,不涉及任何 SDK / REST / GraphQL / webhook / OAuth 等外部 API。`**
+
+**Assumption-Delta(贡献检测器):** `gsd_run query assumption-delta scan 9 --json` 返回 `detected:false` ⇒ 无 ``。
+
+
+
+@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
+@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/REQUIREMENTS.md
+@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
+@.planning/phases/09-minimum-browser-runnable/09-PATTERNS.md
+@.planning/phases/04-render-adaptation/04-CONTEXT.md
+@AGENTS.md
+@dev.md
+
+
+
+
+
+ Task 0: 预执行汇报关卡 —— 汇报「任务 1(UI 接口适配)」全貌(09-01..09-04)、D-10..D-14 裁定、旧→新映射表、阻断项与 `IHeroAttr` 命名待批准,等待用户回复「可以执行」
+ Phase 9 任务 1 的裁定 D-10..D-14 已写入 `.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md`(以文件内实际存在 `## 任务 1` 与 D-10..D-14 为准);`09-PATTERNS.md` 在场;当日 `packages-user/client-modules/src/render/ui/` 与 `src/ui/` 双份并存(`render/ui/**` 11 文件 + `src/ui/**` 6 文件均 tracked)。
+ (只读汇报,不修改任何文件)
+
+ - AGENTS.md(「任何计划的执行,其第一个 block 点必须是向我汇报计划内容并等待我确认」「只有明确回复『可以执行』四个字之后才允许开始执行」「任何公共、受保护、私有方法、字段等内容的命名必须先向我反馈」「不得擅自修改所有 jsDoc 注释」「除非我明确要求,否则任何修改必须经由 GSD 标准流程」)
+ - .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(`## 任务 1` 与 D-10..D-14 原文;D-01..D-07 的执行方式裁定)
+ - .planning/phases/09-minimum-browser-runnable/09-PATTERNS.md(`## Old → New Interface Mapping` 集中表 + `### 无对等(报告,不造接口)` 节 + `### 单例归属` + `No Analog Found`(含 `IHeroAttr`))
+ - .planning/phases/04-render-adaptation/04-CONTEXT.md(D-23 / D-28 存量单例只报告;D-42 / D-48 `material/` 禁用;D-55 / D-65 legacy 不管;D-68 不以 `check:type` / `build` 为门禁;D-69 桶导出边界)
+ - .planning/phases/09-minimum-browser-runnable/09-01-PLAN.md .. 09-04-PLAN.md(本任务四个计划全文)
+ - dev.md(单向依赖 / 双端分离 / 不转发导出 / 命名规则 / 注释规范 / `@ts-expect-error` 须说明原因 / CRLF)
+ - packages-user/client-base/src/types.ts(`IGameUIPropsBase` `:15-19` / `IClientBase` 字段 `:21-50` / `IClientBaseExtended` `:52-55`)
+ - packages-user/client-modules/src/ui/types.ts(`IUIPropsBase` `:4`)
+
+
+ **停下并向用户汇报,等待明确回复后才继续;汇报本身只读,不写任何文件、不改任何生产代码、不派发子代理。**
+
+ ① **要解决的问题:** 用户已把部分 UI 从 `packages-user/client-modules/src/render/ui/` 迁往 `src/ui/`(`src/ui/{load,title,background,func,types,index}` 已 tracked),但迁移未完成——`render/ui/**` 仍留 11 个旧文件与 `src/ui/**` 双份并存;`src/ui/title.tsx` / `load.tsx` 存在 props 基类型断链;`src/ui/index.ts` 仅导出 `./types`;旧 `render/ui/controller.tsx` 与 `src/ui/background.tsx` 重复定义 `MainBackground` 一族。任务 1 收官 = 完成 D-10 目录移动 + D-11 接口对齐(`client` 单例消除)+ D-12 `IHeroAttr` + D-13 flags TODO。
+
+ ② **锁定裁定(须逐条汇报原文要点):**
+ - **D-10(目录移动):** 将 `render/ui/` 移动到 `src/ui/`(减少包内嵌套),并同步改写所有引用该路径的导入。纯移动 + 引用改写,不改变行为。
+ - **D-11(接口对齐):** 将移动后的 UI 对齐到新 UI 系统(`packages/system/src/ui`):组件通过系统提供的「经 props 获取主对象 `IClientBase`」机制(`controller` / `instance` / `state` 由 `setPropsBaseCreator` 注入)获取主对象,**不得使用 `client` 单例**。把旧的 `client` 相关操作逐一替换为 `IClientBase` 上可用的新接口;**计划必须给出「旧接口 → 新接口」映射表**。凡 `client` 上不存在对应接口的操作,先向用户反馈,**不得自造接口**。
+ - **D-12(勇士属性):** 单独实现一个 `IHeroAttr` 响应式对象,监听勇士属性变化并据此更新(**命名待用户批准**;注意与既有 `IHeroAttribute` 一族及 `@user/data-common` 的 `IHeroAttr` 属性字典类型区分)。
+ - **D-13(flags):** flags 尚无监听系统,本任务不处理 flags 的更新,仅在代码中标注 `TODO`。
+ - **D-14(纯适配):** 不引入 UI 形态 / 视觉改动;不做 `@motajs/legacy-ui` 移植(属后续任务)。
+ - 补充:**D-03**=每个计划执行前先汇报并等待「可以执行」;**D-04**=任何新命名须先获批;**D-05**=默认不逐阶段验证。
+
+ ③ **任务 1 的四个计划(须原样呈报)**:09-01 = D-10 叶子 / 基础模块迁移 + barrel 骨架;09-02 = D-10 剩余模块迁移 + 跨目录引用改写 + 旧目录删除 + D-69 桶收口;09-03 = D-11 `client` / `state` 单例消除 + props 基类型统一 + D-13 flags TODO + 映射表落地;09-04 = D-12 `IHeroAttr` 响应式对象(命名待批准)。wave = 1 / 2 / 3 / 4,逐环依赖。
+
+ ④ **旧接口 → 新接口映射表(D-11 要求·须原样呈报;下表的「替换」项由 `09-03` 落地,「阻断」项须用户裁定,「只报告」项不改):**
+
+ | 旧(单例 / 全局) | 出现位置(迁移前) | 新(经 `props.state: IClientBase`) | 新接口证据 | 处置 |
+ |---|---|---|---|---|
+ | `client.loader` | `render/ui/load.tsx:27,36` | `props.state.loader` | `data-state/src/types.ts:29`(`ICoreState.loader`) | 替换(`src/ui/load.tsx` 已用) |
+ | `client.materials` | `render/ui/title.tsx:67`;`statusBar.tsx:119`;`toolbar.tsx:77,175` | `props.state.materials` | `client-base/src/types.ts:31` | 替换(title 已用;statusBar/toolbar 归 09-03) |
+ | `client.flags` | `render/ui/main.tsx:98`;`settings.tsx:166,221` | `props.state.flags` | `data-base/src/types.ts:27`(`IStateBase.flags`) | 替换 + D-13 TODO(归 09-03) |
+ | `client.hero.attribute.getFinalAttribute()` | `render/ui/main.tsx:102-110` | `props.state.hero.attribute.getFinalAttribute()` | `data-base/src/hero/types.ts:921,928,117` | 替换(归 09-03 / 09-04) |
+ | `client.hero.attribute.addHook()` | `render/ui/main.tsx:141` | `props.state.hero.attribute.addHook()` | `data-base/src/hero/types.ts:104`(`IHookable`) | 替换(归 09-04 `IHeroAttr`) |
+ | `client.mainMapRenderer` | `render/ui/main.tsx:35` | `props.state.mainMapRenderer` | `client-base/src/types.ts:39` | 替换(归 09-03) |
+ | `client.mainMapExtension` | `render/ui/main.tsx:35` | `props.state.mainMapExtension` | `client-base/src/types.ts:43` | 替换(归 09-03) |
+ | `state`(`@user/data-state` 单例) | `render/ui/main.tsx:28,249` | `props.state` | `client-base/src/types.ts:21` | 替换(**语义待确认**,归 09-03) |
+ | `using`(模块单例 `render/renderer.ts:43`) | `render/ui/main.tsx:31,155` | `props.state.using` | `client-base/src/types.ts:45` | **阻断·待裁定**(与 `client.ts:122` 实例非同对象) |
+ | `mainUIController`(模块单例 `render/ui/controller.tsx:11`) | `controller.tsx:11`;`main.tsx:29,281,284`;`statusBar.tsx:7,380`;`toolbar.tsx:23,94,97,115,120,126,195` | `props.state.mainUIController` | `client-base/src/types.ts:49` | **阻断·待裁定**(与 `client.ts:127` 实例非同对象) |
+ | `sceneController`(模块单例 `render/scene.ts:3`) | `render/scene.ts:3`;`render/index.tsx:8` | `props.state.sceneController` | `client-base/src/types.ts:47` | **阻断·待裁定**(非组件模块,本 run 不改) |
+
+ **无对等(不造接口,仅报告)**:`@motajs/legacy-ui`(`mainSetting` / `triggerFullscreen` / `mainUi` / `getVitualKeyOnce`)、`@motajs/legacy-common`(`generateBinary`)、`core.*`、`@user/data-state` 的 `ItemState` —— 均属 D-14 / Phase 4 D-55 / D-65,只报告不改、不登记为待办。
+
+ ⑤ **`IHeroAttr` 命名待批准(D-12 / D-04 / AGENTS.md;须在此一并请裁):** 拟新建的响应式对象文件名 / 接口名 / 类名待定。**`IHeroAttr` 与 `@user/data-common` 既有属性字典类型 `IHeroAttr`(`data-base/src/types.ts:8,21` 泛型实参)及 `IHeroAttribute` 一族重名**,不得沿用。请用户在 Task 0 一并给出(或委托 AI 提出候选后批准)。`09-04` 另设独立命名把关。
+
+ ⑥ **范围与硬边界:** 改动面 = 09-01..09-04 各自 `files_modified` / `files_deleted`。**零触碰** = `packages-user/client-base/src/material/**`(D-42 / D-48)、`packages/**`、`src/**`、`@motajs/legacy-ui` 与 `core.*`(D-14 / D-55 / D-65)、用户并发改动(D-56)、除任务 1 四个计划外的任何文件。**无依赖变更、不执行 `pnpm i`。**
+
+ ⑦ **验证方式(须原样呈报并声明无运行时要求):** 全部为**静态**——文件存在 / 不存在、说明符文本、barrel `./` 前缀、`git status --porcelain` 范围;**不得**使用 `pnpm check:type` / `pnpm build` / TS 诊断数作为门禁。收口计划(09-02)另含 `pnpm test:ci` 回归门禁。
+
+ ⑧ **关卡性质与失败语义(必须原样告知):** 无论触发方式(`/gsd-execute-phase`、`/gsd-manager`、`/gsd-autonomous`、子代理/执行器派发)或配置模式(`mode: yolo`、`workflow.auto_advance`)如何,执行者/子代理都**必须在此停下并等待**用户明确回复「可以执行」四个字;未收到前不得执行任何任务、不得派发执行子代理,也不得以「自动化模式」为由自动放行。用户回复其它任何内容(含修订意见、沉默)一律视为未通过:立即停止、保持只读。
+
+ ⑨ **若用户给出修订意见**(含否决 `background`/`controller` 去重方案、否决某处单例归属、改 `IHeroAttr` 命名、调整字段集、扩大 / 缩小范围):退出本计划、修订对应 `09-XX-PLAN.md` 后重新汇报,不得自行改道执行。
+
+ 是否按「任务 1(UI 接口适配)」的四个计划(09-01 迁移叶子/基础模块 + barrel;09-02 迁移剩余模块 + 引用改写 + 删旧目录 + D-69 收口;09-03 `client`/`state` 单例消除 + props 基类型统一 + D-13 flags TODO;09-04 D-12 `IHeroAttr`(命名待批准);模块单例 `using`/`mainUIController`/`sceneController` 与 `state`(data-state) 语义作为阻断项待裁定、本 run 默认不替换)开始执行
+
+ AGENTS.md 要求任何计划的第一个 block 点必须是向用户汇报计划内容并等待用户确认。`.planning/config.json` 为 `mode: yolo`,故该关卡必须显式 `blocking-human` 并 `autonomous: false`。本次存在会改变交付物形态的判断(`MainBackground*` 归 `background.tsx` 的去重方案;`IHeroAttr` 命名;模块级单例 `using`/`mainUIController`/`sceneController` 与 `state`(data-state) 单例是否替换)以及一条会放宽验收的既有裁定(Phase 4 D-68:不要求可运行、不要求零类型错误)必须让用户明确知晓,避免执行者事后以「类型不干净」为由扩大范围或阻塞交付。
+
+
+
+
+
+
+ 回复「可以执行」四个字(修订意见请按 action ⑨ 处理)
+
+ 用户在对话中明确回复「可以执行」四个字(人工核对用户回复原文;本关卡为人工关卡,无自动命令,失败语义写在 action ⑧ 与 acceptance_criteria)
+
+
+ - 用户回复中明确包含「可以执行」四个字
+ - 汇报内容涵盖:要解决的问题(`render/ui` 与 `src/ui` 双份并存 + props 基类型断链 + `MainBackground*` 重复)、锁定裁定 **D-10..D-14 原文要点**、四个计划划分与 wave、**旧接口 → 新接口映射表**(含「替换 / 阻断·待裁定 / 只报告」三类)、`IHeroAttr` 命名待批准、范围与硬边界、验证方式
+ - 汇报中已原样告知**阻断项**:`using` / `mainUIController` / `sceneController` 模块单例与 `state`(data-state) 单例的替换本 run **默认不做**、须用户裁定
+ - 汇报中已原样告知**无运行时要求**(不设 `check:type` / `build` / TS 诊断数类门禁)与 **D-14 只报告项**(legacy / `core.*` / `ItemState`)
+ - 汇报中已原样告知**关卡性质**:执行者/子代理必须停下等待,不因触发方式或 `yolo` / `auto_advance` 模式自动放行
+
+ 用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理
+
+
+
+ Task 1(tracer):端到端迁移叶子模块 `save.tsx` —— `render/ui/save.tsx` → `src/ui/save.tsx`(相对路径改写)+ 新桶 `./save` 导出 + 删除旧文件;证明 `src/ui/title.tsx` 的 `./save` 断链闭合
+ Task 0 已闭环:用户在对话中明确回复「可以执行」四个字(未收到则不得开始本任务)
+ packages-user/client-modules/src/ui/save.tsx, packages-user/client-modules/src/ui/index.ts, packages-user/client-modules/src/render/ui/save.tsx(删除)
+
+ - packages-user/client-modules/src/render/ui/save.tsx(整文件 608 行;须改写的三处相对说明符:`:17` `import { useKey } from '../use';`、`:31-32` 的 `adjustGrid, IGridLayoutData` 自 `@user/client-base`(不变)与 ``:32` `import { getSave, SaveData } from '../utils';`、`:30` 的 `'../../shared'` 常量导入;其余行逐字保留——含既有 jsDoc / 注释不得改)
+ - packages-user/client-modules/src/ui/title.tsx:33(`import { saveLoad } from './save';`——本任务要闭合的断链)
+ - packages-user/client-modules/src/ui/index.ts(整文件 1 行;本任务新增 `export * from './save';`)
+ - packages-user/client-modules/src/ui/types.ts(`IUIPropsBase`;确认引入面)
+ - dev.md(引入置首、无 `import type`、CRLF、不转发导出)
+
+
+ (0a) **起始基线快照(只读前置):** 执行 `git status --porcelain -- packages packages-user src`,逐行记为「只读起始基线」写入记录与 SUMMARY(规划日为 1 条:` M packages-user/data-base/src/enemy/types.ts`;**本计划全程不得触碰**)。另记 `git rev-parse HEAD` 与当前分支。
+
+ (1) **建立目标文件:** 将 `packages-user/client-modules/src/render/ui/save.tsx` 的内容迁移为 `packages-user/client-modules/src/ui/save.tsx`,只改写三处相对说明符:`'../use'` → `'../render'`(`useKey` 现位于 `src/render/use.ts`,经 `src/render/index.tsx` 的 `export * from './use'` 透出);`'../utils'` → `'../render/utils'`(`getSave` / `SaveData` 现位于 `src/render/utils/`);`'../../shared'` → `'../shared'`(共享常量现位于 `src/shared.ts`)。**其余每一行逐字保留**——包括全部既有 jsDoc、`SaveMode` 枚举、`SaveProps` / `SaveItemProps` / `SaveEmits` 类型、`SaveItem` / `Save` 组件、`selectSave` / `saveSave` / `saveLoad` / `saveWithExist` 函数、`SaveUI` 常量;`@user/client-base` 的 `adjustGrid` / `IGridLayoutData` 导入保持原样。
+
+ (2) **新桶导出:** 在 `packages-user/client-modules/src/ui/index.ts` 追加 `export * from './save';`(保持既有 `export * from './types';` 原样;本文件仍不定义 `createUI`——其迁移归 `09-02`)。
+
+ (3) **删除旧文件:** 删除 `packages-user/client-modules/src/render/ui/save.tsx`。**不得**删除 `render/ui/` 下其它文件(归 `09-02`)。
+
+ (4) **CRLF 与收口:** 新文件须为 CRLF(`dev.md:116`);本任务只允许改 (1)..(3) 的文件,不得动任何其它文件、不得新增计划文件。
+
+ 纯文件位移 + 相对说明符改写,全部在 git 追踪范围内,可用 `git revert` 或重跑本计划整体回退;未引入任何接口行为、契约或新命名。
+
+ node -e "const fs=require('fs');const f='packages-user/client-modules/src/ui/save.tsx';if(!fs.existsSync(f)){console.error('MISSING '+f);process.exit(1)}if(fs.existsSync('packages-user/client-modules/src/render/ui/save.tsx')){console.error('OLD save.tsx REMAINS');process.exit(1)}const t=fs.readFileSync(f,'utf8').replace(/\r\n/g,'\n');const bad=['\'../use\'','\'../utils\'','\'../../shared\'','\'../../core\''].filter(s=>t.indexOf(s)>=0);if(bad.length>0){console.error('STALE SPECIFIER '+bad.join(','));process.exit(1)}const need=['from \'../render\'','from \'../render/utils\'','from \'../shared\'','export const SaveUI','export function selectSave','export async function saveSave','export async function saveLoad','export async function saveWithExist'];const miss=need.filter(s=>t.indexOf(s)<0);if(miss.length>0){console.error('CONTENT MISSING '+miss.join(','));process.exit(1)}console.log('OK save.tsx migrated, specifiers rewritten, old deleted')"
+ non-zero exit, 或 stderr 含 "MISSING" / "OLD save.tsx REMAINS" / "STALE SPECIFIER" / "CONTENT MISSING"(新文件未建、旧文件未删、残留旧层级说明符、或关键导出缺失)
+ node -e "const fs=require('fs');const t=fs.readFileSync('packages-user/client-modules/src/ui/index.ts','utf8');if(t.indexOf('export * from \'./save\';')<0){console.error('BARREL ./save MISSING');process.exit(1)}console.log('OK barrel exports ./save')"
+ non-zero exit, 或 stderr 含 "BARREL ./save MISSING"(新桶未导出 `./save`)
+
+
+ - `src/ui/save.tsx` 在场且 `git mv` 语义成立(旧 `render/ui/save.tsx` 不存在)
+ - 新文件无 `'../use'` / `'../utils'` / `'../../shared'` / `'../../core'` 残留;含 `'../render'` / `'../render/utils'` / `'../shared'` 三处新说明符
+ - `SaveUI` / `selectSave` / `saveSave` / `saveLoad` / `saveWithExist` 五个导出逐字在场
+ - `src/ui/index.ts` 含 `export * from './save';` 且既有 `export * from './types';` 未被破坏
+ - `git status` 显示 `render/ui/save.tsx` 处于删除态、`ui/save.tsx` 为新增;无其它计划外文件被改
+
+ `save.tsx` 端到端迁移完成:`src/ui/title.tsx` 的 `import { saveLoad } from './save'` 指向真实文件;旧文件已删;新桶导出就位
+
+
+
+ Task 2:迁移 `statistics.tsx` + `viewmap.tsx`(依赖顺序:statistics 为叶子,viewmap 依赖 statistics 类型)+ 新桶导出 + 删除旧文件
+ Task 1 已完成(`src/ui/save.tsx` 在场、旧 `render/ui/save.tsx` 已删)
+ packages-user/client-modules/src/ui/statistics.tsx, packages-user/client-modules/src/ui/viewmap.tsx, packages-user/client-modules/src/ui/index.ts, packages-user/client-modules/src/render/ui/statistics.tsx(删除), packages-user/client-modules/src/render/ui/viewmap.tsx(删除)
+
+ - packages-user/client-modules/src/render/ui/statistics.tsx(整文件 302 行;**无仓内相对导入**——纯拷贝;含 `// @ts-expect-error 需要重构` + `import { ItemState } from '@user/data-state'`(`:10-11`,legacy,D-14 只报告,逐字保留);`StatisticsUI` / `openStatistics` / `calculateStatistics` 等导出)
+ - packages-user/client-modules/src/render/ui/viewmap.tsx(整文件 568 行;须改相对说明符:`:28` `import { StatisticsDataOneFloor } from './statistics';`(同目录,不变)、`:30` `import { useKey } from '../use';` → `'../render'`、`:31-42` 的 `'../../shared'` 常量导入 → `'../shared'`;其余逐字保留,含大量注释掉的 `layer-group` 块与 `core.*`(legacy,D-14 只报告))
+ - packages-user/client-modules/src/ui/index.ts(新增两行 `export * from './statistics';` / `export * from './viewmap';`)
+ - dev.md(引入置首、无 `import type`、CRLF、不转发导出)
+
+
+ (1) **`statistics.tsx` 纯拷贝:** 将 `packages-user/client-modules/src/render/ui/statistics.tsx` 逐字复制为 `packages-user/client-modules/src/ui/statistics.tsx`——该文件无仓内相对导入,**不得**改动任何行(含 `// @ts-expect-error 需要重构` 与其下 `import { ItemState } from '@user/data-state';`)。删除旧 `render/ui/statistics.tsx`。
+
+ (2) **`viewmap.tsx` 迁移 + 说明符改写:** 将 `packages-user/client-modules/src/render/ui/viewmap.tsx` 迁移为 `packages-user/client-modules/src/ui/viewmap.tsx`,只改写相对说明符:`'../use'` → `'../render'`;`'../../shared'` → `'../shared'`;`'./statistics'` 保持(同目录)。其余每一行逐字保留。删除旧 `render/ui/viewmap.tsx`。
+
+ (3) **新桶导出:** 在 `packages-user/client-modules/src/ui/index.ts` 追加 `export * from './statistics';` 与 `export * from './viewmap';`(与 Task 1 的 `./save` 并列;保持既有行原样)。
+
+ (4) **CRLF 与收口:** 两个新文件须为 CRLF;本任务只允许改 (1)..(3) 的文件。
+
+ 同上:纯文件位移 + 说明符改写,`git revert` 可整体回退,无接口行为 / 新命名。
+
+ node -e "const fs=require('fs');for(const n of ['statistics','viewmap']){const nf='packages-user/client-modules/src/ui/'+n+'.tsx';const of='packages-user/client-modules/src/render/ui/'+n+'.tsx';if(!fs.existsSync(nf)){console.error('MISSING '+nf);process.exit(1)}if(fs.existsSync(of)){console.error('OLD REMAINS '+of);process.exit(1)}}const v=fs.readFileSync('packages-user/client-modules/src/ui/viewmap.tsx','utf8').replace(/\r\n/g,'\n');const bad=['\'../use\'','\'../../shared\''].filter(s=>v.indexOf(s)>=0);if(bad.length>0){console.error('VIEWMAP STALE '+bad.join(','));process.exit(1)}if(v.indexOf('from \'../render\'')<0||v.indexOf('from \'../shared\'')<0||v.indexOf('from \'./statistics\'')<0){console.error('VIEWMAP NEW SPECIFIER MISSING');process.exit(1)}const s=fs.readFileSync('packages-user/client-modules/src/ui/statistics.tsx','utf8');for(const k of ['export const StatisticsUI','export async function openStatistics','export function calculateStatistics','@ts-expect-error 需要重构']){if(s.indexOf(k)<0){console.error('STATISTICS CONTENT MISSING :: '+k);process.exit(1)}}console.log('OK statistics + viewmap migrated')"
+ non-zero exit, 或 stderr 含 "MISSING" / "OLD REMAINS" / "VIEWMAP STALE" / "VIEWMAP NEW SPECIFIER MISSING" / "STATISTICS CONTENT MISSING"(文件未迁移 / 旧文件未删 / viewmap 说明符未改写 / statistics 内容丢失)
+ node -e "const t=require('fs').readFileSync('packages-user/client-modules/src/ui/index.ts','utf8');for(const k of ['export * from \'./types\';','export * from \'./save\';','export * from \'./statistics\';','export * from \'./viewmap\';']){if(t.indexOf(k)<0){console.error('BARREL MISSING :: '+k);process.exit(1)}}console.log('OK barrel exports types/save/statistics/viewmap')"
+ non-zero exit, 或 stderr 含 "BARREL MISSING"(新桶缺任一同目录导出)
+
+
+ - `src/ui/statistics.tsx` 与 `src/ui/viewmap.tsx` 在场;对应旧文件已删
+ - `viewmap.tsx` 无 `'../use'` / `'../../shared'` 残留,含 `'../render'` / `'../shared'` / `'./statistics'`
+ - `statistics.tsx` 逐字保留(含既有 `@ts-expect-error` 与 `ItemState` 导入)
+ - `src/ui/index.ts` 含 `./types` / `./save` / `./statistics` / `./viewmap` 四条同目录导出
+ - 无计划外文件被改
+
+ `statistics.tsx` 与 `viewmap.tsx` 迁移完成,相对说明符改写正确,新桶导出就位,旧文件已删
+
+
+
+ Task 3:迁移并去重 `controller.tsx`(`MainBackground*` 归 `background.tsx`)+ 修正 `background.tsx` 的 `../render` 断链 + 修正 `load.tsx` / `title.tsx` 的 props 基类型断链 + 补齐 barrel + 删除旧 `controller.tsx`
+ Task 2 已完成(`src/ui/{statistics,viewmap}.tsx` 在场、旧文件已删)
+ packages-user/client-modules/src/ui/controller.tsx, packages-user/client-modules/src/ui/background.tsx, packages-user/client-modules/src/ui/load.tsx, packages-user/client-modules/src/ui/title.tsx, packages-user/client-modules/src/ui/index.ts, packages-user/client-modules/src/render/ui/controller.tsx(删除)
+
+ - packages-user/client-modules/src/render/ui/controller.tsx(整文件 34 行:`:1-9` 导入;`:11` `mainUIController = new UIController('main-ui')`;`:13-14` `MainBackgroundProps`;`:16-28` `mainBackgroundProps` + `MainBackground`;`:30` `MainBackgroundUI`;`:32-34` `createMainController()`)
+ - packages-user/client-modules/src/ui/background.tsx(整文件 21 行:`:1-4` 导入含 `:3` `import { MainBackgroundProps } from '../render';`;`:6-8` `mainBackgroundProps`;`:10-18` `MainBackground`;`:21` `MainBackgroundUI`)
+ - packages-user/client-modules/src/ui/load.tsx:1-32(`:23` 断链导入 `IUIDefaultPropsBase` 自 `./types`;`:25` `ILoadProps extends IUIDefaultPropsBase, DefaultProps`;`:32` 已用 `props.state.loader`)
+ - packages-user/client-modules/src/ui/title.tsx:1-68(`:38` 断链导入 `IUIDefaultPropsBase` 自 `./types`;`:60` `GameTitleProps extends DefaultProps, IUIDefaultPropsBase`;`:67` 已用 `props.state.materials`)
+ - packages-user/client-modules/src/ui/types.ts(`IUIPropsBase extends IUIDefaultPropsBase, IClientBaseExtended`,携带 `state`)
+ - packages-user/client-base/src/types.ts(`IGameUIPropsBase`(携带 `state`)与 `IClientBase` 字段)
+ - dev.md(类须 `interface` 先行再 `implements`;不改既有注释;CRLF)
+
+
+ (1) **`controller.tsx` 去重迁移:** 在 `packages-user/client-modules/src/ui/controller.tsx` 只保留 `mainUIController` 与 `createMainController()` 两个导出:`mainUIController = new UIController('main-ui')`(逐字沿用旧 `:11`);`createMainController()` 体内由 `mainUIController.setBackground(MainBackgroundUI, {})` 构成,其中 `MainBackgroundUI` 改从 `./background` 引入。**不得**在新 `controller.tsx` 中重复声明 `MainBackground` / `MainBackgroundUI` / `MainBackgroundProps` / `mainBackgroundProps`。导入按新层级改写:`'../../shared'` → `'../shared'`(若仍在用;`UIController` 自 `@motajs/system`、`GameUI` 视需要而从 `@motajs/system`)。删除旧 `packages-user/client-modules/src/render/ui/controller.tsx`。
+
+ (2) **`background.tsx` 接口归属 + 断链修正:** 在 `packages-user/client-modules/src/ui/background.tsx` 新增导出接口 `MainBackgroundProps extends DefaultProps, IUIDefaultPropsBase {}`(自旧 `render/ui/controller.tsx:13-14` 迁入,逐字同形);删除 `:3` 的 `import { MainBackgroundProps } from '../render';`,改为:`DefaultProps` 自 `@motajs/render-vue` 引入、`IUIDefaultPropsBase` 自 `@motajs/system` 引入。`MainBackground` / `MainBackgroundUI` / `mainBackgroundProps` 主体逐字保留。
+
+ (3) **`load.tsx` / `title.tsx` props 基类型修正(断链闭合):** 两文件均删除从 `./types` 引入 `IUIDefaultPropsBase` 的那一行(`load.tsx:23` / `title.tsx:38`),把 props 接口的基类型改为 `IUIPropsBase`(自 `./types` 引入):`load.tsx` 的 `ILoadProps` 改为 `extends IUIPropsBase, DefaultProps`;`title.tsx` 的 `GameTitleProps` 改为 `extends DefaultProps, IUIPropsBase`。两文件其余行(含 `props: ['controller','instance','state']`、`props.state.loader` / `props.state.materials` 读法、既有注释与 jsDoc)逐字保留。若 `@motajs/system` 的 `IUIDefaultPropsBase` 导入因此变为未使用,一并移除该未使用导入(仅限此目的,不得顺手改其它导入)。
+
+ (4) **补齐 barrel:** 在 `packages-user/client-modules/src/ui/index.ts` 追加 `export * from './controller';`、`export * from './background';`、`export * from './func';`(与既有 `./types` / `./save` / `./statistics` / `./viewmap` 并列,全部为 `./` 同目录说明符,符合 D-69)。**`createUI` 的迁移与 `./main` / `./settings` / `./statusBar` / `./toolbar` 的桶导出属 `09-02`,本任务不得添加。**
+
+ (5) **CRLF 与收口:** 改动文件须为 CRLF;本任务只允许改 (1)..(4) 列出的文件(含删除旧 `controller.tsx`),不得动其它。
+
+ 主要文件为纯位移;`controller.tsx` 去重与 `background.tsx` 接口归属调整均在包内、导出面不变(`MainBackgroundProps` 仍经桶透出),`git revert` 可整体回退;未引入新命名。
+
+ node -e "const fs=require('fs');const r=(p)=>fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');if(fs.existsSync('packages-user/client-modules/src/render/ui/controller.tsx')){console.error('OLD controller.tsx REMAINS');process.exit(1)}const c=r('packages-user/client-modules/src/ui/controller.tsx');const b=r('packages-user/client-modules/src/ui/background.tsx');const bad=[];if(c.indexOf(\"new UIController('main-ui')\")<0)bad.push('controller mainUIController missing');if(c.indexOf('createMainController')<0)bad.push('createMainController missing');if(c.indexOf('MainBackground')>=0)bad.push('controller still declares MainBackground*');if(!/from '\.\/background'/.test(c))bad.push('controller does not import MainBackgroundUI from ./background');if(b.indexOf(\"from '../render'\")>=0)bad.push('background still imports ../render');if(b.indexOf('export interface MainBackgroundProps')<0)bad.push('background MainBackgroundProps missing');if(/from '@motajs\/render-vue'/.test(b)===false)bad.push('background missing DefaultProps import');if(!/from '@motajs\/system'/.test(b)||b.indexOf('IUIDefaultPropsBase')<0)bad.push('background missing IUIDefaultPropsBase import');if(bad.length>0){console.error('DEDUP FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK controller dedup + background import fixed')"
+ non-zero exit, 或 stderr 含 "OLD controller.tsx REMAINS" / "DEDUP FAILED"(去重未完成:controller 仍声明 `MainBackground*` 或未从 `./background` 取;background 仍从 `../render` 引入或未迁入 `MainBackgroundProps`)
+ node -e "const fs=require('fs');const r=(p)=>fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const l=r('packages-user/client-modules/src/ui/load.tsx');const t=r('packages-user/client-modules/src/ui/title.tsx');const bad=[];for(const [n,f] of [['load',l],['title',t]]){if(f.indexOf('IUIDefaultPropsBase')>=0)bad.push(n+' still references IUIDefaultPropsBase');if(f.indexOf('IUIPropsBase')<0)bad.push(n+' missing IUIPropsBase');if(/from '\.\/types'/.test(f)===false)bad.push(n+' does not import from ./types')}if(l.indexOf('props.state.loader')<0)bad.push('load lost props.state.loader');if(t.indexOf('props.state.materials')<0)bad.push('title lost props.state.materials');if(bad.length>0){console.error('PROPS BASE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK load/title props base = IUIPropsBase')"
+ non-zero exit, 或 stderr 含 "PROPS BASE FAILED"(`load.tsx` / `title.tsx` 仍引用 `IUIDefaultPropsBase`、未改用 `IUIPropsBase`,或丢失 `props.state` 读法)
+ node -e "const t=require('fs').readFileSync('packages-user/client-modules/src/ui/index.ts','utf8');for(const k of ['./types','./save','./statistics','./viewmap','./controller','./background','./func']){if(t.indexOf('export * from \\''+k+'\\';')<0){console.error('BARREL MISSING :: '+k);process.exit(1)}}const bad=(t.match(/export \* from '[^.]/g)||[]);if(bad.length>0){console.error('NON-LOCAL EXPORT :: '+bad.join(','));process.exit(1)}console.log('OK barrel same-dir exports only')"
+ non-zero exit, 或 stderr 含 "BARREL MISSING" / "NON-LOCAL EXPORT"(新桶缺失条目或出现非 `./` 越界导出,违反 D-69)
+
+
+ - `src/ui/controller.tsx` 仅含 `mainUIController` + `createMainController`,从 `./background` 取 `MainBackgroundUI`;旧 `render/ui/controller.tsx` 已删
+ - `src/ui/background.tsx` 定义 `MainBackgroundProps`,不再从 `'../render'` 引入
+ - `src/ui/load.tsx` / `title.tsx` 无 `IUIDefaultPropsBase` 引用、改用 `IUIPropsBase`,保留 `props.state.loader` / `props.state.materials` 读法
+ - `src/ui/index.ts` 含 7 条 `./` 同目录导出且无越界导出
+ - 无计划外文件被改
+
+ `controller.tsx` 去重迁移 + `background.tsx` 断链修正 + `load.tsx` / `title.tsx` props 基类型修正全部完成;barrel 骨架(7 条同目录导出)就位;旧文件已删
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| UI 组件 ↔ 主对象 | 组件经 `props.state` 获得完整 `IClientBase`(本计划已迁移文件的既有机制);`load.tsx` / `title.tsx` 的 props 基类型修正即让 `state` 可类型化访问 |
+| 包公共面 `@user/client-modules` | `src/ui/index.ts` barrel 决定对外导出面;D-69 约束其只导出同目录项 |
+| 命名 / 接口边界 | `IHeroAttr` 等新命名须用户批准;不得自造 `IClientBase` 上不存在的接口 |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-09-01-01 | Elevation of Privilege | 命名 / 接口边界(`IHeroAttr`、`MainBackgroundProps` 归属、去重方案) | high | mitigate | Task 0 汇报列出全部会改变交付物形态的判断与待批准命名并等待「可以执行」;`09-04` 另设独立命名把关;不得自造接口 |
+| T-09-01-02 | Tampering | 范围外文件(`material/**`、legacy、`render/` 其它文件、用户并发改动) | medium | mitigate | `files_modified` / `files_deleted` 白名单 + prohibitions + 每个任务静态门禁 + SUMMARY 基线核对;提交前 `git status` 逐条核对 |
+| T-09-01-03 | Tampering | D-69 桶边界(新增越界转发导出) | medium | mitigate | Task 3 门禁断言 `src/ui/index.ts` 全部为 `./` 同目录导出 |
+| T-09-01-04 | Information Disclosure | `props.state` 使所有 UI 组件可读完整主对象 | low | accept | 系新 UI 系统既定设计(D-11);本 run 不做权限收窄,符合项目「无全局单例、主对象经 props 注入」方向 |
+| T-09-01-05 | Repudiation | 未验证即提交 | medium | mitigate | 每个任务静态门禁 + 人工复核结论写入 SUMMARY;验证未通过不得提交(STATE 决策) |
+| T-09-01-SC | Tampering | npm/pip/cargo installs | low | accept | 本计划无任何依赖安装、不执行 `pnpm i`;无供应链面 |
+
+
+
+- **文件位移门禁**:四个模块 `src/ui/{save,statistics,viewmap,controller}.tsx` 在场,对应旧 `render/ui/` 文件不存在(不得双份)。
+- **说明符门禁**:迁移文件无旧层级说明符(`'../use'` / `'../utils'` / `'../../shared'` / `'../../core'`),改用 `'../render'` / `'../render/utils'` / `'../shared'`。
+- **去重门禁**:`MainBackground` / `MainBackgroundUI` / `MainBackgroundProps` 仅由 `background.tsx` 定义;`controller.tsx` 从 `./background` 取 `MainBackgroundUI`。
+- **props 基类型门禁**:`load.tsx` / `title.tsx` 无 `IUIDefaultPropsBase` 引用、改用 `IUIPropsBase`;保留 `props.state` 读法。
+- **D-69 桶门禁**:`src/ui/index.ts` 全部 `export *` 均为 `./` 同目录。
+- **范围门禁**:`git status --porcelain -- packages packages-user src` 除计划内条目外零漂移;`material/` / `packages/` / `src/` porcelain 为空。
+- **CRLF 门禁** 与 **人工复核**(逐条打开当日源码确认门禁结论,写入 SUMMARY)。
+
+
+
+- 叶子 / 基础 UI 模块已完成 D-10 目录迁移且旧文件删除,无双份。
+- 相对说明符与 props 基类型断链全部闭合;`src/ui/title.tsx` 的 `./save` 与 `load.tsx` / `title.tsx` 的 `props.state` 可类型化访问。
+- `MainBackground*` 去重;`src/ui/index.ts` 建立 7 条 D-69 合规的同目录导出。
+- 零范围外改动;无运行时门禁;人工复核结论记录。
+
+
+## Artifacts this phase produces
+
+**新建 / 迁移的文件(`packages-user/client-modules/src/ui/`):**
+
+| 符号 / 路径 | 类型 | 说明 |
+|---|---|---|
+| `src/ui/save.tsx`(新路径) | 文件 | 由 `render/ui/save.tsx` 迁移(导出 `SaveMode` / `SaveProps` / `SaveItemProps` / `SaveEmits` / `SaveItem` / `Save` / `SaveUI` / `SaveValidation` / `SaveValidationFunction` / `selectSave` / `saveSave` / `saveLoad` / `saveWithExist`) |
+| `src/ui/statistics.tsx`(新路径) | 文件 | 由 `render/ui/statistics.tsx` 迁移(导出 `StatisticsDataOneFloor` / `StatisticsDataPotionGem` / `StatisticsData` / `StatisticsProps` / `Statistics` / `calculateStatisticsOne` / `calculateStatistics` / `openStatistics` / `StatisticsUI`) |
+| `src/ui/viewmap.tsx`(新路径) | 文件 | 由 `render/ui/viewmap.tsx` 迁移(导出 `ViewMapProps` / `ViewMap` / `ViewMapUI` / `openViewMap`) |
+| `src/ui/controller.tsx`(新路径) | 文件 | 去重迁移:仅 `mainUIController` + `createMainController()` |
+| `src/ui/background.tsx`(改) | 文件 | 新增导出接口 `MainBackgroundProps`(自旧 `render/ui/controller.tsx` 迁入);既有 `MainBackground` / `MainBackgroundUI` |
+| `src/ui/load.tsx` / `src/ui/title.tsx`(改) | 文件 | props 基类型改 `IUIPropsBase`(断链修正) |
+| `src/ui/index.ts`(改) | barrel | 新增 `./save` / `./statistics` / `./viewmap` / `./controller` / `./background` / `./func` 同目录导出 |
+
+**删除:** `src/render/ui/{save,statistics,viewmap,controller}.tsx`。
+
+**无新增命名**(本计划不引入任何新公共 / 受保护 / 私有成员名;`MainBackgroundProps` 为既有符号的归属迁移)。
+
+
diff --git a/.planning/phases/09-minimum-browser-runnable/09-02-PLAN.md b/.planning/phases/09-minimum-browser-runnable/09-02-PLAN.md
new file mode 100644
index 0000000..982772a
--- /dev/null
+++ b/.planning/phases/09-minimum-browser-runnable/09-02-PLAN.md
@@ -0,0 +1,351 @@
+---
+phase: 09-minimum-browser-runnable
+plan: 2
+type: execute
+wave: 2
+depends_on:
+ - 09-01
+files_modified:
+ # ---- 迁移剩余 UI 模块(D-10)----
+ - packages-user/client-modules/src/ui/main.tsx
+ - packages-user/client-modules/src/ui/settings.tsx
+ - packages-user/client-modules/src/ui/statusBar.tsx
+ - packages-user/client-modules/src/ui/toolbar.tsx
+ - packages-user/client-modules/src/ui/index.ts
+ # ---- 跨目录引用改写 + D-69 桶收口 ----
+ - packages-user/client-modules/src/render/index.tsx
+ - packages-user/client-modules/src/render/action.ts
+ - packages-user/client-modules/src/client.ts
+ - packages-user/client-modules/src/index.ts
+ # ---- 产物 ----
+ - .planning/phases/09-minimum-browser-runnable/09-02-SUMMARY.md
+files_deleted:
+ - packages-user/client-modules/src/render/ui/main.tsx
+ - packages-user/client-modules/src/render/ui/settings.tsx
+ - packages-user/client-modules/src/render/ui/statusBar.tsx
+ - packages-user/client-modules/src/render/ui/toolbar.tsx
+ - packages-user/client-modules/src/render/ui/title.tsx
+ - packages-user/client-modules/src/render/ui/load.tsx
+ - packages-user/client-modules/src/render/ui/index.ts
+autonomous: false
+requirements: []
+estimate:
+ tokens: 60000
+ raw_tokens: 60000
+ tasks: 3
+ confidence: low
+must_haves:
+ truths:
+ - "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求)**:Task 0 必须先汇报「要解决的问题(`render/ui/` 尚余 7 个旧文件与 `src/ui/` 双份并存;`render/index.tsx` / `render/action.ts` / `client.ts` 仍指向旧路径;`render/index.tsx:39` 的 `export * from './ui'` 迁移后成死链且违反 D-69)」「本计划的落地步骤」「范围与硬边界」「验证方式(含 `pnpm test:ci` 回归门禁)」,然后必须**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行任何任务、不得派发执行子代理,且不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行。"
+ - "**D-10 收口:`render/ui/` 整目录删除,`src/ui/` 成为唯一来源,无双份**:`packages-user/client-modules/src/render/ui/` 目录**不存在**;`src/ui/` 含 `main.tsx` / `settings.tsx` / `statusBar.tsx` / `toolbar.tsx`(本计划迁移)与 09-01 的 `save.tsx` / `statistics.tsx` / `viewmap.tsx` / `controller.tsx`、既有 `load.tsx` / `title.tsx` / `background.tsx` / `func.ts` / `types.ts` / `index.ts`。"
+ - "**迁移文件相对路径逐条改写正确**:`src/ui/main.tsx` 用 `'../shared'` / `'../render'` / `'../core'`;`src/ui/settings.tsx` 用 `'../shared'` / `'../render/utils'` / `'../render'` / `'../core'`;`src/ui/statusBar.tsx` 用 `'../shared'` / `'../core'`;`src/ui/toolbar.tsx` 用 `'../shared'` / `'../render'` / `'../core'`。四文件内**不得**残留 `'../../shared'` / `'../use'` / `'../utils'` / `'../renderer'` / `'../../core'`。"
+ - "**`src/ui/index.ts` 承载新 UI 全部导出 + `createUI`**:含 `createUI()`(体内调用自 `./controller` 的 `createMainController()`)与 `./types` / `./controller` / `./main` / `./save` / `./settings` / `./statistics` / `./statusBar` / `./toolbar` / `./viewmap` / `./background` / `./func` 的 `export *`(全部 `./` 同目录,D-69 合规)。"
+ - "**跨目录引用改写 + D-69 桶收口**:`src/render/index.tsx` 的 `import { createUI }` 改指 `'../ui'`、`import { LoadSceneUI }` 改指 `'../ui/load'`,且**删除** `:39` 的 `export * from './ui'`(迁移后死链 + 违反 D-69);`src/render/action.ts` 的 `from './ui'` 改指 `'../ui'`;`src/client.ts` 的 `LoadSceneUI` 改指 `'./ui/load'`;`src/index.ts` **新增** `export * from './ui';`(同目录,承接被删的 `render/index.tsx` 转发,保持 `@user/client-modules` 公共面不缩水)。"
+ - "**范围零外溢 + 无运行时要求(除 `test:ci` 回归门禁外)**:验证以**静态**为主(文件存在 / 不存在 / 说明符 / barrel `./` 前缀 / porcelain);本计划含一次 `pnpm test:ci` 回归门禁(沿用 Phase 08 已验证命令),但**不得**以「TS 诊断数」「`pnpm check:type`」「`pnpm build`」为门禁。`packages-user/client-base/src/material/**`(D-42 / D-48)、`packages/**`、`src/**`(除本计划 `files_modified` 列出的 `src/index.ts` 之外)、legacy(D-14)、用户并发改动(D-56)零触碰。"
+ artifacts:
+ - path: packages-user/client-modules/src/ui/main.tsx
+ provides: "迁移后的主场景 UI(`main-scene`):`MainSceneUI` / `MainScene`;相对路径 `'../shared'` / `'../render'` / `'../core'`;`client` / `state` 单例的消除归 09-03"
+ - path: packages-user/client-modules/src/ui/settings.tsx
+ provides: "迁移后的设置菜单 UI 群(`MainSettings` / `ReplaySettings` / `GameInfo` / `SyncSave*` 等):相对路径改写完成;`openSettings` / `openReplay` 导出保留"
+ - path: packages-user/client-modules/src/ui/statusBar.tsx
+ provides: "迁移后的状态栏 UI(`LeftStatusBar` / `RightStatusBar`):相对路径改写完成"
+ - path: packages-user/client-modules/src/ui/toolbar.tsx
+ provides: "迁移后的工具栏 UI(`PlayingToolbar` / `ReplayingToolbar` / `NumpadToolbar` / `MixedToolbar`):相对路径改写完成"
+ - path: packages-user/client-modules/src/ui/index.ts
+ provides: "新 UI 统一 barrel(D-69 合规)+ `createUI()`"
+ - path: packages-user/client-modules/src/index.ts
+ provides: "`@user/client-modules` 公共面:新增 `export * from './ui'`,承接被删的 `render/index.tsx` 转发,UI 符号不缩水"
+ key_links:
+ - "`packages-user/client-modules/src/render/index.tsx:6`(`import { createUI } from '../ui'`)与 `:11`(`import { LoadSceneUI } from '../ui/load'`)↔ `packages-user/client-modules/src/ui/{index.ts,load.tsx}`"
+ - "`packages-user/client-modules/src/client.ts:44`(`import { LoadSceneUI } from './ui/load'`)↔ `packages-user/client-modules/src/ui/load.tsx` —— 组合根装载点"
+ - "`packages-user/client-modules/src/index.ts`(新增 `export * from './ui'`)↔ `packages-user/client-modules/src/render/index.tsx`(删除 `export * from './ui'`)—— 公共面从 `render` 桶迁至包根桶(D-69 合规)"
+ - "`packages-user/entry-client/src/create.ts:10`(`import * as ClientModules from '@user/client-modules'`)↔ `packages-user/client-modules/src/index.ts` —— 公共面消费者,`./ui` 承接后 `Mota.require('@user/client-modules')` 可见的 UI 符号不缩水"
+ - "`packages-user/client-modules/src/ui/{title.tsx,load.tsx}`(`./main` / `./save` / `./title` 同目录导入)↔ 本计划迁移的模块 —— 迁移补齐后同目录链闭合"
+ assumptions:
+ - "**`render/index.tsx:39` 的 `export * from './ui'` 必须删除(双重理由):** ① `render/ui/` 迁移后不存在,该说明符成死链;② 即便存在,D-69 禁止 barrel 转发自身文件夹之外的任何内容。删除后 `@user/client-modules` 的 UI 符号改由包根 `src/index.ts` 的 `export * from './ui'` 承接。"
+ - "**公共面不缩水(本计划的判断):** `entry-client/src/create.ts:10` 以 `import * as ClientModules` 注册整个包,`packages/legacy-ui` 经 `Mota.require('@user/client-modules')` 取符号。新增包根 `./ui` 导出后,UI 符号(`MainSceneUI` / `LoadSceneUI` / `GameTitleUI` / `saveSave` / `openSettings` 等)仍可从包根取到 ⇒ 面不缩水。若执行日实测有外部消费者依赖 `@user/client-modules` 经 `render` 桶取 UI 符号而无法经包根取到,执行者须**停下并告知用户**,不得自行改道。"
+ - "**本计划只做 D-10 的移动与引用改写,不做 D-11:** `main.tsx` / `settings.tsx` / `statusBar.tsx` / `toolbar.tsx` 迁移时仍**保留** `client` / `state` 读取(仅把其相对导入改写到新层级);`client` / `state` 单例的消除属 `09-03`。"
+ - "**`pnpm test:ci` 回归门禁的边界:** 用户在并发修改数据端(规划日 ` M packages-user/data-base/src/enemy/types.ts`,D-56)。若 `test:ci` 出现失败且失败可归因于用户既有未提交改动或环境,执行者**如实记录并不阻塞**(不得修改、不得回滚用户改动);仅当失败可归因于本计划改动时才算门禁失败。"
+ - "**执行日行号可能漂移:** 引用的 `file:line` 为 2026-10-09 规划日锚点,执行时按符号 / 说明符文本定位并在 SUMMARY 记录差异。"
+ prohibitions:
+ - "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理。"
+ - "本计划**只做 D-10 的移动 / 引用改写 / 删旧目录 / D-69 收口**:不得做 D-11(`client` / `state` / 模块单例消除)、不得新建 `IHeroAttr`(D-12)、不得添加 flags TODO(D-13)。"
+ - "不得触碰 `files_modified` / `files_deleted` 之外的任何生产文件;尤其不得改动 `packages-user/client-modules/src/ui/{save,statistics,viewmap,controller,background,func,types,load,title}.tsx/ts`(`controller` / `background` / `load` / `title` 等已由 09-01 定稿,除非本计划接线需要——如确需,须先停下告知用户),不得改动 `packages-user/client-modules/src/render/{renderer.ts,scene.ts,use.ts,utils/**}`。"
+ - "不得删除或改名任何模块级单例(`mainUIController` / `sceneController` / `using` / `client`)——属 Phase 4 遗留未决项 / D-11 范围。"
+ - "不得新增任何越界转发导出:不得在任何 barrel 出现指向自身文件夹之外的 `export * from`(不得把 `'../ui'` 写进 `src/render/index.tsx` 的 `export *`)。"
+ - "不得修改任何既有 jsDoc 与注释(AGENTS.md);迁移文件除相对路径说明符行外逐字保留;不得引入 `import type`(dev.md:54);不得使用 `?.`(dev.md:107);CRLF。"
+ - "不得修复任何 legacy 报错(D-14):`core.*` / `@motajs/legacy-ui` / `@motajs/legacy-common` / `ItemState` 一律不管、不修、不登记为待办。"
+ - "不得把门禁建立在「全仓 / 范围内 TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上(沿用 Phase 4 D-68);`pnpm test:ci` 之外不得新增测试或测试文件。"
+ - "不得触碰、暂存、提交用户的并发改动(D-56):提交前 `git status` 逐条核对暂存面,只暂存本计划 `files_modified` / `files_deleted` 的条目。"
+---
+
+
+Phase 9 · 任务 1 · 第 2 个计划 —— **D-10:`render/ui/` → `src/ui/` 目录移动收口(剩余 4 模块迁移 + 跨目录引用改写 + 旧目录删除 + D-69 桶收口)**。
+
+**要解决的问题:**
+
+`09-01` 已迁移叶子 / 基础模块(`save` / `statistics` / `viewmap` / `controller`)。本计划完成剩余 4 个模块(`main` / `settings` / `statusBar` / `toolbar`)、把新桶补全(含 `createUI`),改写三处跨目录消费者(`render/index.tsx` / `render/action.ts` / `client.ts`),删除 `render/ui/` 全目录,并按 D-69 把公共面从 `render` 桶迁至包根桶。
+
+1. `render/ui/` 尚余 7 个旧文件与 `src/ui/` 双份并存:`main.tsx` / `settings.tsx` / `statusBar.tsx` / `toolbar.tsx`(待迁移)+ `title.tsx` / `load.tsx`(已在 `src/ui/` 有新版,旧版待删)+ `index.ts`(旧桶待删)。
+2. `render/index.tsx`(`:6` / `:11` / `:39`)、`render/action.ts`(`:11`)、`client.ts`(`:44`)仍指向旧 `render/ui` 路径。
+3. `render/index.tsx:39` 的 `export * from './ui'` 在迁移后成死链,且违反 D-69(桶不得转发自身文件夹之外的内容)。
+
+**锁定裁定(D-10 / D-14 / D-69):** D-10 = 纯移动 + 引用改写、不改变行为;D-14 = 不做视觉 / legacy 改动;D-69(Phase 4 硬约束)= barrel 只允许 `export * from './<同目录项>'`,消费者不得经 barrel 转发——故 `render/index.tsx` 的 `./ui` 转发删除、改由包根 `src/index.ts` 的 `./ui` 承接。
+
+**大致解决方案(本计划 = 09-02):** Task 1 迁移 4 个模块 + 补全新桶(含 `createUI`)+ 删旧;Task 2 改写跨目录消费者 + 删尽 `render/ui/` 剩余旧文件(`title` / `load` / `index`)+ 加包根 `./ui` 导出 + D-69 门禁 + `pnpm test:ci` 回归。
+
+## Source Audit(多源覆盖审计)
+
+| SOURCE | ID | Feature/Requirement | Plan | Status | Notes |
+|--------|----|---------------------|------|--------|-------|
+| GOAL | — | Phase 9(ROADMAP:22)完成渲染端框架 | 09-02 | COVERED(D-10 收口切片) | 承接 `09-01` 未迁移的 4 模块与全部跨目录引用改写 |
+| REQ | — | Phase 9 无需求 ID | — | N/A | `requirements: []` |
+| RESEARCH | — | 无 `09-RESEARCH.md` | 09-02 | N/A | 输入 = `09-PATTERNS.md` §2/§3/§4/§5/§8 |
+| CONTEXT | D-10 | 移动 `render/ui/` → `src/ui/` 并改写全部引用 | 09-02 | COVERED | Task 1/2 完成收口;`src/render/ui/` 目录删除 |
+| CONTEXT | D-14 | 纯适配,不改形态 / 不移植 legacy | 09-02 | COVERED(排除) | 迁移逐字保留,仅改说明符;legacy 只报告 |
+| CONTEXT | D-11 / D-12 / D-13 | 对齐 / `IHeroAttr` / flags TODO | 09-03 / 09-04 | DEFERRED(本计划不做) | prohibitions 明列 |
+| PATTERN | `09-PATTERNS.md` §8 | `render/index.tsx` 3 处、`client.ts` 2 处、`render/action.ts` 1 处的改写表;D-69 冲突警告(`render/index.tsx` 的 `./ui` 转发应删、改由包根直连) | 09-02 | COVERED | Task 2 逐条落地 |
+| CONTEXT | Phase 4 D-68 / D-69 | 不以 `check:type` / `build` 为门禁;桶边界硬约束 | 09-02 | COVERED | 门禁全静态 + 一次 `test:ci`;Task 2 D-69 门禁 |
+
+## Estimate Basis(估算依据)
+
+`estimate-calibration` 无历史样本 ⇒ `factor: 1`、`tokens == raw_tokens`、`confidence: low`。`estimate.tasks: 3` = Task 0 汇报关卡 + 2 个工作任务。Task 0 只读汇报(约 6k);Task 1 四模块迁移 + 桶补全 + 删旧(约 28k);Task 2 跨目录改写 + 删尽旧目录 + D-69 收口 + `test:ci`(约 20k);固定开销约 6k;合计约 60k。
+
+**API coverage:** 对 Phase 9 §9 + 本计划正文运行 `api-coverage.cjs` 得 `detected:false`。**`No external API integration: 本计划仅做渲染端 UI 目录移动与跨目录引用改写,不涉及任何外部 API。`**
+
+
+
+@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
+@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
+
+
+
+@.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-PATTERNS.md
+@.planning/phases/09-minimum-browser-runnable/09-01-PLAN.md
+@.planning/phases/04-render-adaptation/04-CONTEXT.md
+@AGENTS.md
+@dev.md
+
+
+
+
+
+ Task 0: 预执行汇报关卡 —— 汇报「D-10 目录移动收口:剩余 4 模块迁移 + 跨目录引用改写 + 删尽旧目录 + D-69 桶收口」,等待用户回复「可以执行」
+ `09-01` 已闭环:`src/ui/{save,statistics,viewmap,controller}.tsx` 在场,对应旧文件已删;`src/ui/index.ts` 至少含 `./types` / `./save` / `./statistics` / `./viewmap` / `./controller` / `./background` / `./func`。
+ (只读汇报,不修改任何文件)
+
+ - .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-10 原文)
+ - .planning/phases/09-minimum-browser-runnable/09-PATTERNS.md(§8 的 `render/index.tsx` 3 处改写表 + D-69 冲突警告)
+ - .planning/phases/09-minimum-browser-runnable/09-02-PLAN.md(本计划全文)
+ - .planning/phases/09-minimum-browser-runnable/09-01-PLAN.md(前序计划与已定稿文件)
+ - packages-user/client-modules/src/render/index.tsx(`:6` / `:11` / `:39`)
+ - packages-user/client-modules/src/render/action.ts(`:11`)
+ - packages-user/client-modules/src/client.ts(`:44`)
+ - packages-user/client-modules/src/index.ts(`:1-16`,公共面)
+ - packages-user/entry-client/src/create.ts(`:10,24`,公共面消费者)
+ - AGENTS.md / dev.md(第一个 block 点、命名、注释、CRLF、不转发导出)
+
+
+ **停下并向用户汇报,等待明确回复后才继续;汇报本身只读。**
+
+ ① **要解决的问题:** `render/ui/` 尚余 7 个旧文件与 `src/ui/` 双份并存(`main` / `settings` / `statusBar` / `toolbar` 待迁移;`title` / `load` 旧版待删;`index.ts` 旧桶待删);三处跨目录消费者仍指向旧路径;`render/index.tsx:39` 的 `export * from './ui'` 迁移后成死链且违反 D-69。
+
+ ② **锁定裁定:** **D-10**(移动 + 引用改写、不改变行为);**D-14**(纯适配,不改形态);**D-69**(Phase 4 硬约束:barrel 只允许 `export * from './<同目录项>'`;消费者不得经 barrel 转发)。
+
+ ③ **落地步骤(源 → 目标):**
+
+ | # | 文件 | 现状 | 目标 |
+ |---|---|---|---|
+ | 1 | `src/ui/{main,settings,statusBar,toolbar}.tsx` | 仅存在于 `render/ui/` | 迁至 `src/ui/`,相对说明符改指 `'../shared'` / `'../render'` / `'../render/utils'` / `'../core'`;旧文件删除(**保留 `client` / `state` 读取,消除归 09-03**) |
+ | 2 | `src/ui/index.ts` | 缺 `./main` / `./settings` / `./statusBar` / `./toolbar` 与 `createUI` | 补齐上述 4 条 `export *` + 定义 `createUI()`(调 `createMainController()`) |
+ | 3 | `src/render/index.tsx` | `:6`/`:11` 指 `./ui`;`:39` `export * from './ui'` | `:6`/`:11` 改指 `'../ui'` / `'../ui/load'`;`:39` **删除**(死链 + 违反 D-69) |
+ | 4 | `src/render/action.ts` | `:11` `from './ui'` | 改指 `'../ui'` |
+ | 5 | `src/client.ts` | `:44` `from './render/ui/load'` | 改指 `'./ui/load'` |
+ | 6 | `src/index.ts` | 无 `./ui` | **新增** `export * from './ui';`(承接被删转发,公共面不缩水) |
+ | 7 | `render/ui/{title,load,index}.tsx/ts` | 旧版残留 | 删除;`render/ui/` 目录整体移除 |
+
+ ④ **范围与硬边界:** 改动面 = 上表 + `files_modified` / `files_deleted` 白名单。**零触碰** = `client-base/src/material/**`(D-42 / D-48)、`packages/**`、`src/**`(除 `src/index.ts`)、legacy(D-14 / D-55 / D-65)、模块级单例(`mainUIController` / `sceneController` / `using` / `client`)、用户并发改动(D-56)。**无依赖变更、不执行 `pnpm i`。**
+
+ ⑤ **验证方式(须原样呈报):** 静态——文件存在 / 不存在、说明符文本、barrel `./` 前缀、`git status --porcelain` 范围;另加一次 **`pnpm test:ci` 回归门禁**(沿用 Phase 08 已验证命令)。**不得**使用 `pnpm check:type` / `pnpm build` / TS 诊断数作为门禁。`test:ci` 若失败且可归因于用户既有未提交改动或环境,如实记录、不阻塞、不修改用户改动。
+
+ ⑥ **关卡性质与失败语义:** 无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者/子代理必须停下等待用户明确回复「可以执行」四个字;未收到前不得执行任何任务、不得派发执行子代理。用户回复其它内容视为未通过,立即停止、保持只读。
+
+ ⑦ **若用户给出修订意见**(含否决删除 `render/index.tsx:39`、否决新增包根 `./ui`、要求保留某旧文件):退出本计划、修订后重新汇报。
+
+ 是否按本计划(D-10 收口:迁 `main`/`settings`/`statusBar`/`toolbar` + 补桶含 `createUI` + 改写 `render/index.tsx`/`render/action.ts`/`client.ts` 跨目录引用 + 删 `render/ui/` 全目录含 `title`/`load`/`index` + 删 `render/index.tsx:39` 的 `./ui` 转发 + 包根新增 `./ui` + `pnpm test:ci` 回归;无依赖变更)开始执行
+
+ `.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次有一处会改变 `@user/client-modules` 公共面来源的结构判断(删除 `render/index.tsx` 的 `./ui` 转发、改由包根承接)与一次会触及全仓数据测试的命令(`pnpm test:ci`),须让用户知晓。
+
+
+
+
+
+ 回复「可以执行」四个字(修订意见请按 action ⑦ 处理)
+
+ 用户在对话中明确回复「可以执行」四个字(人工核对用户回复原文;失败语义写在 action ⑥ 与 acceptance_criteria)
+
+
+ - 用户回复中明确包含「可以执行」四个字
+ - 汇报内容涵盖:要解决的问题、D-10 / D-14 / D-69 原文要点、源→目标映射表(7 项)、范围与硬边界、验证方式(含 `pnpm test:ci`)
+ - 汇报中已原样告知**公共面承接方式**(删 `render/index.tsx:39` 的 `./ui`、包根新增 `./ui`)与 `test:ci` 的用户改动归因边界
+ - 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
+
+ 用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理
+
+
+
+ Task 1:迁移 `main.tsx` / `settings.tsx` / `statusBar.tsx` / `toolbar.tsx` + 改写相对说明符 + 补全 `src/ui/index.ts`(含 `createUI`)+ 删除对应旧文件
+ Task 0 已闭环:用户明确回复「可以执行」四个字;`09-01` 的产物在场
+ packages-user/client-modules/src/ui/main.tsx, packages-user/client-modules/src/ui/settings.tsx, packages-user/client-modules/src/ui/statusBar.tsx, packages-user/client-modules/src/ui/toolbar.tsx, packages-user/client-modules/src/ui/index.ts, packages-user/client-modules/src/render/ui/main.tsx(删除), packages-user/client-modules/src/render/ui/settings.tsx(删除), packages-user/client-modules/src/render/ui/statusBar.tsx(删除), packages-user/client-modules/src/render/ui/toolbar.tsx(删除)
+
+ - packages-user/client-modules/src/render/ui/main.tsx(整文件 312 行;须改说明符:`:20` `'../../shared'`→`'../shared'`、`:31` `import { using } from '../renderer'`→`'../render'`、`:32` `import { client } from '../../core'`→`'../core'`;`./statusBar` / `./toolbar` / `./controller` 同目录不变;**保留** `client` / `state` 读取;其余(含大量注释块、`core.*`)逐字保留)
+ - packages-user/client-modules/src/render/ui/settings.tsx(整文件 703 行;须改说明符:`:20` `'../utils'`→`'../render/utils'`、`:26` `'../../shared'`→`'../shared'`、`:27` `'../use'`→`'../render'`、`:28` `'../../core'`→`'../core'`;`./statistics` / `./save` / `./viewmap` 不变)
+ - packages-user/client-modules/src/render/ui/statusBar.tsx(整文件 489 行;须改说明符:`:13` `'../../shared'`→`'../shared'`、`:15` `'../../core'`→`'../core'`;`./toolbar` / `./viewmap` / `./controller` 不变)
+ - packages-user/client-modules/src/render/ui/toolbar.tsx(整文件 403 行;须改说明符:`:16` `'../use'`→`'../render'`、`:24` `'../../shared'`→`'../shared'`、`:28` `'../../core'`→`'../core'`;`./save` / `./controller` / `./settings` / `./viewmap` 不变)
+ - packages-user/client-modules/src/render/ui/index.ts(整文件 14 行:`:1` `import { createMainController } from './controller';`、`:3-5` `export function createUI() { createMainController(); }`、`:7-14` 八条 `export *`——本任务把其含义并入新桶)
+ - packages-user/client-modules/src/ui/index.ts(当前 09-01 后的内容:`createUI` 尚未定义;本任务补齐)
+ - dev.md(引入置首、无 `import type`、CRLF、不转发导出)
+
+
+ (1) **迁移 4 模块:** 将 `render/ui/main.tsx` / `settings.tsx` / `statusBar.tsx` / `toolbar.tsx` 分别迁移为 `src/ui/main.tsx` / `settings.tsx` / `statusBar.tsx` / `toolbar.tsx`,**只改写相对说明符**(按 read_first 逐条列出的映射):`'../../shared'`→`'../shared'`、`'../use'`→`'../render'`、`'../utils'`→`'../render/utils'`、`'../renderer'`→`'../render'`、`'../../core'`→`'../core'`;同目录的 `'./statusBar'` / `'./toolbar'` / `'./controller'` / `'./save'` / `'./settings'` / `'./viewmap'` / `'./statistics'` 保持原样。**逐字保留**每一处 `client` / `state` / `core.*` / legacy 用法与全部注释、jsDoc、`// @ts-expect-error`(D-14:不改、不登记为待办)。
+
+ (2) **补全新桶:** 在 `packages-user/client-modules/src/ui/index.ts` 追加 `export * from './main';` / `'./settings';` / `'./statusBar';` / `'./toolbar';`,并补入 `createUI`:文件顶部 `import { createMainController } from './controller';`,且定义 `export function createUI() { createMainController(); }`(与旧 `render/ui/index.ts:3-5` 逐字同构)。既有 `export * from './types' / './controller' / './save' / './statistics' / './viewmap' / './background' / './func';` 保持原样。
+
+ (3) **删除旧文件:** 删除 `render/ui/{main,settings,statusBar,toolbar}.tsx`。**不得**删除 `render/ui/{title,load,index}`(归 Task 2)。
+
+ (4) **CRLF 与收口:** 新文件须为 CRLF;本任务只允许改 (1)..(3) 的文件。
+
+ 纯文件位移 + 说明符改写,无接口行为 / 新命名;`git revert` 可整体回退。
+
+ node -e "const fs=require('fs');const names=['main','settings','statusBar','toolbar'];const bad=[];for(const n of names){const nf='packages-user/client-modules/src/ui/'+n+'.tsx',of='packages-user/client-modules/src/render/ui/'+n+'.tsx';if(!fs.existsSync(nf))bad.push('MISSING '+nf);if(fs.existsSync(of))bad.push('OLD REMAINS '+of);if(fs.existsSync(nf)){const t=fs.readFileSync(nf,'utf8').replace(/\r\n/g,'\n');for(const s of ['\'../../shared\'','\'../use\'','\'../utils\'','\'../renderer\'','\'../../core\'']){if(t.indexOf(s)>=0)bad.push(n+' STALE '+s)}}}if(bad.length>0){console.error('MOVE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK 4 modules migrated, specifiers rewritten')"
+ non-zero exit, 或 stderr 含 "MISSING" / "OLD REMAINS" / "STALE" / "MOVE FAILED"(文件未迁移 / 旧文件未删 / 残留旧层级说明符)
+ node -e "const t=require('fs').readFileSync('packages-user/client-modules/src/ui/index.ts','utf8');const need=['./types','./controller','./main','./save','./settings','./statistics','./statusBar','./toolbar','./viewmap','./background','./func'];const miss=need.filter(k=>t.indexOf('export * from \''+k+'\';')<0);if(miss.length>0){console.error('BARREL MISSING :: '+miss.join(','));process.exit(1)}if(!/export function createUI\(\)/.test(t)){console.error('createUI MISSING');process.exit(1)}if(t.indexOf('createMainController')<0){console.error('createMainController CALL MISSING');process.exit(1)}console.log('OK barrel complete + createUI')"
+ non-zero exit, 或 stderr 含 "BARREL MISSING" / "createUI MISSING" / "createMainController CALL MISSING"(新桶条目缺失或 `createUI` 未定义)
+
+
+ - `src/ui/{main,settings,statusBar,toolbar}.tsx` 在场;对应旧文件已删
+ - 四文件无 `'../../shared'` / `'../use'` / `'../utils'` / `'../renderer'` / `'../../core'` 残留
+ - `client` / `state` / `core.*` / legacy / 注释逐字保留(未做 D-11 改动)
+ - `src/ui/index.ts` 含 11 条 `./` 同目录导出 + `createUI()`
+ - 无计划外文件被改
+
+ 4 个模块迁移完成、相对说明符改写正确、新桶补全(含 `createUI`)、旧文件已删
+
+
+
+ Task 2:改写跨目录消费者(`render/index.tsx` / `render/action.ts` / `client.ts`)+ 包根新增 `./ui` + 删尽 `render/ui/` 剩余旧文件 + D-69 桶门禁 + `pnpm test:ci` 回归
+ Task 1 已完成(4 模块已迁、新桶含 `createUI`、四旧文件已删)
+ packages-user/client-modules/src/render/index.tsx, packages-user/client-modules/src/render/action.ts, packages-user/client-modules/src/client.ts, packages-user/client-modules/src/index.ts, packages-user/client-modules/src/render/ui/title.tsx(删除), packages-user/client-modules/src/render/ui/load.tsx(删除), packages-user/client-modules/src/render/ui/index.ts(删除)
+
+ - packages-user/client-modules/src/render/index.tsx(整文件 44 行:`:6` `import { createUI } from './ui';`、`:11` `import { LoadSceneUI } from './ui/load';`、`:39` `export * from './ui';`、`:38,40-44` 其余 `export *`)
+ - packages-user/client-modules/src/render/action.ts(整文件 38 行:`:3-11` 的 `from './ui'` 导入块;`:12` `import { ElementLocator } from '@motajs/render';`)
+ - packages-user/client-modules/src/client.ts(整文件 162 行:`:44` `import { LoadSceneUI } from './render/ui/load';`;`:45` `import { createUIPropsBase } from './ui/func';`(保持不变))
+ - packages-user/client-modules/src/index.ts(整文件 16 行:`:9-15` 的 `export *` 面;本任务在 `:11` 附近新增 `export * from './ui';`)
+ - packages-user/entry-client/src/create.ts(`:10,24`,确认公共面消费者不依赖 `render` 桶转发 UI 符号)
+ - dev.md(不转发导出、CRLF、引入置首)
+
+
+ (1) **`src/render/index.tsx` 改写:** `:6` 的 `'./ui'` 改指 `'../ui'`;`:11` 的 `'./ui/load'` 改指 `'../ui/load'`;**删除** `:39` 的 `export * from './ui';`(迁移后死链 + 违反 D-69)。其余行(`:1-10`、`:13-37` 的 `createGameRenderer` / `createRender`、`:38,40-44` 的其余 `export *`)逐字保留。
+
+ (2) **`src/render/action.ts` 改写:** `:3-11` 的 `from './ui'` 改指 `'../ui'`;其余逐字保留。
+
+ (3) **`src/client.ts` 改写:** `:44` 的 `'./render/ui/load'` 改指 `'./ui/load'`;`:45` 的 `'./ui/func'` 保持;其余逐字保留(含 `client.ts` 内的单例 / 构造逻辑——本任务不动)。
+
+ (4) **`src/index.ts` 公共面承接:** 在 `packages-user/client-modules/src/index.ts` 的 `export *` 面新增 `export * from './ui';`(同目录;承接被删的 `render/index.tsx` 转发,使 `@user/client-modules` 仍导出 UI 符号)。其余行逐字保留。
+
+ (5) **删尽旧目录:** 删除 `render/ui/title.tsx` / `render/ui/load.tsx` / `render/ui/index.ts`;删除后 `packages-user/client-modules/src/render/ui/` 目录**不得存在**(若残留空目录一并移除)。
+
+ (6) **D-69 桶门禁与回归:** 断言 `src/ui/index.ts`、`src/render/index.tsx`、`src/index.ts` 内每一条 `export * from` 均为 `./` 前缀;运行 `pnpm test:ci` 作回归(失败归因规则见 Task 0 呈报与 assumptions)。CRLF 收口;本任务只允许改 (1)..(5) 的文件。
+
+ 引用改写 + 目录删除均在 git 追踪范围内,`git revert` 可整体回退;未引入新命名(`./ui` 承接为既有导出面搬迁)。
+
+ node -e "const fs=require('fs');const r=(p)=>fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const bad=[];if(fs.existsSync('packages-user/client-modules/src/render/ui'))bad.push('render/ui DIR STILL PRESENT');for(const n of ['title','load','index']){if(fs.existsSync('packages-user/client-modules/src/render/ui/'+n+'.tsx')||fs.existsSync('packages-user/client-modules/src/render/ui/'+n+'.ts'))bad.push('OLD REMAINS render/ui/'+n)}const ri=r('packages-user/client-modules/src/render/index.tsx');if(ri.indexOf(\"from './ui'\")>=0)bad.push('render/index.tsx stale ./ui import');if(ri.indexOf(\"from '../ui'\")<0)bad.push('render/index.tsx missing ../ui import');if(ri.indexOf(\"export * from './ui';\")>=0)bad.push('render/index.tsx ./ui re-export NOT removed');const ra=r('packages-user/client-modules/src/render/action.ts');if(ra.indexOf(\"from './ui'\")>=0||ra.indexOf(\"from '../ui'\")<0)bad.push('action.ts not rewired');const cl=r('packages-user/client-modules/src/client.ts');if(cl.indexOf(\"'./render/ui/load'\")>=0||cl.indexOf(\"'./ui/load'\")<0)bad.push('client.ts not rewired');const idx=r('packages-user/client-modules/src/index.ts');if(idx.indexOf(\"export * from './ui';\")<0)bad.push('root ./ui export missing');if(bad.length>0){console.error('CLOSURE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK consumers rewired, render/ui deleted, root ./ui added')"
+ non-zero exit, 或 stderr 含 "render/ui DIR STILL PRESENT" / "OLD REMAINS" / "stale" / "not rewired" / "NOT removed" / "missing" / "CLOSURE FAILED"(旧目录未删、消费者未改写、或 `.ui` 转发未删 / 包根 `./ui` 未加)
+ node -e "const fs=require('fs');const check=(p)=>{const t=fs.readFileSync(p,'utf8');const re=/export \* from '([^']+)'/g;let m;const out=[];while((m=re.exec(t))){if(!m[1].startsWith('./'))out.push(m[1])}return out};const bad=[];for(const p of ['packages-user/client-modules/src/ui/index.ts','packages-user/client-modules/src/render/index.tsx','packages-user/client-modules/src/index.ts']){const b=check(p);if(b.length>0)bad.push(p+' :: '+b.join(','))}if(bad.length>0){console.error('D69 VIOLATION :: '+bad.join(' ; '));process.exit(1)}console.log('OK D-69: all barrel exports are ./ same-dir')"
+ non-zero exit, 或 stderr 含 "D69 VIOLATION"(任一 barrel 含非 `./` 前缀的 `export * from`)
+ pnpm test:ci
+ `pnpm test:ci` 报告任一失败测试,且该失败可归因于本计划改动(非用户既有未提交改动或环境问题)
+
+
+ - `src/render/index.tsx` 用 `'../ui'` / `'../ui/load'`,且 `export * from './ui'` 已删除;其余 `export *` 全为 `./` 前缀
+ - `src/render/action.ts` 用 `'../ui'`;`src/client.ts` 用 `'./ui/load'`;`src/index.ts` 含 `export * from './ui';`
+ - `packages-user/client-modules/src/render/ui/` 目录不存在;全部 11 个旧文件均已删
+ - D-69 门禁通过(3 个 barrel 全 `./`)
+ - `pnpm test:ci` 结果如实记录(失败若归因用户改动则不阻塞)
+ - 无计划外文件被改
+
+ D-10 收口完成:`render/ui/` 整目录删除、`src/ui/` 成为唯一来源、三处跨目录消费者改写、公共面经包根 `./ui` 承接、D-69 桶边界成立、`pnpm test:ci` 已运行并记录
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| 包公共面 `@user/client-modules` | barrel 来源从 `render` 桶迁至包根桶;`entry-client` / `legacy-ui` 经 `Mota.require` 消费 |
+| D-69 桶边界 | 三个 barrel(`src/ui`、`src/render`、`src/index`)的 `export *` 说明符 |
+| 范围边界 | 迁移须逐字保留 `client` / `state` / legacy 用法;不得顺手做 D-11 |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-09-02-01 | Tampering | 公共面缩水(删 `render/index.tsx:39` 后 UI 符号不可达) | high | mitigate | 包根新增 `export * from './ui'`;Task 2 门禁断言;assumptions 记录「若外部无法经包根取到须停下告知用户」;`pnpm test:ci` 回归 |
+| T-09-02-02 | Tampering | D-69 越界转发导出 | medium | mitigate | Task 2 D-69 门禁断言三 barrel 全 `./` 前缀 |
+| T-09-02-03 | Tampering | 范围外文件(`material/**` / 模块单例 / legacy / 用户改动) | medium | mitigate | 白名单 + prohibitions + 每任务静态门禁 + `git status` 逐条核对 |
+| T-09-02-04 | Elevation of Privilege | 迁移时误做 D-11(消除单例 / 改 props) | medium | mitigate | 迁移动作限定为「只改说明符」;门禁断言 `client`/`state` 用法仍在;prohibitions 明列 |
+| T-09-02-05 | Repudiation | 未验证即提交 | medium | mitigate | 每任务门禁 + `pnpm test:ci` + SUMMARY 记录;验证未通过不得提交 |
+| T-09-02-SC | Tampering | npm/pip/cargo installs | low | accept | 无依赖安装、不执行 `pnpm i` |
+
+
+
+- **目录门禁**:`packages-user/client-modules/src/render/ui/` 不存在;11 个旧文件全部删除;`src/ui/` 为唯一来源。
+- **说明符门禁**:4 个迁移文件无旧层级说明符,改用 `'../shared'` / `'../render'` / `'../render/utils'` / `'../core'`。
+- **桶门禁**:`src/ui/index.ts` 11 条同目录导出 + `createUI`;`src/render/index.tsx` 不含 `./ui` 转发;`src/index.ts` 含 `./ui`;三 barrel D-69 通过。
+- **消费者门禁**:`render/index.tsx` / `render/action.ts` / `client.ts` 说明符改写到位。
+- **范围门禁**:`git status --porcelain -- packages packages-user src` 除计划内条目外零漂移;`material/` / `packages/` 为空。
+- **回归门禁**:`pnpm test:ci`(归因规则见 assumptions)。
+- **CRLF 门禁** 与 **人工复核**(结论写入 SUMMARY)。
+
+
+
+- `render/ui/` 整目录删除,`src/ui/` 成为唯一 UI 来源,无双份。
+- 三处跨目录消费者改写到位;公共面经包根 `./ui` 承接、不缩水;D-69 桶边界成立。
+- 迁移逐字保留既有行为与注释(未做 D-11)。
+- `pnpm test:ci` 已运行并记录;零范围外改动。
+
+
+## Artifacts this phase produces
+
+**新建 / 迁移的文件:**
+
+| 符号 / 路径 | 类型 | 说明 |
+|---|---|---|
+| `packages-user/client-modules/src/ui/main.tsx`(新路径) | 文件 | 由 `render/ui/main.tsx` 迁移(导出 `MainSceneUI`) |
+| `packages-user/client-modules/src/ui/settings.tsx`(新路径) | 文件 | 由 `render/ui/settings.tsx` 迁移(导出 `MainSettingsProps` / `MainSettings` / `ReplaySettings` / `GameInfo` / `SyncSave` / `SaveSelect` 群 / `MainSettingsUI` / `ReplaySettingsUI` / `GameInfoUI` / `SyncSaveUI` / `SyncSaveSelectUI` / `DownloadSaveSelectUI` / `ClearSaveSelectUI` / `openSettings` / `openReplay`) |
+| `packages-user/client-modules/src/ui/statusBar.tsx`(新路径) | 文件 | 由 `render/ui/statusBar.tsx` 迁移(导出 `ILeftHeroStatus` / `IRightHeroStatus` / `LeftStatusBar` / `RightStatusBar` / `leftStatusBarUI` / `rightStatusBarUI`) |
+| `packages-user/client-modules/src/ui/toolbar.tsx`(新路径) | 文件 | 由 `render/ui/toolbar.tsx` 迁移(导出 `PlayingToolbar` / `ReplayingStatus` / `ReplayingProps` / `ReplayingToolbar` / `NumpadToolbar` / `MixedToolbar`) |
+| `packages-user/client-modules/src/ui/index.ts`(改) | barrel | 补齐 4 条同目录导出 + 定义 `export function createUI()` |
+| `packages-user/client-modules/src/index.ts`(改) | barrel | 新增 `export * from './ui';`(承接被删的 `render/index.tsx` 转发) |
+
+**删除:** `packages-user/client-modules/src/render/ui/`(整目录,余 7 文件)。
+
+**无新增命名**(`createUI` 为既有函数搬迁;`./ui` 承接为既有导出面搬迁)。
+
+
diff --git a/.planning/phases/09-minimum-browser-runnable/09-03-PLAN.md b/.planning/phases/09-minimum-browser-runnable/09-03-PLAN.md
new file mode 100644
index 0000000..21c5a64
--- /dev/null
+++ b/.planning/phases/09-minimum-browser-runnable/09-03-PLAN.md
@@ -0,0 +1,333 @@
+---
+phase: 09-minimum-browser-runnable
+plan: 3
+type: execute
+wave: 3
+depends_on:
+ - 09-02
+files_modified:
+ - packages-user/client-modules/src/ui/main.tsx
+ - packages-user/client-modules/src/ui/settings.tsx
+ - packages-user/client-modules/src/ui/statusBar.tsx
+ - packages-user/client-modules/src/ui/toolbar.tsx
+ - .planning/phases/09-minimum-browser-runnable/09-03-SUMMARY.md
+autonomous: false
+requirements: []
+estimate:
+ tokens: 60000
+ raw_tokens: 60000
+ tasks: 3
+ confidence: low
+must_haves:
+ truths:
+ - "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求)**:Task 0 必须先汇报「要解决的问题(`src/ui/` 内 4 个文件仍从 `src/core` 取 `client` 单例)」「锁定裁定 D-11 / D-13 / D-14 原文要点」「**旧接口 → 新接口映射表**(含「替换 / 阻断·待裁定 / 只报告」三类)」「嵌套组件经上层 UI 透传 `state` 的机制(须用户确认)」「大致解决方案」「范围与硬边界」「验证方式」,然后必须**停下等待**用户明确回复「可以执行」四个字;未收到前不得执行任何任务、不得派发执行子代理,且不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行。"
+ - "**D-11:`src/ui/` 内不得再出现 `client` 单例**:`packages-user/client-modules/src/ui/` 下**任一**文件**不得**含从 `src/core.ts` 引入 `client` 的语句(规划日命中面 = `main.tsx` / `statusBar.tsx` / `toolbar.tsx` / `settings.tsx`);这些读取全部改经 `props.state`(`IClientBase`)。`client` 单例本体(`src/core.ts`)与其它消费者(非 `src/ui/**`)不受影响。"
+ - "**旧接口 → 新接口映射表逐条落地(替换项)**:`client.mainMapRenderer`→`props.state.mainMapRenderer`;`client.mainMapExtension`→`props.state.mainMapExtension`;`client.flags`→`props.state.flags`;`client.hero.attribute.getFinalAttribute()`→`props.state.hero.attribute.getFinalAttribute()`;`client.hero.attribute.addHook()`→`props.state.hero.attribute.addHook()`;`client.materials`→`props.state.materials`。**六项全部有 `IClientBase` 上的实证**(`client-base/src/types.ts:31,39,43`;`data-base/src/types.ts:27`;`data-base/src/hero/types.ts:104,117,921,928`)。"
+ - "**D-11:主对象经 props 机制获取**:顶层 UI(经 `controller.open` 打开)由 `setPropsBaseCreator` 注入 `controller` / `instance` / `state`,其 props 基类型为 `IUIPropsBase`(`src/ui/types.ts`)且 `props` 数组含 `'state'`;非顶层(嵌套)子组件(`LeftStatusBar` / `RightStatusBar` / `MixedToolbar` / `PlayingToolbar` / `ReplayingToolbar` / `NumpadToolbar`)不在容器注入面,改由上层 UI 以 `state` prop 逐级透传;`MainScene`(顶层)须新增 `props` 声明与 props 参数(规划日其为无参 `defineComponent(() => ...)`)。"
+ - "**阻断项一律不替换、仅登记(不发明接口 / 不臆测)**:`state`(`@user/data-state` 单例,`main.tsx:249` 的 `state.maps`)、`using`(模块单例 `render/renderer.ts:43`,`main.tsx:155`)、`mainUIController`(模块单例 `controller.tsx`,`main.tsx:281,284` 与 `statusBar.tsx:380` 与 `toolbar.tsx` 多处)、`sceneController`(模块单例 `render/scene.ts:3`)——本计划**保持原读取不动**,在映射表与 SUMMARY 中登记为「与 `props.state` 同名字段非同实例、是否替换待用户裁定」。`@motajs/legacy-ui` / `@motajs/legacy-common` / `core.*` / `ItemState` 一律只报告(D-14)。"
+ - "**D-13:flags 更新仅标注 TODO**:`main.tsx` 与 `settings.tsx` 中 flags 读取点替换为 `props.state.flags` 后,就近新增 `// TODO:` 注释(`dev.md:78` 格式),说明「flags 更新未接(当前无监听系统)」,**不得**新增任何监听 / 订阅代码。"
+ - "**范围零外溢 + 无运行时门禁**:验证为**静态**(`client` 导入零命中 / props 基类型 / TODO 在场);**不得**以「TS 诊断数」「`pnpm check:type`」「`pnpm build`」为门禁。`client-base/src/material/**`(D-42 / D-48)、`packages/**`、`src/**`、模块单例本体、legacy(D-14)、用户并发改动(D-56)零触碰。"
+ artifacts:
+ - path: packages-user/client-modules/src/ui/main.tsx
+ provides: "对齐后的主场景 UI:`props.state` 读取 `mainMapRenderer` / `mainMapExtension` / `flags` / `hero.attribute`;新增 `MainSceneProps extends IUIPropsBase` + props 声明;flags 读取点带 `// TODO:`(D-13);`state`(data-state) / `using` / `mainUIController` 阻断项保持不动"
+ - path: packages-user/client-modules/src/ui/settings.tsx
+ provides: "对齐后的设置菜单 UI 群:`props.state.flags` 替换 `client.flags`;`MainSettingsProps` 基类型携带 `state` 且 `props` 数组含 `'state'`;`__seed__` 读取点带 `// TODO:`(D-13)"
+ - path: packages-user/client-modules/src/ui/statusBar.tsx
+ provides: "对齐后的状态栏:`props.state.materials` 替换 `client.materials`;`StatusBarProps` 携带 `state`;向上层透传 `state` 给 `MixedToolbar`"
+ - path: packages-user/client-modules/src/ui/toolbar.tsx
+ provides: "对齐后的工具栏:`props.state.materials` 替换 `client.materials`;`ToolbarProps` / `ReplayingProps` 携带 `state` 并向上层透传"
+ key_links:
+ - "`packages-user/client-modules/src/client.ts:128-130`(`createUIPropsBase(this)` → `setPropsBaseCreator`)↔ `packages-user/client-modules/src/ui/func.ts:13-25`(产出 `{instance, controller, state}`)↔ 各顶层 UI 的 `props.state` —— D-11 的注入链"
+ - "`packages-user/system/src/ui/container.tsx:32-43`(渲染时 `data.createPropsBase(v)` 并 `{...pb}` 展开)↔ 顶层 UI 组件 props —— `state` 注入机制证据"
+ - "`packages-user/client-base/src/types.ts`(`IGameUIPropsBase.state: IClientBase` `:15-19`;`IClientBase.materials/mainMapRenderer/mainMapExtension` `:31,39,43`)↔ `src/ui/types.ts`(`IUIPropsBase`)—— props 基类型证据"
+ - "`packages-user/data-base/src/types.ts:27`(`IStateBase.flags`)与 `packages-user/data-base/src/hero/types.ts:104,117,921,928`(`IHeroState.attribute` / `IHookable` / `getFinalAttribute`)↔ `props.state.flags` / `props.state.hero.attribute.*` —— 数据端字段证据"
+ assumptions:
+ - "**`props.state === client`(替换为行为保持的依据):** `ClientCore` 构造器以 `this` 调用 `createUIPropsBase(this)`(`client.ts:128`)并把 creator 注入两个控制器,故顶层 UI 的 `props.state` 即 `ClientCore` 实例本身,与 `src/core.ts:6` 的 `client` 单例为同一对象 ⇒ `client.*` → `props.state.*` 不改变行为。"
+ - "**`state`(data-state) 单例列为阻断项(不替换):** `main.tsx:28,249` 的 `state`(`packages-user/data-state/src/ins.ts` 的 `CoreState` 实例)与 `props.state`(`ClientCore` 实例)**非同一对象**;`IClientBase` 虽 `extends ICoreState` 提供同类字段,但替换会改变实际读取的地图状态实例 ⇒ 属语义变更,须用户裁定。本计划**保留** `main.tsx` 的 `state` 读取。"
+ - "**模块单例(`using` / `mainUIController` / `sceneController`)列为阻断项(不替换):** 三者虽有 `IClientBase` 同名字段,但模块单例与 `ClientCore` 构造器内实例非同对象(`client.ts:122,126,127`),切换会改变行为且属 Phase 4 遗留结构决策(`04-15-RENDER-SINGLETON-AUDIT` `#04-15-F-02/F-03/F-04`)。本计划保留原读取。"
+ - "**嵌套组件的 `state` 透传机制须用户确认:** 顶层 UI 由容器注入;`LeftStatusBar` / `RightStatusBar` / `MixedToolbar` / `PlayingToolbar` / `ReplayingToolbar` / `NumpadToolbar` 为嵌套组件、不在注入面,只能由上层以 prop 透传 `state`。替代方案(如改用 `provide`/`inject`)未被用户指定 ⇒ 默认取「prop 透传」,须 Task 0 确认。"
+ - "**D-12 的 `IHeroAttr` 未在本计划实现:** `main.tsx` 的 `hero.attribute` 读取本计划只做 `client`→`props.state` 替换;抽取为独立响应式对象属 `09-04`。"
+ - "**执行日行号可能漂移:** 引用锚点为 2026-10-09 规划日;执行时按符号文本定位并在 SUMMARY 记录差异。"
+ prohibitions:
+ - "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理。"
+ - "本计划**只做 D-11(`client` 消除 + props 基类型统一 + 嵌套透传)与 D-13(flags TODO)**:不得新建 `IHeroAttr`(D-12,归 09-04)、不得改 UI 形态 / 视觉(D-14)、不得移植 `@motajs/legacy-ui`。"
+ - "**不得替换任何阻断项**:不得删改 `main.tsx` 的 `state`(data-state) 读取、不得改 `using` / `mainUIController` / `sceneController` 的模块单例读取、不得删除或改名这些单例本身。"
+ - "不得触碰 `files_modified` 之外的任何生产文件;尤其不得改动 `src/ui/{load,title,controller,background,func,types,save,statistics,viewmap}.tsx/ts`(`load` / `title` 已对齐且只剩 legacy 只报告项)、不得改动 `packages-user/client-modules/src/elements/**` / `components/**`(属 `client-base`)。"
+ - "不得修改任何既有 jsDoc 与注释(AGENTS.md);除(`client`→`props.state` 替换、props 声明新增、D-13 的 `// TODO:` 新增)外逐字保留;新增注释遵循 `dev.md:72-86`;不得引入 `import type`(dev.md:54);不得使用 `?.`(dev.md:107);CRLF。"
+ - "不得新增 / 改名任何公共、受保护、私有成员、方法、字段(嵌套组件新增 `state` prop 属 D-11 接口对齐,须在 Task 0 呈报;其余命名零变更)。"
+ - "不得新增任何监听 / 订阅实现(D-13 只标 TODO);不得修复任何 legacy 报错(D-14)。不得把门禁建立在「TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上。"
+ - "不得触碰、暂存、提交用户的并发改动(D-56):提交前 `git status` 逐条核对暂存面,只暂存本计划 `files_modified` 的条目。"
+---
+
+
+Phase 9 · 任务 1 · 第 3 个计划 —— **D-11:把迁移后的 UI 对齐到新 UI 系统(消除 `client` 单例,改经 `props.state: IClientBase`)+ D-13:flags 更新仅标注 TODO**。
+
+**要解决的问题:**
+
+`09-02` 完成后 `src/ui/` 已成为唯一 UI 来源,但 `main.tsx` / `settings.tsx` / `statusBar.tsx` / `toolbar.tsx` 仍从 `src/core.ts` 取 `client` 单例读取主对象(`client.materials` / `client.flags` / `client.hero.attribute.*` / `client.mainMapRenderer` / `client.mainMapExtension`)。新 UI 系统要求组件经 `props` 机制(`setPropsBaseCreator` 注入 `controller` / `instance` / `state`)获取 `IClientBase`,不得用 `client` 单例(D-11)。同时 flags 尚无监听系统,本任务仅就近标注 `TODO`(D-13)。
+
+**锁定裁定:** D-11 = 组件经系统提供的 props 机制取主对象(不用 `client` 单例);把旧 `client` 相关操作逐一替换为 `IClientBase` 上可用的新接口;**计划须给出「旧接口 → 新接口」映射表**;`client` 上不存在对应接口的操作先向用户反馈、不得自造。D-13 = flags 不处理更新、仅标 `TODO`。D-14 = 纯适配,不改形态、不移植 legacy。
+
+## Old → New Interface Mapping(D-11 要求,本计划权威表)
+
+> 「替换」项由本计划落地;「阻断」项**本 run 不替换**、仅登记待用户裁定;「只报告」项不改、不登记为待办(D-14)。
+
+| # | 旧(单例 / 全局) | 出现位置(`src/ui/`,迁移后行号以当日实测为准) | 新(经 `props.state: IClientBase`) | 新接口证据 | 处置 |
+|---|---|---|---|---|---|
+| 1 | `client.materials` | `title.tsx:67`(已用 `props.state.materials`);`statusBar.tsx:119`;`toolbar.tsx:77,175` | `props.state.materials` | `client-base/src/types.ts:31` | **替换**(title 已达成;statusBar/toolbar 本计划落地) |
+| 2 | `client.flags` | `main.tsx:98`;`settings.tsx:166,221` | `props.state.flags` | `data-base/src/types.ts:27`(`IStateBase.flags`) | **替换** + D-13 TODO |
+| 3 | `client.hero.attribute.getFinalAttribute()` | `main.tsx:102-110`(9 处) | `props.state.hero.attribute.getFinalAttribute()` | `data-base/src/hero/types.ts:921,928,117` | **替换** |
+| 4 | `client.hero.attribute.addHook()` | `main.tsx:141` | `props.state.hero.attribute.addHook()` | `data-base/src/hero/types.ts:104`(`IHookable`) | **替换**(订阅对象抽取归 09-04) |
+| 5 | `client.mainMapRenderer` | `main.tsx:35` | `props.state.mainMapRenderer` | `client-base/src/types.ts:39` | **替换** |
+| 6 | `client.mainMapExtension` | `main.tsx:35` | `props.state.mainMapExtension` | `client-base/src/types.ts:43` | **替换** |
+| 7 | `client.loader` | `load.tsx:32`(已用 `props.state.loader`) | `props.state.loader` | `data-state/src/types.ts:29` | 已达成(无改动) |
+| 8 | `state`(`@user/data-state` 单例) | `main.tsx:28,249` | `props.state`(`IClientBase extends ICoreState`) | `client-base/src/types.ts:21` | **阻断·待裁定**(非同实例,语义变更) |
+| 9 | `using`(模块单例 `render/renderer.ts:43`) | `main.tsx:31,155` | `props.state.using` | `client-base/src/types.ts:45` | **阻断·待裁定**(非同实例) |
+| 10 | `mainUIController`(模块单例 `ui/controller.tsx`) | `main.tsx:281,284`;`statusBar.tsx:380`;`toolbar.tsx` 多处 | `props.state.mainUIController` | `client-base/src/types.ts:49` | **阻断·待裁定**(非同实例;Phase 4 `#04-15-F-03`) |
+| 11 | `sceneController`(模块单例 `render/scene.ts:3`) | 非 `src/ui/` 内 | `props.state.sceneController` | `client-base/src/types.ts:47` | **阻断·待裁定**(非组件模块,本 run 不改) |
+
+**无对等(只报告,不造接口)**:`@motajs/legacy-ui`(`mainSetting` / `triggerFullscreen` / `mainUi` / `getVitualKeyOnce`)、`@motajs/legacy-common`(`generateBinary`)、`core.*`、`@user/data-state` 的 `ItemState` —— 属 D-14 / Phase 4 D-55 / D-65,只报告不改、不登记为待办。
+
+**大致解决方案(本计划 = 09-03):** Task 1(tracer)= `settings.tsx` 端到端替换(顶层 UI 经容器注入 `state`,`client.flags`→`props.state.flags`,最小证明 props 机制);Task 2 = `main.tsx`(新增 props 声明;`client.*` 全替换;聚合 `state`/`using`/`mainUIController` 阻断项保持;flags TODO)+ `statusBar.tsx` / `toolbar.tsx`(`client.materials`→`props.state.materials` + `state` prop 透传 + props 基类型统一)+ 全局 `src/ui` 零 `client` 导入门禁。
+
+## Source Audit(多源覆盖审计)
+
+| SOURCE | ID | Feature/Requirement | Plan | Status | Notes |
+|--------|----|---------------------|------|--------|-------|
+| GOAL | — | Phase 9(ROADMAP:22)完成渲染端框架 | 09-03 | COVERED(D-11 / D-13 切片) | 接口对齐核心 |
+| REQ | — | Phase 9 无需求 ID | — | N/A | `requirements: []` |
+| RESEARCH | — | 无 `09-RESEARCH.md` | 09-03 | N/A | 输入 = `09-PATTERNS.md` Old→New 表 + Shared Patterns |
+| CONTEXT | D-11 | 组件经 props 机制取 `IClientBase`、不用 `client` 单例;给映射表;无对等项先反馈 | 09-03 | COVERED | 表 + Task 1/2;阻断项登记 |
+| CONTEXT | D-13 | flags 无监听系统、仅标 TODO | 09-03 | COVERED | Task 1/2 添加 `// TODO:` |
+| CONTEXT | D-14 | 纯适配、不改形态、不移植 legacy | 09-03 | COVERED(排除) | 无视觉 / legacy 任务 |
+| CONTEXT | D-07 | UI 目标由用户提供 | 09-03 | COVERED(边界) | 不自行臆造 UI 形态;嵌套透传机制待确认 |
+| PATTERN | `09-PATTERNS.md` §1/§2/§3/§4/§5/§7 + Shared Patterns | props 声明范式、`props.state` 读法、`StatusBarProps`/`ToolbarProps` 须挂主对象基类型、flags TODO 占位 | 09-03 | COVERED | Task 1/2 逐条落地 |
+| LEDGER | `04-UI-ADAPTATION-IMPACT.md` #04-13-E-02 / #04-13-F-03 | `client` / `state` 单例使用点;状态栏更新对象 undetermined | 09-03 | COVERED(阻断登记) | `state` / module singletons 列阻断 |
+
+## Estimate Basis(估算依据)
+
+`estimate-calibration` 无历史样本 ⇒ `tokens == raw_tokens`、`confidence: low`。`estimate.tasks: 3` = Task 0 汇报关卡 + 2 个工作任务。Task 0 只读汇报 + 映射表 + 阻断项 + 透传机制确认(约 8k);Task 1(tracer)`settings.tsx` flags 替换 + props 基类型 + TODO + 门禁(约 12k);Task 2 `main.tsx`(props 声明 + 全替换)+ `statusBar.tsx` / `toolbar.tsx`(materials + 透传 + props 基类型)+ 零 `client` 门禁(约 34k);固定开销约 6k;合计约 60k。
+
+**API coverage:** `detected:false`。**`No external API integration: 本计划是渲染端组件经 props 获取 IClientBase 的内部接口适配,不涉及任何外部 API。`**
+
+
+
+@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
+@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
+
+
+
+@.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-PATTERNS.md
+@.planning/phases/09-minimum-browser-runnable/09-02-PLAN.md
+@.planning/phases/04-render-adaptation/04-CONTEXT.md
+@.planning/phases/04-render-adaptation/04-UI-ADAPTATION-IMPACT.md
+@AGENTS.md
+@dev.md
+
+
+
+
+
+ Task 0: 预执行汇报关卡 —— 汇报「D-11 `client` 单例消除 + 旧→新映射表 + 阻断项 + 嵌套 `state` 透传机制 + D-13 flags TODO」,等待用户回复「可以执行」
+ `09-02` 已闭环:`src/ui/` 为唯一 UI 来源、`render/ui/` 目录不存在;`src/ui/{main,settings,statusBar,toolbar}.tsx` 仍在从 `src/core` 取 `client` 单例。
+ (只读汇报,不修改任何文件)
+
+ - .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-11 / D-13 / D-14 原文;D-07)
+ - .planning/phases/09-minimum-browser-runnable/09-PATTERNS.md(**Old → New Interface Mapping 集中表** + `### 无对等(报告,不造接口)` + Shared Patterns 的「Props 基类型统一」「单例归属」「flags 更新占位」)
+ - .planning/phases/09-minimum-browser-runnable/09-03-PLAN.md(本计划全文与权威映射表)
+ - packages-user/client-base/src/types.ts(`IGameUIPropsBase` / `IClientBase` 字段 / `IClientBaseExtended`)
+ - packages-user/client-modules/src/ui/types.ts(`IUIPropsBase`);packages-user/client-modules/src/ui/func.ts(`createUIPropsBase`);packages-user/client-modules/src/client.ts:128-130(`setPropsBaseCreator` 接线)
+ - packages-user/system/src/ui/container.tsx:32-43(`createPropsBase` → `{...pb}` 注入面)
+ - packages-user/data-base/src/types.ts:27(`IStateBase.flags`);packages-user/data-base/src/hero/types.ts:95-117,921-928(`IHeroAttributeHooks` / `IReadonlyHeroAttribute` / `IHeroState.attribute`)
+ - .planning/phases/04-render-adaptation/04-UI-ADAPTATION-IMPACT.md(#04-13-E-02 / #04-13-F-03 / #04-13-F-04)
+ - AGENTS.md / dev.md(第一个 block 点、命名须先反馈、注释规范、TODO 格式)
+
+
+ **停下并向用户汇报,等待明确回复后才继续;汇报本身只读。**
+
+ ① **要解决的问题:** `src/ui/{main,settings,statusBar,toolbar}.tsx` 仍从 `src/core.ts` 取 `client` 单例读取主对象,违反 D-11 的「组件经 props 机制取 `IClientBase`、不用 `client` 单例」。
+
+ ② **锁定裁定:** **D-11**(props 机制 / 映射表 / 无对等项先反馈不造接口);**D-13**(flags 仅标 TODO);**D-14**(纯适配)。
+
+ ③ **旧 → 新接口映射表(原样呈报 objective 的权威表)**:6 项「替换」(`materials` / `flags` / `hero.attribute.getFinalAttribute` / `hero.attribute.addHook` / `mainMapRenderer` / `mainMapExtension`,均有 `IClientBase` 实证);4 项「阻断·待裁定」(`state`(data-state) / `using` / `mainUIController` / `sceneController`);若干「只报告」(legacy / `core.*` / `ItemState`)。
+
+ ④ **阻断项(须用户裁定,本 run 默认不替换)**:`main.tsx` 的 `state`(data-state) 读取(`state.maps`,与 `props.state` 非同实例)、`using`(模块单例)、`mainUIController`(模块单例)、`sceneController`(模块单例)。默认方案 = 全部保持原读取不动、仅登记;若用户要求替换,须另立裁定并评估行为影响。
+
+ ⑤ **嵌套 `state` 透传机制(须用户确认)**:顶层 UI(`MainScene` / `MainSettings` 等经 `controller.open` 打开者)由 `setPropsBaseCreator` 注入 `controller` / `instance` / `state`;但 `LeftStatusBar` / `RightStatusBar` / `MixedToolbar` / `PlayingToolbar` / `ReplayingToolbar` / `NumpadToolbar` 是**嵌套组件**、不在注入面。默认方案 = 由上层 UI 以 `state` prop 逐级透传,各组件 props 基类型改挂 `IUIPropsBase`。**新增的 `state` prop 属接口对齐、非 UI 形态改动(D-14)**;替代方案(`provide` / `inject`)未被指定,请确认采用透传。
+
+ ⑥ **大致的源 → 目标改动:** `main.tsx` 新增 `MainSceneProps extends IUIPropsBase` + `props: ['controller','instance','state']` + props 参数;`const { mainMapRenderer, mainMapExtension } = props.state;`;`flags`→`props.state.flags`;9 处 `hero.attribute.getFinalAttribute`→`props.state.*`;`hero.attribute.addHook`→`props.state.*`;`state` / `using` / `mainUIController` 保持。`statusBar.tsx` / `toolbar.tsx`:`client.materials`→`props.state.materials` + `StatusBarProps`/`ToolbarProps`/`ReplayingProps` 挂 `IUIPropsBase` + `state` 透传。`settings.tsx`:`client.flags`→`props.state.flags` + `MainSettingsProps` 基类型 + `state` prop + TODO。
+
+ ⑦ **范围与硬边界:** 改动面 = 4 个 `files_modified`。**零触碰** = 阻断项(不得替换)、`src/ui` 其余文件、模块单例本体、`client-base/src/material/**`(D-42 / D-48)、`packages/**`、`src/**`、legacy(D-14)、用户并发改动(D-56)。**无依赖变更、不执行 `pnpm i`。**
+
+ ⑧ **验证方式:** 静态——`src/ui/**` 内 `client`(`src/core`)导入零命中、props 基类型为 `IUIPropsBase`、`state` prop 在场、flags 读取点 `// TODO:` 在场、阻断项读取保持。**不得**使用 `pnpm check:type` / `pnpm build` / TS 诊断数作门禁。
+
+ ⑨ **关卡性质与失败语义:** 无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者/子代理必须停下等待用户明确回复「可以执行」四个字;未收到前不得执行任何任务、不得派发执行子代理。回复其它内容视为未通过,立即停止、保持只读。
+
+ ⑩ **若用户给出修订意见**(含替换 / 不替换某阻断项、否决透传机制、调整 flags TODO 位置):退出本计划、修订后重新汇报。
+
+ 是否按本计划(D-11:`src/ui/{main,settings,statusBar,toolbar}.tsx` 的 6 项 `client.*` → `props.state.*` 替换 + props 基类型统一 `IUIPropsBase` + 嵌套 `state` 透传;4 项阻断项不替换仅登记;D-13:flags 读取点标 `// TODO:`;无依赖变更、无运行时门禁)开始执行
+
+ `.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次有多处会改变契约 / 行为的判断(嵌套组件新增 `state` prop;`state`(data-state) 与三个模块单例是否替换),以及一条会放宽验收的既有裁定(Phase 4 D-68)必须让用户知晓,避免执行者事后以「类型不干净」为由扩大范围。
+
+
+
+
+
+
+ 回复「可以执行」四个字(修订意见请按 action ⑩ 处理)
+
+ 用户在对话中明确回复「可以执行」四个字(人工核对用户回复原文;失败语义写在 action ⑨ 与 acceptance_criteria)
+
+
+ - 用户回复中明确包含「可以执行」四个字
+ - 汇报内容涵盖:要解决的问题、D-11 / D-13 / D-14 原文要点、**旧→新映射表**(替换 / 阻断·待裁定 / 只报告三类)、阻断项默认方案、嵌套 `state` 透传机制、范围与硬边界、验证方式
+ - 汇报中已原样告知**无运行时要求**(不设 `check:type` / `build` / TS 诊断数类门禁)
+ - 汇报中已原样告知**关卡性质**(必须停下等待,不因触发方式或 `yolo` / `auto_advance` 自动放行)
+
+ 用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理
+
+
+
+ Task 1(tracer):端到端一条线走通 —— `settings.tsx` 顶层 UI 经 props 机制取主对象:`client.flags` → `props.state.flags`(+ props 基类型 + D-13 flags TODO)
+ Task 0 已闭环:用户明确回复「可以执行」四个字;用户已确认嵌套透传机制(或未要求改用替代方案)
+ packages-user/client-modules/src/ui/settings.tsx
+
+ - packages-user/client-modules/src/ui/settings.tsx(整文件;`:28` `import { client } from '../core';`(09-02 迁移后层级);`:30-37` `MainSettingsProps extends Partial, IUIDefaultPropsBase` 与 `mainSettingsProps` 的 `props: ['loc','controller','instance']`;`:166-167` 与 `:221-222` 的 `client.flags` + `getFieldValue('__seed__')`;`:112` 的 legacy `core.restart()`(只报告))
+ - packages-user/client-modules/src/ui/types.ts(`IUIPropsBase`)
+ - packages-user/client-base/src/types.ts:15-19,21-50(`IGameUIPropsBase` / `IClientBase`);packages-user/data-base/src/types.ts:27(`IStateBase.flags`)
+ - packages-user/system/src/ui/container.tsx:32-43(注入面证据)
+ - dev.md(注释规范 / TODO 格式 / 不改既有注释 / CRLF)
+
+
+ (1) **props 基类型与声明:** 将 `MainSettingsProps` 的基类型改为携带 `state` 者 —— 用 `IUIPropsBase`(自 `./types` 引入)替换原 `IUIDefaultPropsBase`(自 `@motajs/system`);`mainSettingsProps` 的 `props` 数组改为 `['loc','controller','instance','state']`。**其余(`Partial`、所有组件 `MainSettings` / `ReplaySettings` / `GameInfo` / `SyncSave` / `SyncSaveSelect` / `DownloadSaveSelect` / `ClearSaveSelect` 与 `openSettings` / `openReplay`)逐字保留**。
+
+ (2) **单例替换:** 删除 `:28` 的 `import { client } from '../core';`;把两处 `const flags = client.flags;` 改为 `const flags = props.state.flags;`(`ReplaySettings` 与 `DownloadSaveSelect` 各自的 `choose` 内;以当日实测为准)。
+
+ (3) **D-13 flags TODO:** 在两处 `getFieldValue('__seed__')` 读取点就近各新增一行单行注释 `// TODO:`(`dev.md:78` 格式),说明「flags 更新未接(当前无监听系统)」。**不得**新增任何监听 / 订阅代码。**不得**修改任何既有注释。
+
+ (4) **CRLF 与收口:** 文件须为 CRLF;本任务只允许改本文件。
+
+ 组件内 props 读取与基类型调整;`settings.tsx` 为包内实现层文件,改动可用 `git revert` 回退;未改公共 `IClientBase` 契约。
+
+ node -e "const fs=require('fs');const t=fs.readFileSync('packages-user/client-modules/src/ui/settings.tsx','utf8').replace(/\r\n/g,'\n');const bad=[];if(/from '\.\.\/core'/.test(t))bad.push('core client import remains');if(/\bclient\./.test(t))bad.push('client. usage remains');if(t.indexOf('props.state.flags')<0)bad.push('props.state.flags missing');if(t.indexOf('IUIPropsBase')<0||t.indexOf('IUIDefaultPropsBase')>=0)bad.push('props base not IUIPropsBase');if(!/props:\s*\[[^\]]*'state'/.test(t))bad.push('state prop missing');if((t.match(/\/\/ TODO:/g)||[]).length<2)bad.push('D-13 TODO missing (need >=2)');if(t.indexOf('getFieldValue(\\'__seed__\\')')<0&&t.indexOf('__seed__')<0)bad.push('seed read missing');if(bad.length>0){console.error('SETTINGS ALIGN FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK settings.tsx client eliminated, props.state.flags + TODO')"
+ non-zero exit, 或 stderr 含 "SETTINGS ALIGN FAILED"(仍含 `client` 导入 / 用法、未用 `props.state.flags`、props 基类型未改、缺 `state` prop、缺 `// TODO:`、或 `__seed__` 读取丢失)
+
+
+ - `settings.tsx` 无 `client`(`src/core`)导入与 `client.` 用法
+ - 两处 flags 读取改经 `props.state.flags`
+ - props 基类型为 `IUIPropsBase` 且 `props` 数组含 `'state'`
+ - 两处 `__seed__` 读取点各带一行 `// TODO:`(≥2 处)
+ - 其余导出与既有注释逐字保留
+
+ `settings.tsx` 端到端对齐完成:顶层 UI 经 props 机制取得 `IClientBase`,`client.flags` 消除,flags 更新以 TODO 占位
+
+
+
+ Task 2:`main.tsx`(新增 props 声明 + 6 类 `client.*` 替换 + 阻断项保持 + flags TODO)+ `statusBar.tsx` / `toolbar.tsx`(`materials` 替换 + `state` 透传 + props 基类型统一)+ 全局 `src/ui` 零 `client` 门禁
+ Task 1 已完成(`settings.tsx` 已对齐,证明 props 机制可行)
+ packages-user/client-modules/src/ui/main.tsx, packages-user/client-modules/src/ui/statusBar.tsx, packages-user/client-modules/src/ui/toolbar.tsx
+
+ - packages-user/client-modules/src/ui/main.tsx(整文件;`:8` 的 `defineComponent(() => {...})` 无 props;`:28` `import { state } from '@user/data-state';`(**阻断项,保留**);`:29` `import { mainUIController } from './controller';`(**阻断项,保留**);`:31` `import { using } from '../render';`(**阻断项,保留**);`:32` `import { client } from '../core';`(删除);`:35` `const { mainMapRenderer, mainMapExtension } = client;`;`:66-93` 的 `reactive` 状态;`:96-133` `updateStatus`(`:98` `client.flags`、`:102-110` 9 处 `client.hero.attribute.getFinalAttribute`、`:119-121` poison/weak/curse、`:132` hard);`:141` `client.hero.attribute.addHook`;`:155` `using.onExcitedFunc`;`:249` `layerState={state.maps}`;`:281,284` `mainUIController`)
+ - packages-user/client-modules/src/ui/statusBar.tsx(整文件;`:15` `client` 导入;`:96-104` `StatusBarProps extends DefaultProps` + `statusBarProps` `props: ['loc','status','hidden']`;`:119` `client.materials`;`:380` `openViewMap(mainUIController, ...)`(**阻断项,保留**);`:450-453` ``)
+ - packages-user/client-modules/src/ui/toolbar.tsx(整文件;`:28` `client` 导入;`:30-45` `ToolbarProps extends DefaultProps` + `toolbarProps`;`:77,175` `client.materials`;`:163-170` `ReplayingProps extends ToolbarProps`;`:94,97,115,120,126,195` `mainUIController`(**阻断项,保留**);`:397-401` 渲染子工具栏)
+ - packages-user/client-modules/src/ui/types.ts(`IUIPropsBase`);packages-user/client-base/src/types.ts:31-49
+ - dev.md(props 声明范式 / 不改既有注释 / TODO / CRLF / 无 `?.`)
+
+
+ (1) **`main.tsx`:** 新增接口 `MainSceneProps extends IUIPropsBase {}`(`IUIPropsBase` 自 `./types` 引入)与 `const mainSceneProps = { props: ['controller','instance','state'] } satisfies SetupComponentOptions;`;把 `defineComponent(() => {...})` 改为 `defineComponent(props => {...}, mainSceneProps)`。删除 `:32` 的 `import { client } from '../core';`。把 `:35` 的 `const { mainMapRenderer, mainMapExtension } = client;` 改为 `= props.state;`。`updateStatus` 内:`:98` 的 `const flags = client.flags;` → `const flags = props.state.flags;`;`:102-110` 的 9 处 `client.hero.attribute.getFinalAttribute(...)` → `props.state.hero.attribute.getFinalAttribute(...)`;`:141` 的 `const attributeHook = client.hero.attribute.addHook({...})` → `props.state.hero.attribute.addHook({...})`。**保留不动**:`:28` 的 `state`(data-state) 读取(含 `:249` 的 `layerState={state.maps}`)、`:31`/`:155` 的 `using`、`:29`/`:281`/`:284` 的 `mainUIController`。**D-13:** 在 `:98-99`(hideStatusBar)、`:119-121`(poison/weak/curse)、`:132`(hard)三处 flags 读取就近各新增 `// TODO:`(说明 flags 更新未接),不新增监听。其余(`reactive` 状态、`onUnmounted`、`renderMapMisc`、事件回调、渲染树、`MainSceneUI`)逐字保留。
+
+ (2) **`statusBar.tsx`:** 把 `StatusBarProps` 基类型改为 `IUIPropsBase`(保留 `` 泛型与 `loc`/`status`/`hidden` 成员),`statusBarProps` 的 `props` 数组改为 `['loc','status','hidden','state']`;删除 `:15` 的 `client` 导入;`:119` 的 `const materials = client.materials;` → `const materials = p.state.materials;`。`:450-453` 渲染 `` 时新增 `state={p.state}` 透传。**保留不动**:`:380` 的 `openViewMap(mainUIController, ...)`(阻断项)。其余逐字保留。
+
+ (3) **`toolbar.tsx`:** 把 `ToolbarProps` 基类型改为 `IUIPropsBase`(保留 `loc`),`toolbarProps` 的 `props` 改为 `['loc','state']`;`ReplayingProps extends ToolbarProps` 自动携带 `state`,`replayingProps` 的 `props` 改为 `['status','loc','state']`;删除 `:28` 的 `client` 导入;`:77` / `:175` 的 `const materials = client.materials;` → `props.state.materials`。`:397-401` 渲染 `NumpadToolbar` / `ReplayingToolbar` / `PlayingToolbar` 时新增 `state={props.state}` 透传。**保留不动**:所有 `mainUIController` 用法(阻断项)、legacy 导入(`mutate-animate` / `@motajs/legacy-common` / `@motajs/legacy-ui` / `@motajs/client-base` 的 `KeyCode`)与 `core.*`(D-14 只报告)。其余逐字保留。
+
+ (4) **全局门禁与 CRLF:** 确认 `packages-user/client-modules/src/ui/**` 内不再有从 `src/core` 引入 `client` 的语句;本任务只允许改三个文件。
+
+ 包内实现层组件的 props / 读取调整;未改公共契约;`git revert` 可整体回退。阻断项保持不动,语义变更风险为零。
+
+ node -e "const fs=require('fs');const path=require('path');const dir='packages-user/client-modules/src/ui';const files=fs.readdirSync(dir).filter(f=>/\.(ts|tsx)$/.test(f));const bad=[];for(const f of files){const t=fs.readFileSync(path.join(dir,f),'utf8');if(/from '\.\.\/(?:\.\.\/)?core'/.test(t))bad.push(f+' imports core');if(/\bclient\.[a-zA-Z]/.test(t))bad.push(f+' uses client.*')}if(bad.length>0){console.error('CLIENT REMAINS :: '+bad.join(' ; '));process.exit(1)}console.log('OK no client singleton in src/ui')"
+ non-zero exit, 或 stderr 含 "CLIENT REMAINS"(`src/ui/**` 任一文件仍从 `src/core` 引入 `client` 或使用 `client.*`)
+ node -e "const fs=require('fs');const r=(p)=>fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const m=r('packages-user/client-modules/src/ui/main.tsx');const s=r('packages-user/client-modules/src/ui/statusBar.tsx');const t=r('packages-user/client-modules/src/ui/toolbar.tsx');const bad=[];if(m.indexOf('props.state.mainMapRenderer')<0||m.indexOf('props.state.mainMapExtension')<0)bad.push('main missing props.state map renderers');if(m.indexOf('props.state.flags')<0)bad.push('main missing props.state.flags');if((m.match(/props\.state\.hero\.attribute\.getFinalAttribute/g)||[]).length<9)bad.push('main getFinalAttribute count < 9');if(m.indexOf('props.state.hero.attribute.addHook')<0)bad.push('main addHook not rewired');if(!/interface MainSceneProps[^{]*extends[^{]*IUIPropsBase/.test(m))bad.push('main MainSceneProps base missing');if((m.match(/\/\/ TODO:/g)||[]).length<3)bad.push('main D-13 TODO < 3');if(m.indexOf(\"from '../core'\")>=0)bad.push('main core import remains');if(s.indexOf('props')>=0&&s.indexOf('p.state.materials')<0&&s.indexOf('props.state.materials')<0)bad.push('statusBar materials not rewired');if(!/StatusBarProps[\s\S]{0,80}IUIPropsBase/.test(s))bad.push('statusBar base not IUIPropsBase');if(!/state=\{p\.state\}/.test(s))bad.push('statusBar state not threaded to MixedToolbar');if(t.indexOf('props.state.materials')<0)bad.push('toolbar materials not rewired');if(!/ToolbarProps[\s\S]{0,60}IUIPropsBase/.test(t))bad.push('toolbar base not IUIPropsBase');if(!/state=\{props\.state\}/.test(t))bad.push('toolbar state not threaded');if(bad.length>0){console.error('ALIGN FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK main/statusBar/toolbar aligned to props.state')"
+ non-zero exit, 或 stderr 含 "ALIGN FAILED"(`main` 的 props 声明 / 6 类替换 / TODO 缺失,或 `statusBar` / `toolbar` 的 `materials` 替换、props 基类型、`state` 透传缺失)
+ node -e "const fs=require('fs');const r=(p)=>fs.readFileSync(p,'utf8').replace(/\r\n/g,'\n');const m=r('packages-user/client-modules/src/ui/main.tsx');const s=r('packages-user/client-modules/src/ui/statusBar.tsx');const t=r('packages-user/client-modules/src/ui/toolbar.tsx');const bad=[];if(m.indexOf(\"from '@user/data-state'\")<0||m.indexOf('state.maps')<0)bad.push('main data-state singleton wrongly changed');if(m.indexOf('using.onExcitedFunc')<0||m.indexOf(\"from '../render'\")<0)bad.push('main using module singleton wrongly changed');if(m.indexOf('mainUIController')<0)bad.push('main mainUIController wrongly changed');if(s.indexOf('openViewMap(mainUIController')<0)bad.push('statusBar mainUIController wrongly changed');if((t.match(/mainUIController/g)||[]).length<1)bad.push('toolbar mainUIController wrongly changed');if(bad.length>0){console.error('BLOCKED ITEMS CHANGED :: '+bad.join(' ; '));process.exit(1)}console.log('OK blocked items (state/using/mainUIController) untouched')"
+ non-zero exit, 或 stderr 含 "BLOCKED ITEMS CHANGED"(阻断项被误替换:`state`(data-state) / `using` / `mainUIController` 读取被改动)
+
+
+ - `src/ui/**` 内零 `client`(`src/core`)导入与 `client.*` 用法
+ - `main.tsx` 新增 `MainSceneProps extends IUIPropsBase` 与 props 声明;`mainMapRenderer` / `mainMapExtension` / `flags` / 9 处 `hero.attribute.getFinalAttribute` / `hero.attribute.addHook` 全部改经 `props.state`;≥3 处 `// TODO:`
+ - `statusBar.tsx` / `toolbar.tsx` 的 `materials` 改经 `props.state`;props 基类型为 `IUIPropsBase`;`state` 透传给子工具栏
+ - 阻断项(`state`(data-state) / `using` / `mainUIController` / `sceneController`)保持原读取不动
+ - legacy / `core.*` / `ItemState` 只报告,未修改;既有注释逐字保留
+ - 无计划外文件被改
+
+ D-11 核心达成:`src/ui/` 内 `client` 单例彻底消除、主对象经 props 机制获取;D-13 flags 更新以 TODO 占位;4 个阻断项如实保持并登记
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| 组件 ↔ 主对象 | 顶层 UI 经容器注入 `props.state`;嵌套组件经上层透传 `state` — D-11 的接口边界 |
+| 数据端字段契约 | `props.state.flags` / `props.state.hero.attribute.*` / `props.state.materials` 取自 `IStateBase` / `IHeroState` / `IClientBase` 既有声明 |
+| 阻断项边界 | `state`(data-state) / `using` / `mainUIController` / `sceneController` 不得被替换 |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-09-03-01 | Elevation of Privilege | 自造 `IClientBase` 不存在的接口 / 误改阻断项 | high | mitigate | 映射表 6 项替换均有实证;阻断项默认不替换;Task 0 待裁定;门禁断言阻断项读取仍在 |
+| T-09-03-02 | Tampering | 误改 `props.state` 语义(`state`(data-state) 被当作 `props.state`) | medium | mitigate | assumptions 明列非同实例;门禁断言 `state`(data-state) 读取仍在 |
+| T-09-03-03 | Tampering | props 契约改动外溢(改公共 `IClientBase` / `client-base`) | medium | mitigate | 只改 `src/ui` 4 文件;prohibitions 明列;范围门禁 |
+| T-09-03-04 | Information Disclosure | 嵌套 `state` 透传使更多组件持完整主对象 | low | accept | 系 D-11 既定方向;不做权限收窄 |
+| T-09-03-05 | Repudiation | 未验证即提交 | medium | mitigate | 每任务静态门禁 + SUMMARY;验证未通过不得提交 |
+| T-09-03-SC | Tampering | npm/pip/cargo installs | low | accept | 无依赖安装、不执行 `pnpm i` |
+
+
+
+- **零 `client` 门禁**:`src/ui/**` 无 `src/core` 的 `client` 导入、无 `client.*` 用法。
+- **映射门禁**:6 项替换(`materials` / `flags` / `getFinalAttribute` ×9 / `addHook` / `mainMapRenderer` / `mainMapExtension`)全部改经 `props.state`。
+- **props 门禁**:`main.tsx` 新增 `MainSceneProps extends IUIPropsBase` + props 声明;`statusBar.tsx` / `toolbar.tsx` / `settings.tsx` props 基类型为 `IUIPropsBase` 且含 `state`;嵌套透传在场。
+- **阻断项门禁**:`state`(data-state) / `using` / `mainUIController` / `sceneController` 读取保持不动。
+- **D-13 门禁**:`main.tsx` ≥3 处、`settings.tsx` ≥2 处 `// TODO:`。
+- **范围门禁** 与 **CRLF 门禁** + **人工复核**(结论写入 SUMMARY)。
+
+
+
+- `src/ui/` 内 `client` 单例彻底消除,主对象经 props 机制获取(顶层注入 + 嵌套透传)。
+- 旧→新映射表的 6 项替换全部落地并有实证;4 项阻断项如实保持并登记待裁定。
+- flags 更新全部以 TODO 占位,未引入监听。
+- 零范围外改动;既有注释逐字保留;无运行时门禁。
+
+
+## Artifacts this phase produces
+
+**新增 / 修改的符号:**
+
+| 符号 | 类型 | 所属文件 | 说明 |
+|---|---|---|---|
+| `MainSceneProps` | 接口 | `packages-user/client-modules/src/ui/main.tsx` | 新增(`extends IUIPropsBase`),使 `MainScene` 经 props 机制取得 `state` |
+| `state`(props 成员) | prop | `ui/{main,settings,statusBar,toolbar}.tsx` | 各顶层 / 嵌套组件 props 新增 / 挂载主对象基类型(D-11 接口对齐) |
+
+**无其它新命名**(`client.*` → `props.state.*` 为既有成员替换)。**阻断项零变更**。
+
+
diff --git a/.planning/phases/09-minimum-browser-runnable/09-04-PLAN.md b/.planning/phases/09-minimum-browser-runnable/09-04-PLAN.md
new file mode 100644
index 0000000..a056598
--- /dev/null
+++ b/.planning/phases/09-minimum-browser-runnable/09-04-PLAN.md
@@ -0,0 +1,281 @@
+---
+phase: 09-minimum-browser-runnable
+plan: 4
+type: execute
+wave: 4
+depends_on:
+ - 09-03
+files_modified:
+ # ---- 新建响应式勇士属性对象(命名待用户批准)----
+ - packages-user/client-modules/src/ui/heroAttr.ts
+ - packages-user/client-modules/src/ui/main.tsx
+ - packages-user/client-modules/src/ui/index.ts
+ - .planning/phases/09-minimum-browser-runnable/09-04-SUMMARY.md
+autonomous: false
+requirements: []
+estimate:
+ tokens: 52000
+ raw_tokens: 52000
+ tasks: 3
+ confidence: low
+must_haves:
+ truths:
+ - "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,且 D-04 / D-12 明确命名须先获批)**:Task 0 必须先汇报「要解决的问题(`main.tsx` 内联的 hero 属性读取(9 处 `props.state.hero.attribute.getFinalAttribute`)与 `attributeHook` 订阅未封装为独立响应式对象)」「锁定裁定 D-12 原文要点」「命名候选与冲突说明(**`IHeroAttr` 与 `@user/data-common` 既有属性字典类型 `IHeroAttr` 及 `IHeroAttribute` 一族重名,不得沿用**)」「拟实现的对象契约(接口 / 类 / 工厂、字段集、订阅与卸载、非属性字段归属)」「范围与硬边界」「验证方式」,然后必须**停下等待**用户明确回复「可以执行」四个字**并给出命名批准**;未收到命名批准前不得创建任何以该名字命名的文件 / 接口 / 类,也不得执行本计划任何一步、不得派发执行子代理。"
+ - "**D-12:单独实现一个响应式勇士属性对象**:新增文件 `packages-user/client-modules/src/ui/.ts`(文件名与接口 / 类名取用户批准者;规划默认占位与注册的路径为 `heroAttr.ts` —— **仅在用户批准该名后方可沿用**);该对象:① 以 `reactive` 暴露指定勇士属性字段;② 经 `props.state.hero.attribute.addHook({ onUpdateAttribute })` 监听属性变化并据此更新;③ 提供卸载(`addHook` 返回的 `IHookController.unload()`),在组件 `onUnmounted` 时调用;④ 遵循 `dev.md:100`(先 `interface` 声明再 `class implements`)。"
+ - "**命名纪律(D-04 / D-12 / AGENTS.md)**:**不得**使用 `IHeroAttr` 作为新对象名(与 `@user/data-common` 的属性字典类型 `IHeroAttr`(`data-base/src/types.ts:8,21` 泛型实参)冲突);**不得**复用 / 覆写 `IHeroAttribute` / `IReadonlyHeroAttribute` 一族语义;除用户批准的新命名外**零命名变更**。"
+ - "**订阅与读值证据(只读基准,不改)**:`IHeroAttributeHooks.onUpdateAttribute?(name, value)`(`data-base/src/hero/types.ts:95-102`);`IReadonlyHeroAttribute extends IHookable`(`:104-106`);`getFinalAttribute(name)`(`:117`);`IHeroState.attribute: IReadonlyHeroAttribute`(`:921-928`)。"
+ - "**接线:`main.tsx` 改用新对象**:`main.tsx` 的 9 处 hero 属性读取与 `attributeHook` 订阅改由新响应式对象承担(对象在组件 setup 内创建、`onUnmounted` 卸载);`main.tsx` 中**非属性字段**(钥匙 / `core.itemCount`、`flags` 派生的 poison/weak/curse、`core.status.floorId`、`core.getLvName`、`core.getNextLvUpNeed`、`replayStatus`、`locked`)**保持原读取不动**(属 legacy / 其它来源,不在 D-12 范围);4 个阻断项(`state`(data-state) / `using` / `mainUIController` / `sceneController`)与所有 `core.*`、legacy 只报告项**保持不动**。"
+ - "**范围零外溢 + 无运行时门禁**:验证为**静态**(新文件在场 / 接口与类 / `addHook` 订阅 / `unload` 卸载 / `main.tsx` 已接线 / `src/ui` 零 `client`);**不得**以「TS 诊断数」「`pnpm check:type`」「`pnpm build`」为门禁。`client-base/src/material/**`(D-42 / D-48)、`packages/**`、`src/**`、legacy(D-14)、用户并发改动(D-56)零触碰。"
+ artifacts:
+ - path: packages-user/client-modules/src/ui/heroAttr.ts
+ provides: "D-12 的响应式勇士属性对象(文件名 / 接口名 / 类名待用户批准;默认占位 `heroAttr.ts`):`interface` + `class implements`,`reactive` 属性字段 + `props.state.hero.attribute.addHook({ onUpdateAttribute })` 订阅 + `unload` 卸载契约"
+ - path: packages-user/client-modules/src/ui/main.tsx
+ provides: "接线后的主场景 UI:hero 属性读取与订阅委托给新响应式对象;非属性字段 / 阻断项 / legacy 保持不动"
+ - path: packages-user/client-modules/src/ui/index.ts
+ provides: "新增新响应式对象的同目录 `export * from './'`(D-69 合规)"
+ key_links:
+ - "`packages-user/client-modules/src/ui/main.tsx`(组件 setup)↔ 新响应式对象(`createXxx` / `new Xxx`)↔ `props.state.hero.attribute.addHook` —— D-12 的订阅链"
+ - "`packages-user/client-modules/src/ui/main.tsx:148-150`(既有 `onUnmounted(() => attributeHook.unload())`)↔ 新对象的卸载契约 —— 生命周期清理范式(`09-PATTERNS.md` Shared Patterns / 生命周期清理)"
+ - "`packages-user/data-base/src/hero/types.ts:95-117,921-928`(`onUpdateAttribute` / `getFinalAttribute` / `IHeroState.attribute`)↔ 新对象读值 / 监听 —— 数据端接口证据"
+ assumptions:
+ - "**命名待批准(D-04 / D-12;阻断本计划):** 规划日无法确定新对象名。**`IHeroAttr` 因与 `@user/data-common` 的属性字典类型 `IHeroAttr` 重名而不可用**。执行者必须在 Task 0 取得用户的明确批准后再创建文件 / 命名;未获批准前不得臆造名字。"
+ - "**对象形态待确认:** 规划默认 = `interface` + `class implements`(`dev.md:100`),由 `main.tsx` 在 setup 内实例化并 `onUnmounted` 卸载;若用户偏好工厂函数 / composable,按 Task 0 确认结果调整。因为 `reactive` 与 `onUnmounted` 依赖组件实例,对象须在组件 setup 内创建。"
+ - "**字段集待确认:** 默认覆盖 `main.tsx` 现经 `getFinalAttribute` 读取的 9 个属性(`atk` / `hp` / `hpmax` / `mana` / `manamax` / `def` / `mdef` / `money` / `exp`);非属性字段(钥匙 / debuff / floor / lv / up / replay / locked)**不在本对象内**。若用户要求并入更多字段,按 Task 0 确认结果调整。"
+ - "**`main.tsx` 的 `updateDataFallback()`(`locked.value = core.status.lockControl`)不在 D-12 范围:** 其来源为 legacy `core.status`、非勇士属性;默认保持在 `main.tsx`(若原订阅点被新对象取代,须确保 `updateDataFallback` 仍有触发路径或按 Task 0 确认后的方案处理)。**不得**因封装新对象而丢失既有更新行为。"
+ - "**执行日行号可能漂移:** 引用锚点为 2026-10-09 规划日;执行时按符号文本定位并在 SUMMARY 记录差异。"
+ prohibitions:
+ - "不得在用户明确回复「可以执行」四个字**并给出命名批准**之前执行本计划的任何任务、或创建任何以未批准名字命名的文件 / 接口 / 类、或派发任何执行子代理。"
+ - "**不得擅自使用 `IHeroAttr` 作为新对象名**(与既有属性字典类型冲突);不得复用 / 覆写 `IHeroAttribute` / `IReadonlyHeroAttribute` 语义;除用户批准的新命名外零命名变更。"
+ - "本计划**只做 D-12**:不得做 D-11 之外的接口改造、不得改 UI 形态 / 视觉(D-14)、不得移植 `@motajs/legacy-ui`、不得新增 flags 监听(D-13)。"
+ - "不得触碰 `files_modified` 之外的任何生产文件;尤其不得改 `main.tsx` 的非属性字段 / 阻断项 / legacy 读取,不得改 `src/ui/{load,title,controller,background,func,types,save,statistics,viewmap,statusBar,toolbar}.tsx/ts`。"
+ - "不得修改任何既有 jsDoc 与注释(AGENTS.md);新增注释遵循 `dev.md:72-86`;不得引入 `import type`(dev.md:54);不得使用 `?.`(dev.md:107);类的成员显式声明类型(dev.md:91);CRLF。"
+ - "不得删除或改名任何模块级单例;不得修复任何 legacy 报错(D-14);不得把门禁建立在「TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上。"
+ - "不得触碰、暂存、提交用户的并发改动(D-56):提交前 `git status` 逐条核对暂存面,只暂存本计划 `files_modified` 的条目。"
+---
+
+
+Phase 9 · 任务 1 · 第 4 个计划 —— **D-12:单独实现一个响应式勇士属性对象,监听勇士属性变化并据此更新指定属性(命名待用户批准)**。
+
+**要解决的问题:**
+
+`main.tsx` 目前在组件内内联读取 9 处勇士属性(`props.state.hero.attribute.getFinalAttribute(...)`)并以内联 `attributeHook = props.state.hero.attribute.addHook({ onUpdateAttribute })` 订阅更新;用户要求把这部分**单独实现为一个响应式对象**(D-12),以便复用与隔离。**命名待用户批准**——`IHeroAttr` 与 `@user/data-common` 既有属性字典类型 `IHeroAttr` 重名,不得沿用。
+
+**锁定裁定:** D-12 = 单独实现一个 `IHeroAttr` 响应式对象,监听勇士属性变化并据此更新(命名待批准;注意与既有 `IHeroAttribute` 一族区分)。D-14 = 纯适配,不改形态、不移植 legacy。
+
+**大致解决方案(本计划 = 09-04):** Task 1(tracer)= 新建响应式对象文件(`interface` + `class implements`,`reactive` 属性字段 + `addHook` 订阅 + `unload` 卸载);Task 2 = 接入 `main.tsx`(替换 9 处属性读取与 `attributeHook`,保留非属性字段与阻断项)+ 桶导出 + 门禁。
+
+**API coverage:** `detected:false`。**`No external API integration: 本对象仅订阅数据端 `IHeroAttribute` 钩子并更新 Vue `reactive` 字段,不涉及任何外部 API。`**
+
+
+
+@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
+@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
+
+
+
+@.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-PATTERNS.md
+@.planning/phases/09-minimum-browser-runnable/09-03-PLAN.md
+@AGENTS.md
+@dev.md
+
+
+
+
+
+ Task 0: 预执行汇报关卡 —— 汇报「D-12 响应式勇士属性对象」设计并**请求命名批准**(`IHeroAttr` 不可用),等待用户回复「可以执行」
+ `09-03` 已闭环:`main.tsx` 已使用 `props.state.hero.attribute.getFinalAttribute`(9 处)与 `props.state.hero.attribute.addHook`;`src/ui/` 内零 `client`。
+ (只读汇报,不修改任何文件)
+
+ - .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-12 原文;D-04 命名须先获批;D-07)
+ - .planning/phases/09-minimum-browser-runnable/09-PATTERNS.md(§7「新建 `IHeroAttr` 响应式对象」+ `No Analog Found` 表 + Shared Patterns「生命周期清理」)
+ - .planning/phases/09-minimum-browser-runnable/09-04-PLAN.md(本计划全文)
+ - packages-user/client-modules/src/ui/main.tsx(`:66-93` reactive 状态;`:96-133` `updateStatus` 的 9 处 getFinalAttribute;`:135-138` `updateDataFallback`;`:141-150` `attributeHook` + `onUnmounted`)
+ - packages-user/data-base/src/hero/types.ts:95-129,921-934(`IHeroAttributeHooks` / `IReadonlyHeroAttribute` / `IHeroState.attribute`)
+ - packages-user/data-base/src/types.ts:8,21(既有 `IHeroAttr` 属性字典类型——命名冲突证据)
+ - dev.md(命名规则 / 类须 interface 先行再 implements / 成员显式类型 / 不写 getter-setter / CRLF)
+
+
+ **停下并向用户汇报,等待明确回复并取得命名批准后才继续;汇报本身只读。**
+
+ ① **要解决的问题:** `main.tsx` 内联读取 9 处勇士属性并内联订阅 `attributeHook`;D-12 要求把这部分封装为**单独一个响应式对象**。
+
+ ② **锁定裁定:** **D-12**(单独实现响应式 `IHeroAttr` 对象,监听勇士属性变化并据此更新;命名待批准);**D-14**(纯适配)。
+
+ ③ **命名冲突(须用户拍板):** **`IHeroAttr` 与 `@user/data-common` 既有属性字典类型 `IHeroAttr`(`data-base/src/types.ts:8,21` 泛型实参)重名,本计划不得沿用 `IHeroAttr`。** 请用户在 Task 0 给出新对象名(文件名 + 接口名 + 类名;可先不指定由 AI 提候选再批准)。规划默认占位 = 文件 `heroAttr.ts`、接口 `IHeroAttrState`、类 `HeroAttrState`(**仅为占位,未获批不得使用**)。
+
+ ④ **拟实现契约(须用户确认):**
+ - 形态:`interface` 先声明、`class implements`(`dev.md:100`);在组件 setup 内创建(`reactive` 与 `onUnmounted` 依赖组件实例)。
+ - 读值:经构造参数接收 `props.state`(`IClientBase`),内部用 `props.state.hero.attribute.getFinalAttribute(name)` 初始化 / 刷新字段。
+ - 监听:`props.state.hero.attribute.addHook({ onUpdateAttribute })`(`data-base/src/hero/types.ts:95-102`);提供 `unload()`(透传 `IHookController.unload()`)供 `onUnmounted` 调用。
+ - 字段集:默认覆盖 `atk` / `hp` / `hpmax` / `mana` / `manamax` / `def` / `mdef` / `money` / `exp`(现 9 处读取);**非属性字段(钥匙 / debuff / floor / lv / up / replay / locked)不并入**。
+ - `updateDataFallback()`(`locked = core.status.lockControl`)来源为 legacy `core.status`、非属性 ⇒ 默认保留在 `main.tsx`,不并入对象。
+
+ ⑤ **范围与硬边界:** 改动面 = 新文件 + `main.tsx` 接线 + 桶导出。**零触碰** = 非属性字段 / 阻断项(`state`(data-state) / `using` / `mainUIController` / `sceneController`)/ legacy(D-14)/ `client-base/src/material/**`(D-42 / D-48)/ `packages/**` / `src/**` / 用户并发改动(D-56)。**无依赖变更、不执行 `pnpm i`。**
+
+ ⑥ **验证方式:** 静态——新文件在场(含 `interface` + `class implements` + `reactive` + `addHook` + `unload`)、`main.tsx` 已接线且非属性字段 / 阻断项保持、`src/ui` 零 `client`。**不得**使用 `pnpm check:type` / `pnpm build` / TS 诊断数作门禁。
+
+ ⑦ **关卡性质与失败语义:** 无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者/子代理必须停下等待用户明确回复「可以执行」四个字**并给出命名批准**;未收到命名批准前不得创建任何以该名字命名的文件 / 接口 / 类,也不得执行任何一步、不得派发执行子代理。回复其它内容视为未通过。
+
+ ⑧ **若用户给出修订意见**(含否决形态 / 调整字段集 / 指定不同命名):退出本计划、修订后重新汇报。
+
+ 是否按本计划(新建响应式勇士属性对象[文件名 / 接口名 / 类名取用户批准者]、`reactive` 字段 + `props.state.hero.attribute.addHook({onUpdateAttribute})` 订阅 + `unload` 卸载;接入 `main.tsx` 替换 9 处属性读取与 `attributeHook`,非属性字段 / 阻断项 / legacy 保持不动;桶导出;无依赖变更、无运行时门禁)开始执行;**并批准该新对象的具体命名**
+
+ AGENTS.md 与 D-04 / D-12 均要求命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。本次核心未知项 = **命名**(`IHeroAttr` 重名不可用)与对象形态 / 字段集,必须让用户拍板后才能落地。
+
+
+
+
+
+
+ 回复「可以执行」四个字并给出命名批准(修订意见请按 action ⑧ 处理)
+
+ 用户在对话中明确回复「可以执行」四个字且给出了新对象的命名批准(人工核对用户回复原文;失败语义写在 action ⑦ 与 acceptance_criteria)
+
+
+ - 用户回复中明确包含「可以执行」四个字
+ - 用户已明确批准新对象的具体命名(文件名 / 接口名 / 类名至少其一),且**不是** `IHeroAttr`
+ - 汇报内容涵盖:要解决的问题、D-12 / D-14 原文要点、命名冲突说明(`IHeroAttr` 不可用)、拟实现契约(形态 / 读值 / 监听 / 卸载 / 字段集 / 非属性字段归属)、范围与硬边界、验证方式
+ - 汇报中已原样告知**非属性字段与 `updateDataFallback` 不并入对象**、阻断项保持不动
+ - 汇报中已原样告知**无运行时要求**(不设 `check:type` / `build` / TS 诊断数类门禁)
+ - 汇报中已原样告知**关卡性质**(必须停下等待命名批准,不因触发方式或 `yolo` / `auto_advance` 自动放行)
+
+ 用户明确回复「可以执行」并批准命名;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理
+
+
+
+ Task 1(tracer):端到端创建响应式勇士属性对象 —— 新建 `src/ui/.ts`(`interface` + `class implements`:`reactive` 属性字段 + `addHook` 订阅 + `unload` 卸载),并加桶导出
+ Task 0 已闭环:用户明确回复「可以执行」四个字**并批准命名**(未收到命名批准则不得创建任何文件)
+ packages-user/client-modules/src/ui/heroAttr.ts, packages-user/client-modules/src/ui/index.ts
+
+ - packages-user/client-modules/src/ui/main.tsx:66-93,96-150(内联 `leftStatus` reactive 字段集 + `getFinalAttribute` 读取 + `attributeHook` 订阅 + `onUnmounted` 卸载——本对象要封装的既有逻辑)
+ - packages-user/data-base/src/hero/types.ts:95-129(`IHeroAttributeHooks.onUpdateAttribute` / `IReadonlyHeroAttribute extends IHookable` / `getFinalAttribute` / `markDirty`)
+ - packages-user/data-base/src/types.ts:8,21(既有 `IHeroAttr` 属性字典类型——命名不可沿用)
+ - packages-user/client-base/src/types.ts:21(`IClientBase extends ICoreState`,其 `hero.attribute` 可供读值)
+ - .planning/phases/09-minimum-browser-runnable/09-PATTERNS.md(§7 内联参照 + 约束;`No Analog Found`)
+ - dev.md(类须 interface 先行再 implements;成员显式类型;不写 getter/setter;引入置首;CRLF)
+
+
+ (1) **新建文件:** 在 `packages-user/client-modules/src/ui/` 新建文件,文件名取 **Task 0 用户批准的名字**(规划默认占位 = `heroAttr.ts`;仅在用户批准后可沿用;未获批准**停下询问**)。文件内先 `export interface <接口名>` 声明契约,再 `export class <类名> implements <接口名>`(`dev.md:100`),类成员显式声明类型(`dev.md:91`)。
+
+ (2) **对象契约:** 构造器接收主对象(类型 `IClientBase`,自 `@user/client-base` 引入);以 `reactive`(自 `vue` 引入)暴露 9 个属性字段(`atk` / `hp` / `hpmax` / `mana` / `manamax` / `def` / `mdef` / `money` / `exp`,命名沿用 `ILeftHeroStatus` 同款);提供刷新方法(用 `this.state.hero.attribute.getFinalAttribute(name)` 逐字段赋值)与订阅:`this.hook = this.state.hero.attribute.addHook({ onUpdateAttribute: () => this.update() })`。提供 `unload()` 方法透传 `this.hook.unload()`。**不得**使用 `?.`;**不得**写 getter / setter(`dev.md:113`)。**不得**并入非属性字段(钥匙 / debuff / floor / lv / up / replay / locked)。
+
+ (3) **桶导出:** 在 `packages-user/client-modules/src/ui/index.ts` 追加 `export * from './';`(同目录,D-69 合规)。
+
+ (4) **CRLF 与收口:** 新文件须为 CRLF;本任务只允许新建该文件并改 `index.ts`。
+
+ 新增一个包内实现层文件 + 一行桶导出;`git revert` 可整体回退;未改任何既有公共契约。
+
+ node -e "const fs=require('fs');const dir='packages-user/client-modules/src/ui';const names=fs.readdirSync(dir).filter(f=>f.endsWith('.ts')&&f!=='index.ts'&&f!=='types.ts'&&f!=='func.ts'&&f!=='controller.ts'&&f!=='heroAttr.ts');const target='packages-user/client-modules/src/ui/heroAttr.ts';if(!fs.existsSync(target)){console.error('NEW FILE MISSING (approved name?) '+target);process.exit(1)}const t=fs.readFileSync(target,'utf8').replace(/\r\n/g,'\n');const bad=[];if(!/export interface \w+/.test(t))bad.push('interface missing');if(!/export class \w+ implements \w+/.test(t))bad.push('class implements missing');if(!/reactive\(/.test(t))bad.push('reactive missing');if(!/addHook\(/.test(t))bad.push('addHook missing');if(!/onUpdateAttribute/.test(t))bad.push('onUpdateAttribute missing');if(!/unload\(\)/.test(t))bad.push('unload missing');if(!/getFinalAttribute/.test(t))bad.push('getFinalAttribute missing');if(/\bIHeroAttr\b/.test(t))bad.push('ILlegal name IHeroAttr used');if(bad.length>0){console.error('HERO ATTR FILE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK hero-attr object file: interface+class+reactive+addHook+unload')"
+ non-zero exit, 或 stderr 含 "NEW FILE MISSING" / "HERO ATTR FILE FAILED" / "ILlegal name IHeroAttr"(新文件缺失、缺 interface/class/reactive/addHook/unload/getFinalAttribute,或误用 `IHeroAttr`)
+
+
+ - 新文件在场(文件名 = 用户批准者;规划默认 `heroAttr.ts`)
+ - 含 `export interface` + `export class ... implements`(`dev.md:100`)
+ - 含 `reactive(`、`addHook(`、`onUpdateAttribute`、`unload()`、`getFinalAttribute`
+ - **未**出现 `IHeroAttr` 作为新对象名
+ - `src/ui/index.ts` 含对应同目录 `export *`
+
+ 响应式勇士属性对象文件创建完成(禁名未用、契约齐备),桶导出就位
+
+
+
+ Task 2:接入 `main.tsx`(用新对象替换 9 处属性读取与 `attributeHook` 订阅;保留非属性字段 / `updateDataFallback` / 阻断项 / legacy)+ 门禁
+ Task 1 已完成(新对象文件在场且桶导出就位)
+ packages-user/client-modules/src/ui/main.tsx
+
+ - packages-user/client-modules/src/ui/main.tsx(整文件;`:35` `const { mainMapRenderer, mainMapExtension } = props.state;`;`:66-90` `leftStatus` 字段集;`:96-133` `updateStatus`(9 处 `props.state.hero.attribute.getFinalAttribute`、keys、debuff、floor、lv、up、replay、hard);`:135-138` `updateDataFallback`;`:141-150` 新订阅与 `onUnmounted`;`:228-232` ``)
+ - packages-user/client-modules/src/ui/heroAttr.ts(Task 1 的产物,确认构造签名与字段名)
+ - dev.md(不改既有注释;CRLF;无 `?.`)
+
+
+ (1) **接线:** 在 `main.tsx` 组件 setup 内,用 Task 1 的新响应式对象承载勇士属性:创建该对象(传入 `props.state`),以它的属性字段替换 `leftStatus` 中对应的 9 个属性字段读取(`leftStatus.atk/hp/hpmax/mana/manamax/def/mdef/money/exp` 改由新对象提供);把原 `attributeHook = props.state.hero.attribute.addHook({...})` 的订阅交给新对象(新对象内部持有钩子),`onUnmounted` 改为调用新对象的 `unload()`。**不得**改变 `` 对外的形态(D-14)。
+
+ (2) **保留不动:** `leftStatus` 的非属性字段(`up` / `yellowKey` / `blueKey` / `redKey` / `greenKey` / `pickaxe` / `bomb` / `centerFly` / `poison` / `weak` / `curse` / `floor` / `lv` / `replay`)与其来源(`core.itemCount` / `flags` / `core.status`);`updateDataFallback()` 与 `locked`;`rightStatus`;阻断项(`state`(data-state) 含 `layerState={state.maps}`、`using`、`mainUIController`);所有 `core.*` 与 legacy 导入。**不得**因接线而丢失任一既有更新触发路径(若原 `attributeHook` 同时驱动属性更新与 `updateDataFallback`,须保证二者仍有触发路径)。
+
+ (3) **CRLF 与收口:** 文件须为 CRLF;本任务只允许改 `main.tsx`。
+
+ 组件内接线调整;`git revert` 可整体回退;未改公共契约。
+
+ node -e "const fs=require('fs');const t=fs.readFileSync('packages-user/client-modules/src/ui/main.tsx','utf8').replace(/\r\n/g,'\n');const bad=[];if(!/heroAttr|HeroAttr|AttrState|AttrView|HeroAttributeState/i.test(t))bad.push('new object not wired into main.tsx');if(!/\.unload\(\)/.test(t))bad.push('unload() not called');if(t.indexOf('updateDataFallback')<0)bad.push('updateDataFallback lost');if(t.indexOf('layerState={state.maps}')<0)bad.push('data-state layerState wrongly changed');if(t.indexOf('using.onExcitedFunc')<0)bad.push('using singleton wrongly changed');if(t.indexOf('mainUIController')<0)bad.push('mainUIController wrongly changed');if(t.indexOf('leftStatus.')<0)bad.push('leftStatus non-attr fields lost');if(bad.length>0){console.error('WIRING FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK hero-attr object wired, non-attr/blocked items preserved')"
+ non-zero exit, 或 stderr 含 "WIRING FAILED"(新对象未接线 / 未 `unload` / 丢失 `updateDataFallback` / 误改阻断项 / 丢失非属性字段)
+ node -e "const fs=require('fs');const path=require('path');const dir='packages-user/client-modules/src/ui';const bad=[];for(const f of fs.readdirSync(dir).filter(f=>/\.(ts|tsx)$/.test(f))){const t=fs.readFileSync(path.join(dir,f),'utf8');if(/from '\.\.\/(?:\.\.\/)?core'/.test(t)||/\bclient\.[a-zA-Z]/.test(t))bad.push(f)}if(bad.length>0){console.error('CLIENT REMAINS :: '+bad.join(','));process.exit(1)}console.log('OK src/ui still client-free')"
+ non-zero exit, 或 stderr 含 "CLIENT REMAINS"(接线使 `src/ui` 重新出现 `client` 单例用法)
+
+
+ - `main.tsx` 引用并使用新响应式对象;`onUnmounted` 调用其 `unload()`
+ - 9 个勇士属性读取改由新对象提供;非属性字段(keys / debuff / floor / lv / up / replay)与 `updateDataFallback` / `locked` 保持
+ - 阻断项(`state`(data-state) / `using` / `mainUIController` / `sceneController`)与 legacy / `core.*` 保持不动
+ - `src/ui/**` 仍零 `client`
+ - 无计划外文件被改
+
+ D-12 完成:响应式勇士属性对象独立实现并接入 `main.tsx`,属性更新经该对象驱动,非属性字段 / 阻断项 / legacy 零改动
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| 命名 / 接口边界 | 新对象命名须用户批准;不得与既有 `IHeroAttr` / `IHeroAttribute` 冲突 |
+| 组件 ↔ 数据端钩子 | 新对象经 `props.state.hero.attribute.addHook` 订阅数据端属性钩子 |
+| 生命周期边界 | 订阅须在 `onUnmounted` 卸载,避免泄漏 |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-09-04-01 | Elevation of Privilege | 未获批命名 / 复用 `IHeroAttr` | high | mitigate | Task 0 强制命名批准、明列 `IHeroAttr` 不可用;Task 1 门禁断言 `IHeroAttr` 未出现 |
+| T-09-04-02 | Denial of Service | 订阅未卸载导致泄漏 / 重复订阅 | medium | mitigate | `unload()` 契约 + `onUnmounted` 调用;门禁断言 `unload()` 调用在场 |
+| T-09-04-03 | Tampering | 接线丢失既有更新行为(`updateDataFallback` / 非属性字段) | medium | mitigate | Task 2 门禁断言 `updateDataFallback` / `leftStatus` 非属性字段 / 阻断项保持 |
+| T-09-04-04 | Tampering | 范围外文件 / 阻断项 / legacy 被改 | medium | mitigate | 白名单 + prohibitions + 任务门禁 + `git status` 核对 |
+| T-09-04-05 | Repudiation | 未验证即提交 | medium | mitigate | 每任务静态门禁 + SUMMARY;验证未通过不得提交 |
+| T-09-04-SC | Tampering | npm/pip/cargo installs | low | accept | 无依赖安装、不执行 `pnpm i` |
+
+
+
+- **文件门禁**:新对象文件在场(含 `interface` + `class implements` + `reactive` + `addHook` + `onUnmounted`/`unload`),且未使用禁用名 `IHeroAttr`。
+- **接线门禁**:`main.tsx` 使用新对象、调用 `unload()`;非属性字段 / `updateDataFallback` / 阻断项 / legacy 保持。
+- **零 `client` 门禁**:`src/ui/**` 无 `client` 用法。
+- **范围门禁** 与 **CRLF 门禁** + **人工复核**(结论写入 SUMMARY)。
+
+
+
+- 响应式勇士属性对象按用户批准命名独立实现,订阅与卸载契约齐备。
+- `main.tsx` 的勇士属性读取与更新改由该对象驱动,无行为丢失。
+- 非属性字段 / 阻断项 / legacy 零改动;零范围外改动;无运行时门禁。
+
+
+## Artifacts this phase produces
+
+**新增文件与符号(命名待用户批准;默认占位仅供参考):**
+
+| 符号 / 路径 | 类型 | 说明 |
+|---|---|---|
+| `packages-user/client-modules/src/ui/heroAttr.ts`(文件名待批准) | 文件(新) | D-12 的响应式勇士属性对象 |
+| `<接口名>`(待批准;默认占位 `IHeroAttrState`) | 接口(新) | 对象契约:属性字段 + 更新方法 + `unload()` |
+| `<类名>`(待批准;默认占位 `HeroAttrState`) | 类(新,`implements <接口名>`) | `reactive` 字段 + `state.hero.attribute.addHook({ onUpdateAttribute })` 订阅 + `unload()` |
+| `packages-user/client-modules/src/ui/index.ts`(改) | barrel | 新增 `export * from './';` |
+
+**禁用名:** `IHeroAttr`(与 `@user/data-common` 属性字典类型冲突)——不得使用。
+
+