fix(09): revise plans based on checker feedback (round 2)

This commit is contained in:
unanmed 2026-10-09 14:27:17 +08:00
parent d53188af1f
commit dbbcf533dc
6 changed files with 470 additions and 49 deletions

View File

@ -33,18 +33,18 @@ 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'` 等旧层级说明符。"
- "**迁移后相对路径逐条改写正确**:`src/ui/save.tsx` 用 `'../render/use'`(`useKey`,直连 `src/render/use.ts` 叶子模块、不经 `src/render/index.tsx` 桶,避免与 `render/index.tsx` 的 `import ... from '../ui'` 构成循环引用)与 `'../render/utils'`(`getSave` / `SaveData`)与 `'../shared'`;`src/ui/viewmap.tsx` 用 `'../render/use'`(`useKey`)与 `'../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`"
provides: "D-10 迁移后的存读档 UI(叶子模块):相对路径改指 `'../render/use'` / `'../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'`"
provides: "D-10 迁移后的浏览地图 UI:`'./statistics'` 同目录保持,`'../use'`→`'../render/use'`、`'../../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
@ -247,7 +247,7 @@ Phase 9 · 任务 1(UI 接口适配)· 四个计划中的第 1 个 —— **
<action>
(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` 导入保持原样。
(1) **建立目标文件:** 将 `packages-user/client-modules/src/render/ui/save.tsx` 的内容迁移为 `packages-user/client-modules/src/ui/save.tsx`,只改写三处相对说明符:`'../use'` → `'../render/use'`(`useKey` 现位于 `src/render/use.ts`,**必须直连该叶子模块**;不得改指 `'../render'` 桶 `src/render/index.tsx`——`render/index.tsx` 会 `import ... from '../ui'`,经桶转发将构成 `render/index.tsx ↔ src/ui` 循环引用,违反 `dev.md:53`);`'../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`)。
@ -257,14 +257,14 @@ Phase 9 · 任务 1(UI 接口适配)· 四个计划中的第 1 个 —— **
</action>
<reversibility rating="reversible">纯文件位移 + 相对说明符改写,全部在 git 追踪范围内,可用 `git revert` 或重跑本计划整体回退;未引入任何接口行为、契约或新命名。</reversibility>
<verify>
<automated>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')"</automated>
<automated>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/use\'','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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "MISSING" / "OLD save.tsx REMAINS" / "STALE SPECIFIER" / "CONTENT MISSING"(新文件未建、旧文件未删、残留旧层级说明符、或关键导出缺失)</fails_when>
<automated>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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "BARREL ./save MISSING"(新桶未导出 `./save`)</fails_when>
</verify>
<acceptance_criteria>
- `src/ui/save.tsx` 在场且 `git mv` 语义成立(旧 `render/ui/save.tsx` 不存在)
- 新文件无 `'../use'` / `'../utils'` / `'../../shared'` / `'../../core'` 残留;含 `'../render'` / `'../render/utils'` / `'../shared'` 三处新说明符
- 新文件无 `'../use'` / `'../utils'` / `'../../shared'` / `'../../core'` 残留;含 `'../render/use'` / `'../render/utils'` / `'../shared'` 三处新说明符(`render` 能力经叶子模块直连,不经 `'../render'` 桶)
- `SaveUI` / `selectSave` / `saveSave` / `saveLoad` / `saveWithExist` 五个导出逐字在场
- `src/ui/index.ts` 含 `export * from './save';` 且既有 `export * from './types';` 未被破坏
- `git status` 显示 `render/ui/save.tsx` 处于删除态、`ui/save.tsx` 为新增;无其它计划外文件被改
@ -278,14 +278,14 @@ Phase 9 · 任务 1(UI 接口适配)· 四个计划中的第 1 个 —— **
<files>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(删除)</files>
<read_first>
- 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/render/ui/viewmap.tsx(整文件 568 行;须改相对说明符:`:28` `import { StatisticsDataOneFloor } from './statistics';`(同目录,不变)、`:30` `import { useKey } from '../use';` → `'../render/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、不转发导出)
</read_first>
<action>
(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`。
(2) **`viewmap.tsx` 迁移 + 说明符改写:** 将 `packages-user/client-modules/src/render/ui/viewmap.tsx` 迁移为 `packages-user/client-modules/src/ui/viewmap.tsx`,只改写相对说明符:`'../use'` → `'../render/use'`(直连 `src/render/use.ts` 叶子模块;不得改指 `'../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` 并列;保持既有行原样)。
@ -293,14 +293,14 @@ Phase 9 · 任务 1(UI 接口适配)· 四个计划中的第 1 个 —— **
</action>
<reversibility rating="reversible">同上:纯文件位移 + 说明符改写,`git revert` 可整体回退,无接口行为 / 新命名。</reversibility>
<verify>
<automated>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')"</automated>
<automated>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/use\'')<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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "MISSING" / "OLD REMAINS" / "VIEWMAP STALE" / "VIEWMAP NEW SPECIFIER MISSING" / "STATISTICS CONTENT MISSING"(文件未迁移 / 旧文件未删 / viewmap 说明符未改写 / statistics 内容丢失)</fails_when>
<automated>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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "BARREL MISSING"(新桶缺任一同目录导出)</fails_when>
</verify>
<acceptance_criteria>
- `src/ui/statistics.tsx` 与 `src/ui/viewmap.tsx` 在场;对应旧文件已删
- `viewmap.tsx` 无 `'../use'` / `'../../shared'` 残留,含 `'../render'` / `'../shared'` / `'./statistics'`
- `viewmap.tsx` 无 `'../use'` / `'../../shared'` 残留,含 `'../render/use'` / `'../shared'` / `'./statistics'`
- `statistics.tsx` 逐字保留(含既有 `@ts-expect-error` 与 `ItemState` 导入)
- `src/ui/index.ts` 含 `./types` / `./save` / `./statistics` / `./viewmap` 四条同目录导出
- 无计划外文件被改
@ -315,8 +315,8 @@ Phase 9 · 任务 1(UI 接口适配)· 四个计划中的第 1 个 —— **
<read_first>
- 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/load.tsx:1-32(`:18` `import { transitioned } from '../render';`(经 `render` 桶 → 须改 `'../render/use'`);`:23` 断链导入 `IUIDefaultPropsBase` 自 `./types`;`:25` `ILoadProps extends IUIDefaultPropsBase, DefaultProps`;`:32` 已用 `props.state.loader`)
- packages-user/client-modules/src/ui/title.tsx:1-40(`:25-30` `import { ITransitionedController, transitioned, transitionedColor, useKey } from '../render';`(经 `render` 桶 → 须改 `'../render/use'`,四个符号均定义于 `src/render/use.ts`);`: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)
@ -326,7 +326,7 @@ Phase 9 · 任务 1(UI 接口适配)· 四个计划中的第 1 个 —— **
(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` 导入因此变为未使用,一并移除该未使用导入(仅限此目的,不得顺手改其它导入)。
(3) **`load.tsx` / `title.tsx` props 基类型修正(断链闭合)+ `render` 能力导入改指叶子模块:** 两文件均删除从 `./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` 导入因此变为未使用,一并移除该未使用导入(仅限此目的,不得顺手改其它导入)。**同时**把两文件从 `'../render'`(`src/render/index.tsx` 桶)引入 `render/use.ts` 导出的语句改指 `'../render/use'`:`load.tsx:18` 的 `{ transitioned }`、`title.tsx:25-30` 的 `{ ITransitionedController, transitioned, transitionedColor, useKey }`(符号文本不变,仅改说明符)。此改写属 D-10 引用改写,**必须**:`render/index.tsx` 会 `import ... from '../ui'`,若 `ui` 经 `'../render'` 桶回指 `render/index.tsx` 即构成循环引用(`dev.md:53`)。合并后 `load.tsx` 第 1 行仍为 import、import 区间无空行/注释(`dev.md:55`)。
(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`,本任务不得添加。**
@ -334,21 +334,21 @@ Phase 9 · 任务 1(UI 接口适配)· 四个计划中的第 1 个 —— **
</action>
<reversibility rating="reversible">主要文件为纯位移;`controller.tsx` 去重与 `background.tsx` 接口归属调整均在包内、导出面不变(`MainBackgroundProps` 仍经桶透出),`git revert` 可整体回退;未引入新命名。</reversibility>
<verify>
<automated>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')"</automated>
<automated>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(/(export\s+(?:const|function|class)\s+MainBackground\b)|(export\s+(?:const|function|class)\s+MainBackgroundUI\b)|(export\s+interface\s+MainBackgroundProps\b)|(\bmainBackgroundProps\b)/.test(c))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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "OLD controller.tsx REMAINS" / "DEDUP FAILED"(去重未完成:controller 仍声明 `MainBackground*` 或未从 `./background` 取;background 仍从 `../render` 引入或未迁入 `MainBackgroundProps`)</fails_when>
<automated>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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PROPS BASE FAILED"(`load.tsx` / `title.tsx` 仍引用 `IUIDefaultPropsBase`、未改用 `IUIPropsBase`,或丢失 `props.state` 读法)</fails_when>
<automated>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(f.indexOf(\"from '../render/use'\")<0)bad.push(n+' missing ../render/use import');if(/from '\.\.\/render'/.test(f))bad.push(n+' still imports from render barrel')}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 + render/use leaf import')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "PROPS BASE FAILED"(`load.tsx` / `title.tsx` 仍引用 `IUIDefaultPropsBase`、未改用 `IUIPropsBase`、丢失 `props.state` 读法,或未改指 `'../render/use'` / 仍从 `'../render'` 桶导入)</fails_when>
<automated>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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "BARREL MISSING" / "NON-LOCAL EXPORT"(新桶缺失条目或出现非 `./` 越界导出,违反 D-69)</fails_when>
</verify>
<acceptance_criteria>
- `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/load.tsx` / `title.tsx` 无 `IUIDefaultPropsBase` 引用、改用 `IUIPropsBase`,保留 `props.state.loader` / `props.state.materials` 读法;两文件的 `render` 能力导入改指 `'../render/use'`(不经 `'../render'` 桶)
- `src/ui/index.ts` 含 7 条 `./` 同目录导出且无越界导出
- 无计划外文件被改
</acceptance_criteria>
<done>`controller.tsx` 去重迁移 + `background.tsx` 断链修正 + `load.tsx` / `title.tsx` props 基类型修正全部完成;barrel 骨架(7 条同目录导出)就位;旧文件已删</done>
<done>`controller.tsx` 去重迁移 + `background.tsx` 断链修正 + `load.tsx` / `title.tsx` props 基类型修正与 `'../render/use'` 说明符改写全部完成;barrel 骨架(7 条同目录导出)就位;旧文件已删</done>
</task>
</tasks>
@ -371,14 +371,16 @@ Phase 9 · 任务 1(UI 接口适配)· 四个计划中的第 1 个 —— **
| 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-06 | Tampering | 循环引用(`src/ui` 经 `'../render'` 桶回指 `render/index.tsx`) | medium | mitigate | 迁移文件 `render` 能力导入一律直连叶子模块(`'../render/use'`);Task 1/2/3 门禁断言无 `from '../render'`;`dev.md:53` |
| T-09-01-SC | Tampering | npm/pip/cargo installs | low | accept | 本计划无任何依赖安装、不执行 `pnpm i`;无供应链面 |
</threat_model>
<verification>
- **文件位移门禁**:四个模块 `src/ui/{save,statistics,viewmap,controller}.tsx` 在场,对应旧 `render/ui/` 文件不存在(不得双份)。
- **说明符门禁**:迁移文件无旧层级说明符(`'../use'` / `'../utils'` / `'../../shared'` / `'../../core'`),改用 `'../render'` / `'../render/utils'` / `'../shared'`。
- **说明符门禁**:迁移文件无旧层级说明符(`'../use'` / `'../utils'` / `'../../shared'` / `'../../core'`),改用 `'../render/use'` / `'../render/utils'` / `'../shared'`。
- **去重门禁**:`MainBackground` / `MainBackgroundUI` / `MainBackgroundProps` 仅由 `background.tsx` 定义;`controller.tsx` 从 `./background` 取 `MainBackgroundUI`。
- **props 基类型门禁**:`load.tsx` / `title.tsx` 无 `IUIDefaultPropsBase` 引用、改用 `IUIPropsBase`;保留 `props.state` 读法。
- **桶/循环门禁**:本计划迁移的 `src/ui/{save,viewmap}.tsx` 与修正的 `src/ui/{load,title}.tsx` 的 `render` 能力导入一律直连叶子模块(`'../render/use'`),**不得**出现 `from '../render'`(`src/render/index.tsx` 桶)——避免与 `render/index.tsx` 的 `import ... from '../ui'` 构成循环引用(`dev.md:53`)。
- **D-69 桶门禁**:`src/ui/index.ts` 全部 `export *` 均为 `./` 同目录。
- **范围门禁**:`git status --porcelain -- packages packages-user src` 除计划内条目外零漂移;`material/` / `packages/` / `src/` porcelain 为空。
- **CRLF 门禁** 与 **人工复核**(逐条打开当日源码确认门禁结论,写入 SUMMARY)。

View File

@ -38,13 +38,14 @@ 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/main.tsx` 用 `'../shared'` / `'../render/renderer'` / `'../core'`;`src/ui/settings.tsx` 用 `'../shared'` / `'../render/utils'` / `'../render/use'` / `'../core'`;`src/ui/statusBar.tsx` 用 `'../shared'` / `'../core'`;`src/ui/toolbar.tsx` 用 `'../shared'` / `'../render/use'` / `'../core'`。四文件内**不得**残留 `'../../shared'` / `'../use'` / `'../utils'` / `'../renderer'` / `'../../core'`。"
- "**循环引用防线(`dev.md:53` 硬约束,本计划收口)**:`src/ui/**` 内**任一**文件**不得**从 `'../render'`(`src/render/index.tsx` 桶)导入;凡需 `render/use.ts` 能力(`useKey` / `transitioned` / `transitionedColor` / `ITransitionedController`)一律直连 `'../render/use'`,需 `render/renderer.ts` 的 `using` 直连 `'../render/renderer'`。据此 `src/render/index.tsx`(`import ... from '../ui'`)→ `src/ui/**` → `src/render/use.ts` / `src/render/renderer.ts` 为无环 DAG,不再构成 `render/index.tsx ↔ src/ui` 循环(`src/render/action.ts` 的 `from '../ui'` 亦仅单向汇入 `ui` 桶,无回边)。"
- "**`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"
provides: "迁移后的主场景 UI(`main-scene`):`MainSceneUI` / `MainScene`;相对路径 `'../shared'` / `'../render/renderer'` / `'../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
@ -160,7 +161,7 @@ Phase 9 · 任务 1 · 第 2 个计划 —— **D-10:`render/ui/` → `src/ui/
| # | 文件 | 现状 | 目标 |
|---|---|---|---|
| 1 | `src/ui/{main,settings,statusBar,toolbar}.tsx` | 仅存在于 `render/ui/` | 迁至 `src/ui/`,相对说明符改指 `'../shared'` / `'../render'` / `'../render/utils'` / `'../core'`;旧文件删除(**保留 `client` / `state` 读取,消除归 09-03**) |
| 1 | `src/ui/{main,settings,statusBar,toolbar}.tsx` | 仅存在于 `render/ui/` | 迁至 `src/ui/`,相对说明符改指 `'../shared'` / `'../render/renderer'` / `'../render/use'` / `'../render/utils'` / `'../core'`(`render` 能力经叶子模块直连,不经 `'../render'` 桶);旧文件删除(**保留 `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'` |
@ -210,16 +211,16 @@ Phase 9 · 任务 1 · 第 2 个计划 —— **D-10:`render/ui/` → `src/ui/
<precondition>Task 0 已闭环:用户明确回复「可以执行」四个字;`09-01` 的产物在场</precondition>
<files>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(删除)</files>
<read_first>
- 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/main.tsx(整文件 312 行;须改说明符:`:20` `'../../shared'`→`'../shared'`、`:31` `import { using } from '../renderer'`→`'../render/renderer'`(直连 `render/renderer.ts` 叶子模块,不经 `'../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/use'`(直连 `render/use.ts` 叶子模块,不经 `'../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/toolbar.tsx(整文件 403 行;须改说明符:`:16` `'../use'`→`'../render/use'`(直连 `render/use.ts` 叶子模块,不经 `'../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、不转发导出)
</read_first>
<action>
(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:不改、不登记为待办)。
(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/use'`、`'../utils'`→`'../render/utils'`、`'../renderer'`→`'../render/renderer'`、`'../../core'`→`'../core'`;同目录的 `'./statusBar'` / `'./toolbar'` / `'./controller'` / `'./save'` / `'./settings'` / `'./viewmap'` / `'./statistics'` 保持原样。**`render` 能力一律直连叶子模块(`render/use.ts` / `render/renderer.ts`),不得改指 `'../render'` 桶**——`src/render/index.tsx:6` 会 `import ... from '../ui'`,经桶回指即构成循环引用(`dev.md:53`)。**逐字保留**每一处 `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';` 保持原样。
@ -229,14 +230,14 @@ Phase 9 · 任务 1 · 第 2 个计划 —— **D-10:`render/ui/` → `src/ui/
</action>
<reversibility rating="reversible">纯文件位移 + 说明符改写,无接口行为 / 新命名;`git revert` 可整体回退。</reversibility>
<verify>
<automated>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')"</automated>
<automated>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)}const need={main:'\'../render/renderer\'',settings:'\'../render/use\'',toolbar:'\'../render/use\''};if(need[n]&&t.indexOf(need[n])<0)bad.push(n+' MISSING '+need[n])}}if(bad.length>0){console.error('MOVE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK 4 modules migrated, specifiers rewritten to leaf modules')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "MISSING" / "OLD REMAINS" / "STALE" / "MOVE FAILED"(文件未迁移 / 旧文件未删 / 残留旧层级说明符)</fails_when>
<automated>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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "BARREL MISSING" / "createUI MISSING" / "createMainController CALL MISSING"(新桶条目缺失或 `createUI` 未定义)</fails_when>
</verify>
<acceptance_criteria>
- `src/ui/{main,settings,statusBar,toolbar}.tsx` 在场;对应旧文件已删
- 四文件无 `'../../shared'` / `'../use'` / `'../utils'` / `'../renderer'` / `'../../core'` 残留
- 四文件无 `'../../shared'` / `'../use'` / `'../utils'` / `'../renderer'` / `'../../core'` 残留;`render` 能力经 `'../render/use'`(settings/toolbar)与 `'../render/renderer'`(main)叶子模块直连,无 `from '../render'` 桶导入
- `client` / `state` / `core.*` / legacy / 注释逐字保留(未做 D-11 改动)
- `src/ui/index.ts` 含 11 条 `./` 同目录导出 + `createUI()`
- 无计划外文件被改
@ -267,7 +268,7 @@ Phase 9 · 任务 1 · 第 2 个计划 —— **D-10:`render/ui/` → `src/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) 的文件。
(6) **D-69 桶门禁 + 循环引用门禁与回归:** 断言 `src/ui/index.ts`、`src/render/index.tsx`、`src/index.ts` 内每一条 `export * from` 均为 `./` 前缀。**循环引用门禁:** 断言 `src/ui/**` 内**任一** `.ts`/`.tsx` 文件均无 `from '../render'`(`src/render/index.tsx` 桶)——`render` 能力必须经叶子模块(`'../render/use'` / `'../render/renderer'` / `'../render/utils'`)直连。该门禁保证 `render/index.tsx → ui` 与 `ui → render/index.tsx` 两条边不同时存在,从而消解 `render/index.tsx ↔ src/ui` 循环(`dev.md:53`;`release the edge`)。另运行 `pnpm check:circular`(`madge --circular src/main.ts`)并如实记录:若报告与 `render/index.tsx ↔ src/ui` 无关的既有循环,记录、不阻塞;若报告二者之间的循环,本计划判为失败。最后运行 `pnpm test:ci` 作回归(失败归因规则见 Task 0 呈报与 assumptions)。CRLF 收口;本任务只允许改 (1)..(5) 的文件。
</action>
<reversibility rating="reversible">引用改写 + 目录删除均在 git 追踪范围内,`git revert` 可整体回退;未引入新命名(`./ui` 承接为既有导出面搬迁)。</reversibility>
<verify>
@ -275,6 +276,8 @@ Phase 9 · 任务 1 · 第 2 个计划 —— **D-10:`render/ui/` → `src/ui/
<fails_when>non-zero exit, 或 stderr 含 "render/ui DIR STILL PRESENT" / "OLD REMAINS" / "stale" / "not rewired" / "NOT removed" / "missing" / "CLOSURE FAILED"(旧目录未删、消费者未改写、或 `.ui` 转发未删 / 包根 `./ui` 未加)</fails_when>
<automated>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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "D69 VIOLATION"(任一 barrel 含非 `./` 前缀的 `export * from`)</fails_when>
<automated>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').replace(/\r\n/g,'\n');if(/from '\.\.\/render'/.test(t))bad.push(f)}if(bad.length>0){console.error('RENDER BARREL IMPORT :: '+bad.join(','));process.exit(1)}console.log('OK no src/ui file imports the render barrel (cycle edge removed)')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "RENDER BARREL IMPORT"(`src/ui/**` 仍有文件从 `'../render'` 桶导入,`render/index.tsx ↔ src/ui` 循环未消解,违反 `dev.md:53`)</fails_when>
<automated>pnpm test:ci</automated>
<fails_when>`pnpm test:ci` 报告任一失败测试,且该失败可归因于本计划改动(非用户既有未提交改动或环境问题)</fails_when>
</verify>
@ -283,6 +286,7 @@ Phase 9 · 任务 1 · 第 2 个计划 —— **D-10:`render/ui/` → `src/ui/
- `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 全 `./`)
- 循环引用门禁通过(`dev.md:53`):`src/ui/**` 无 `from '../render'` 桶导入(`render` 能力经 `'../render/use'` / `'../render/renderer'` / `'../render/utils'` 直连);`pnpm check:circular` 未报告 `render/index.tsx ↔ src/ui` 循环
- `pnpm test:ci` 结果如实记录(失败若归因用户改动则不阻塞)
- 无计划外文件被改
</acceptance_criteria>
@ -309,13 +313,15 @@ Phase 9 · 任务 1 · 第 2 个计划 —— **D-10:`render/ui/` → `src/ui/
| 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-06 | Tampering | 循环引用(`render/index.tsx` 经 `'../ui'` 引入 UI,UI 又经 `'../render'` 桶回指 `render/index.tsx`) | high | mitigate | `src/ui/**` 的 `render` 能力导入强制直连叶子模块(`'../render/use'` / `'../render/renderer'`);Task 2 门禁断言 `src/ui/**` 无 `from '../render'`;`pnpm check:circular` 记录;`dev.md:53` |
| T-09-02-SC | Tampering | npm/pip/cargo installs | low | accept | 无依赖安装、不执行 `pnpm i` |
</threat_model>
<verification>
- **目录门禁**:`packages-user/client-modules/src/render/ui/` 不存在;11 个旧文件全部删除;`src/ui/` 为唯一来源。
- **说明符门禁**:4 个迁移文件无旧层级说明符,改用 `'../shared'` / `'../render'` / `'../render/utils'` / `'../core'`。
- **说明符门禁**:4 个迁移文件无旧层级说明符,改用 `'../shared'` / `'../render/renderer'` / `'../render/use'` / `'../render/utils'` / `'../core'`(`render` 能力经叶子模块直连,不经 `'../render'` 桶)。
- **桶门禁**:`src/ui/index.ts` 11 条同目录导出 + `createUI`;`src/render/index.tsx` 不含 `./ui` 转发;`src/index.ts` 含 `./ui`;三 barrel D-69 通过。
- **循环引用门禁**:`src/ui/**` 无 `from '../render'`(桶)导入;`render` 能力经 `'../render/use'` / `'../render/renderer'` / `'../render/utils'` 叶子模块直连;`render/index.tsx ↔ src/ui` 循环消解(`dev.md:53`)。`pnpm check:circular` 已运行并记录。
- **消费者门禁**:`render/index.tsx` / `render/action.ts` / `client.ts` 说明符改写到位。
- **范围门禁**:`git status --porcelain -- packages packages-user src` 除计划内条目外零漂移;`material/` / `packages/` 为空。
- **回归门禁**:`pnpm test:ci`(归因规则见 assumptions)。
@ -325,6 +331,7 @@ Phase 9 · 任务 1 · 第 2 个计划 —— **D-10:`render/ui/` → `src/ui/
<success_criteria>
- `render/ui/` 整目录删除,`src/ui/` 成为唯一 UI 来源,无双份。
- 三处跨目录消费者改写到位;公共面经包根 `./ui` 承接、不缩水;D-69 桶边界成立。
- `render/index.tsx ↔ src/ui` 无循环引用(`src/ui/**` 的 `render` 能力经叶子模块直连,`dev.md:53`)。
- 迁移逐字保留既有行为与注释(未做 D-11)。
- `pnpm test:ci` 已运行并记录;零范围外改动。
</success_criteria>

View File

@ -23,22 +23,22 @@ must_haves:
- "**本计划的第一个 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(() => ...)`)。"
- "**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(() => ...)`),并**必须**在其渲染树中向 `<LeftStatusBar>`(`main.tsx:228`)/ `<RightStatusBar>`(`main.tsx:274`)传入 `state={props.state}`,否则嵌套状态栏读取 `p.state.materials` 时 `props.state === undefined` 将抛错。"
- "**阻断项一律不替换、仅登记(不发明接口 / 不臆测)**:`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` 阻断项保持不动"
provides: "对齐后的主场景 UI:`props.state` 读取 `mainMapRenderer` / `mainMapExtension` / `flags` / `hero.attribute`;新增 `MainSceneProps extends IUIPropsBase` + props 声明;**渲染树向 `<LeftStatusBar>` / `<RightStatusBar>` 传 `state={props.state}`**(嵌套透传,D-11);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`"
provides: "对齐后的状态栏:`props.state.materials` 替换 `client.materials`;`StatusBarProps` 携带 `state`;`state` 由 `MainScene` 经 props 注入(`LeftStatusBar` / `RightStatusBar` 为嵌套组件、不经容器注入),并向上层透传 `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/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:
@ -53,7 +53,7 @@ must_haves:
- "本计划**只做 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。"
- "不得修改任何既有 jsDoc 与注释(AGENTS.md);除(`client`→`props.state` 替换、props 声明新增、嵌套 `state` 透传新增、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` 的条目。"
@ -141,7 +141,7 @@ Phase 9 · 任务 1 · 第 3 个计划 —— **D-11:把迁移后的 UI 对齐
- .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/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 格式)
@ -159,7 +159,7 @@ Phase 9 · 任务 1 · 第 3 个计划 —— **D-11:把迁移后的 UI 对齐
⑤ **嵌套 `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。
⑥ **大致的源 → 目标改动:** `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` 保持;渲染树向 `<LeftStatusBar>` / `<RightStatusBar>` 传 `state={props.state}`(嵌套透传)。`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`。**
@ -211,7 +211,7 @@ Phase 9 · 任务 1 · 第 3 个计划 —— **D-11:把迁移后的 UI 对齐
- packages-user/client-modules/src/ui/settings.tsx(整文件;`:28` `import { client } from '../core';`(09-02 迁移后层级);`:30-37` `MainSettingsProps extends Partial<ChoicesProps>, 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(注入面证据)
- packages/system/src/ui/container.tsx:32-43(注入面证据)
- dev.md(注释规范 / TODO 格式 / 不改既有注释 / CRLF)
</read_first>
<action>
@ -243,14 +243,14 @@ Phase 9 · 任务 1 · 第 3 个计划 —— **D-11:把迁移后的 UI 对齐
<precondition>Task 1 已完成(`settings.tsx` 已对齐,证明 props 机制可行)</precondition>
<files>packages-user/client-modules/src/ui/main.tsx, packages-user/client-modules/src/ui/statusBar.tsx, packages-user/client-modules/src/ui/toolbar.tsx</files>
<read_first>
- 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/main.tsx(整文件;`:8` 的 `defineComponent(() => {...})` 无 props;`:28` `import { state } from '@user/data-state';`(**阻断项,保留**);`:29` `import { mainUIController } from './controller';`(**阻断项,保留**);`:31` `import { using } from '../render/renderer';`(**阻断项,保留**;09-02 已将说明符由 `'../renderer'` 迁至 `'../render/renderer'` 叶子模块);`: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`;`:228-232` `<LeftStatusBar>`、`:274-278` `<RightStatusBar>`(渲染树中的嵌套组件,均须新增 `state={props.state}` 透传,否则其 `p.state.materials` 抛错))
- packages-user/client-modules/src/ui/statusBar.tsx(整文件;`:15` `client` 导入;`:96-104` `StatusBarProps<T> extends DefaultProps` + `statusBarProps` `props: ['loc','status','hidden']`;`:119` `client.materials`;`:380` `openViewMap(mainUIController, ...)`(**阻断项,保留**);`:450-453` `<MixedToolbar loc=... status=... />`)
- 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 / 无 `?.`)
</read_first>
<action>
(1) **`main.tsx`:** 新增接口 `MainSceneProps extends IUIPropsBase {}`(`IUIPropsBase` 自 `./types` 引入)与 `const mainSceneProps = { props: ['controller','instance','state'] } satisfies SetupComponentOptions<MainSceneProps>;`;把 `defineComponent(() => {...})` 改为 `defineComponent<MainSceneProps>(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`)逐字保留。
(1) **`main.tsx`:** 新增接口 `MainSceneProps extends IUIPropsBase {}`(`IUIPropsBase` 自 `./types` 引入)与 `const mainSceneProps = { props: ['controller','instance','state'] } satisfies SetupComponentOptions<MainSceneProps>;`;把 `defineComponent(() => {...})` 改为 `defineComponent<MainSceneProps>(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-11 必需,属接口对齐、非 UI 形态改动):** 在返回的渲染树中,向 `:228-232` 的 `<LeftStatusBar>` 与 `:274-278` 的 `<RightStatusBar>` 各新增 `state={props.state}`;二者为嵌套组件、不经容器注入 `state`,不传则其 `p.state.materials` 因 `props.state === undefined` 抛错。**D-13:** 在 `:98-99`(hideStatusBar)、`:119-121`(poison/weak/curse)、`:132`(hard)三处 flags 读取就近各新增 `// TODO:`(说明 flags 更新未接),不新增监听。其余(`reactive` 状态、`onUnmounted`、`renderMapMisc`、事件回调、`MainSceneUI`,以及渲染树中**除上述两处 `state={props.state}` 新增外的部分**)逐字保留。
(2) **`statusBar.tsx`:** 把 `StatusBarProps<T>` 基类型改为 `IUIPropsBase`(保留 `<T>` 泛型与 `loc`/`status`/`hidden` 成员),`statusBarProps` 的 `props` 数组改为 `['loc','status','hidden','state']`;删除 `:15` 的 `client` 导入;`:119` 的 `const materials = client.materials;` → `const materials = p.state.materials;`。`:450-453` 渲染 `<MixedToolbar>` 时新增 `state={p.state}` 透传。**保留不动**:`:380` 的 `openViewMap(mainUIController, ...)`(阻断项)。其余逐字保留。
@ -262,15 +262,16 @@ Phase 9 · 任务 1 · 第 3 个计划 —— **D-11:把迁移后的 UI 对齐
<verify>
<automated>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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CLIENT REMAINS"(`src/ui/**` 任一文件仍从 `src/core` 引入 `client` 或使用 `client.*`)</fails_when>
<automated>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')"</automated>
<automated>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.match(/state=\{props\.state\}/g)||[]).length<2)bad.push('main does not thread state to LeftStatusBar/RightStatusBar');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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "ALIGN FAILED"(`main` 的 props 声明 / 6 类替换 / TODO 缺失,或 `statusBar` / `toolbar` 的 `materials` 替换、props 基类型、`state` 透传缺失)</fails_when>
<automated>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')"</automated>
<automated>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/renderer'\")<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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "BLOCKED ITEMS CHANGED"(阻断项被误替换:`state`(data-state) / `using` / `mainUIController` 读取被改动)</fails_when>
</verify>
<acceptance_criteria>
- `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` 透传给子工具栏
- `main.tsx` 渲染树向 `<LeftStatusBar>` / `<RightStatusBar>` 传 `state={props.state}`(与为二者新增 `state` prop 配套,满足嵌套透传)
- 阻断项(`state`(data-state) / `using` / `mainUIController` / `sceneController`)保持原读取不动
- legacy / `core.*` / `ItemState` 只报告,未修改;既有注释逐字保留
- 无计划外文件被改
@ -304,7 +305,7 @@ Phase 9 · 任务 1 · 第 3 个计划 —— **D-11:把迁移后的 UI 对齐
<verification>
- **零 `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`;嵌套透传在场。
- **props 门禁**:`main.tsx` 新增 `MainSceneProps extends IUIPropsBase` + props 声明;`statusBar.tsx` / `toolbar.tsx` / `settings.tsx` props 基类型为 `IUIPropsBase` 且含 `state`;嵌套透传在场(`main.tsx` → `<LeftStatusBar>` / `<RightStatusBar>` 的 `state={props.state}`;`statusBar.tsx` → `<MixedToolbar>` 的 `state={p.state}`;`toolbar.tsx` → 子工具栏的 `state={props.state}`)。
- **阻断项门禁**:`state`(data-state) / `using` / `mainUIController` / `sceneController` 读取保持不动。
- **D-13 门禁**:`main.tsx` ≥3 处、`settings.tsx` ≥2 处 `// TODO:`。
- **范围门禁** 与 **CRLF 门禁** + **人工复核**(结论写入 SUMMARY)。

View File

@ -171,7 +171,7 @@ Phase 9 · 任务 1 · 第 4 个计划 —— **D-12:单独实现一个响应
- dev.md(类须 interface 先行再 implements;成员显式类型;不写 getter/setter;引入置首;CRLF)
</read_first>
<action>
(1) **新建文件:** 在 `packages-user/client-modules/src/ui/` 新建文件,文件名取 **Task 0 用户批准的名字**(规划默认占位 = `heroAttr.ts`;仅在用户批准后可沿用;未获批准**停下询问**)。文件内先 `export interface <接口名>` 声明契约,再 `export class <类名> implements <接口名>`(`dev.md:100`),类成员显式声明类型(`dev.md:91`)。
(1) **新建文件:** 在 `packages-user/client-modules/src/ui/` 新建文件,文件名取 **Task 0 用户批准的名字**(规划默认占位 = `heroAttr.ts`;仅在用户批准后可沿用;未获批准**停下询问**)。文件内先 `export interface <接口名>` 声明契约,再 `export class <类名> implements <接口名>`(`dev.md:100`),类成员显式声明类型(`dev.md:91`)。本任务门禁据「`src/ui/` 内除 `index.ts` / `types.ts` / `func.ts` 外的唯一 `.ts` 文件」**自动定位**该文件,故批准的基名不影响门禁匹配(不得再新增其它 `.ts` 文件)。
(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)。
@ -181,8 +181,8 @@ Phase 9 · 任务 1 · 第 4 个计划 —— **D-12:单独实现一个响应
</action>
<reversibility rating="reversible">新增一个包内实现层文件 + 一行桶导出;`git revert` 可整体回退;未改任何既有公共契约。</reversibility>
<verify>
<automated>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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "NEW FILE MISSING" / "HERO ATTR FILE FAILED" / "ILlegal name IHeroAttr"(新文件缺失、缺 interface/class/reactive/addHook/unload/getFinalAttribute,或误用 `IHeroAttr`)</fails_when>
<automated>node -e "const fs=require('fs');const dir='packages-user/client-modules/src/ui';const known=new Set(['index.ts','types.ts','func.ts']);const cand=fs.readdirSync(dir).filter(f=>f.endsWith('.ts')&&!known.has(f));if(cand.length!==1){console.error('EXPECTED EXACTLY ONE NEW .ts FILE (the approved hero-attr object), GOT :: '+JSON.stringify(cand));process.exit(1)}const target=dir+'/'+cand[0];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 ('+cand[0]+'): interface+class+reactive+addHook+unload')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "EXPECTED EXACTLY ONE NEW .ts FILE" / "HERO ATTR FILE FAILED" / "ILlegal name IHeroAttr"(未找到唯一新 `.ts` 文件(用户批准名)、缺 interface/class/reactive/addHook/unload/getFinalAttribute,或误用 `IHeroAttr`)</fails_when>
</verify>
<acceptance_criteria>
- 新文件在场(文件名 = 用户批准者;规划默认 `heroAttr.ts`)
@ -200,11 +200,11 @@ Phase 9 · 任务 1 · 第 4 个计划 —— **D-12:单独实现一个响应
<files>packages-user/client-modules/src/ui/main.tsx</files>
<read_first>
- 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` `<LeftStatusBar status={leftStatus} ...>`)
- packages-user/client-modules/src/ui/heroAttr.ts(Task 1 的产物,确认构造签名与字段名)
- packages-user/client-modules/src/ui/&lt;APPROVED_NAME&gt;.ts(Task 1 的产物,文件名取 Task 0 用户批准者;规划默认占位 `heroAttr.ts`;确认构造签名与字段名)
- dev.md(不改既有注释;CRLF;无 `?.`)
</read_first>
<action>
(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()`。**不得**改变 `<LeftStatusBar status={...}>` 对外的形态(D-14)。
(1) **接线:** 在 `main.tsx` 顶部从 `'./<APPROVED_NAME>'` 直连导入新响应式对象(接口 / 类名取用户批准者;**必须直连该文件**,不得经桶导入——本任务门禁据该文件名自动定位并断言 `main.tsx` 含 `from './<APPROVED_NAME>'`)。在组件 setup 内用该对象承载勇士属性:创建该对象(传入 `props.state`),以它的属性字段替换 `leftStatus` 中对应的 9 个属性字段读取(`leftStatus.atk/hp/hpmax/mana/manamax/def/mdef/money/exp` 改由新对象提供);把原 `attributeHook = props.state.hero.attribute.addHook({...})` 的订阅交给新对象(新对象内部持有钩子),`onUnmounted` 改为调用新对象的 `unload()`。**不得**改变 `<LeftStatusBar status={...}>` 对外的形态(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`,须保证二者仍有触发路径)。
@ -212,7 +212,7 @@ Phase 9 · 任务 1 · 第 4 个计划 —— **D-12:单独实现一个响应
</action>
<reversibility rating="reversible">组件内接线调整;`git revert` 可整体回退;未改公共契约。</reversibility>
<verify>
<automated>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')"</automated>
<automated>node -e "const fs=require('fs');const dir='packages-user/client-modules/src/ui';const known=new Set(['index.ts','types.ts','func.ts']);const cand=fs.readdirSync(dir).filter(f=>f.endsWith('.ts')&&!known.has(f));const t=fs.readFileSync(dir+'/main.tsx','utf8').replace(/\r\n/g,'\n');const bad=[];if(cand.length!==1)bad.push('cannot locate the new hero-attr .ts file');else{const base=cand[0].replace(/\.ts$/,'');if(t.indexOf(\"'./\"+base+\"'\")<0)bad.push('new object not imported in main.tsx from ./'+base)}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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "WIRING FAILED"(新对象未接线 / 未 `unload` / 丢失 `updateDataFallback` / 误改阻断项 / 丢失非属性字段)</fails_when>
<automated>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')"</automated>
<fails_when>non-zero exit, 或 stderr 含 "CLIENT REMAINS"(接线使 `src/ui` 重新出现 `client` 单例用法)</fails_when>

View File

@ -38,6 +38,16 @@
- **D-08(拆分):** 收尾 `client-modules` → `client-base` / `client-modules` 的依赖拆分。已有部分完成,需按实际依赖关系盘点剩余未拆内容,再逐任务迁移。
- **D-09(对齐):** 实现层与架构层接口对齐——含 UI 系统重构后的 UI 实现对齐,以及 `@motajs/legacy-ui` 等 legacy 实现向新接口的移植。
### 任务 1(当前规划单元:UI 接口适配)
> 本节界定**本次规划运行**的范围:仅规划任务 1,不规划 Phase 9 的其余内容;后续任务由用户再派发时追加计划。
- **D-10(目录移动):** 将 `packages-user/client-modules/src/render/ui/` 移动到 `packages-user/client-modules/src/ui/`(减少包内嵌套),并同步改写所有引用该路径的导入。纯移动 + 引用改写,不改变行为。
- **D-11(接口对齐):** 将移动后的 UI 实现对齐到新 UI 系统(`packages/system/src/ui`):组件通过系统提供的「经 props 获取主对象 `IClientBase`」机制获取主对象,不得使用 `client` 单例。把旧的 `client` 相关操作逐一替换为 `IClientBase` 上可用的新接口;**计划必须给出「旧接口 → 新接口」映射表**。凡 `client` 上不存在对应接口的操作,先向用户反馈,不得自造接口。
- **D-12(勇士属性):** 单独实现一个 `IHeroAttr` 响应式对象,监听勇士属性变化并据此更新(命名待用户批准;注意与既有 `IHeroAttribute` 一族区分)。
- **D-13(flags):** flags 尚无监听系统,本任务不处理 flags 的更新,仅在代码中标注 `TODO`。
- **D-14(纯适配):** 本任务不引入 UI 形态/视觉改动;不做 `@motajs/legacy-ui` 移植(属后续任务)。
### the agent's Discretion
- 无。用户明确未授权 AI 自行决定 UI 形态或任务范围。

View File

@ -0,0 +1,401 @@
# Phase 9: 最小浏览器可运行版本 - Pattern Map
**Mapped:** 2026-10-09
**规划范围:** 仅任务 1(UI 接口适配,D-10..D-14);不含 Phase 9 其余内容、不含 `@motajs/legacy-ui` 移植(D-14)
**Files analyzed:** 10 个 UI 实现文件(迁移 + 对齐)+ 4 个接线/barrel 修改点 + 1 个新建响应式对象 + 1 个待删目录
**Analogs found:** 全部有既有范式;唯一「无直接 analog」项 = D-12 的 `IHeroAttr` 响应式对象(仅可借 `main.tsx` 内联写法)
> **性质声明:** 本文件是 pattern map(只读产出),不修改任何生产代码;唯一写文件是本 `09-PATTERNS.md`。
> 所有生产路径均经 `git ls-files` 验证为 tracked source(`render/ui/**` 11 文件 + `src/ui/**` 6 文件均 tracked,无 gitignored 镜像路径)。
> **命名纪律(AGENTS.md):** 本文件出现的任何新公共/受保护/私有命名(尤其 D-12 的响应式对象名)均标注「命名待用户批准」,不得据此拍板。
---
## Scope Note(本 run 的边界)
| 项 | 值 |
|----|----|
| 当前规划单元 | 任务 1「UI 接口适配」(CONTEXT `## 任务 1`,D-10..D-14) |
| 迁移源 | `packages-user/client-modules/src/render/ui/**`(11 文件,全 tracked) |
| 迁移目标 | `packages-user/client-modules/src/ui/`(已存在 6 文件:`background.tsx` / `func.ts` / `index.ts` / `load.tsx` / `title.tsx` / `types.ts`,部分迁移先行) |
| 接口基准(只读) | `packages-user/client-base/src/types.ts`(`IClientBase` / `IGameUIPropsBase`)、`packages-user/data-{base,state}/src/types.ts`(`IStateBase` / `ICoreState`)、`packages/system/src/ui/**`(新 UI 系统) |
| 明确排除 | 任何 UI 形态/视觉改动、`@motajs/legacy-ui` 移植、flags 更新监听(D-13 仅标 TODO)、D-08 拆分剩余内容(属后续任务) |
| 交付物 | 本 `09-PATTERNS.md` |
---
## File Classification
### A. 迁移并接口对齐的 UI 实现(D-10 + D-11)
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|-------------------|------|-----------|----------------|---------------|
| `packages-user/client-modules/src/ui/main.tsx` ← `render/ui/main.tsx` | component | request-response(读状态 + 渲染) | `src/ui/title.tsx`(已迁移,`props.state` 读法) | exact(同目录同角色) |
| `packages-user/client-modules/src/ui/controller.tsx` ← `render/ui/controller.tsx` | controller/component | request-response | `src/ui/background.tsx`(已迁移,`GameUI` + `props`) | role-match |
| `packages-user/client-modules/src/ui/save.tsx` ← `render/ui/save.tsx` | component | request-response | `src/ui/load.tsx`(已迁移) | role-match |
| `packages-user/client-modules/src/ui/settings.tsx` ← `render/ui/settings.tsx` | component | request-response | `src/ui/title.tsx`(已迁移) | role-match |
| `packages-user/client-modules/src/ui/statistics.tsx` ← `render/ui/statistics.tsx` | component | transform(统计聚合) | `src/ui/load.tsx`(已迁移) | role-match |
| `packages-user/client-modules/src/ui/statusBar.tsx` ← `render/ui/statusBar.tsx` | component | request-response | `src/ui/title.tsx`(已迁移,`props.state.materials`) | exact(同 `client`→`props.state` 读点) |
| `packages-user/client-modules/src/ui/toolbar.tsx` ← `render/ui/toolbar.tsx` | component | request-response | `src/ui/title.tsx`(已迁移,`props.state.materials`) | exact(同 `client`→`props.state` 读点) |
| `packages-user/client-modules/src/ui/viewmap.tsx` ← `render/ui/viewmap.tsx` | component | request-response | `src/ui/load.tsx`(已迁移) | role-match |
| `packages-user/client-modules/src/ui/load.tsx`(已存在,需补齐 props 类型) | component | file-I/O(加载进度迭代) | 自身旧版 `render/ui/load.tsx` + 新系统 `packages/system/src/ui` | exact |
| `packages-user/client-modules/src/ui/title.tsx`(已存在,需补齐 import + `client`→`props.state`) | component | request-response | 自身旧版 `render/ui/title.tsx` | exact |
### B. 接线 / barrel 修改点
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|-------------------|------|-----------|----------------|---------------|
| `packages-user/client-modules/src/ui/index.ts` | barrel / wiring(`createUI`) | request-response | `render/ui/index.ts`(旧 barrel)+ `render/index.tsx:29-36`(旧 `createRender`) | role-match |
| `packages-user/client-modules/src/ui/types.ts` | contract / types | — | 自身既有 `IUIPropsBase` | exact |
| `packages-user/client-modules/src/ui/func.ts` | factory | request-response | `packages/system/src/ui/container.tsx`(props creator 消费端) | role-match |
| `packages-user/client-modules/src/ui/background.tsx` | component | request-response | 自身既有 + `render/ui/controller.tsx`(`MainBackgroundProps` 源) | role-match |
| `packages-user/client-modules/src/render/index.tsx` | composition root | event-driven | 自身既有 | exact |
| `packages-user/client-modules/src/client.ts` | composition root | event-driven | 自身既有 | exact |
| `packages-user/client-modules/src/render/action.ts` | wiring(hotkey → UI) | event-driven | 自身既有 | exact |
### C. 新建文件(D-12,命名待批准)
| New File | Role | Data Flow | Closest Analog | Match Quality |
|----------|------|-----------|----------------|---------------|
| `packages-user/client-modules/src/ui/<hero-attr>.ts`(文件名与 `IHeroAttr` 名**待用户批准**) | hook / composable | event-driven(`addHook` 监听) | `render/ui/main.tsx:66-90,141-150`(内联 `reactive` + `attribute.addHook` + `onUnmounted` 卸载) | no-exact(仓内无独立响应式属性包装先例) |
### D. 待删除(迁移后残留)
| File | 说明 |
|------|------|
| `packages-user/client-modules/src/render/ui/index.ts` | 旧 barrel;能力由 `src/ui/index.ts` 承接 |
| `packages-user/client-modules/src/render/ui/main.tsx` | 能力由 `src/ui/main.tsx` 承接 |
| `packages-user/client-modules/src/render/ui/controller.tsx` | 能力由 `src/ui/controller.tsx` + `src/ui/background.tsx` 承接 |
| `packages-user/client-modules/src/render/ui/load.tsx` | 能力由 `src/ui/load.tsx` 承接(已迁移) |
| `packages-user/client-modules/src/render/ui/save.tsx` | 能力由 `src/ui/save.tsx` 承接 |
| `packages-user/client-modules/src/render/ui/settings.tsx` | 能力由 `src/ui/settings.tsx` 承接 |
| `packages-user/client-modules/src/render/ui/statistics.tsx` | 能力由 `src/ui/statistics.tsx` 承接 |
| `packages-user/client-modules/src/render/ui/statusBar.tsx` | 能力由 `src/ui/statusBar.tsx` 承接 |
| `packages-user/client-modules/src/render/ui/title.tsx` | 能力由 `src/ui/title.tsx` 承接(已迁移) |
| `packages-user/client-modules/src/render/ui/toolbar.tsx` | 能力由 `src/ui/toolbar.tsx` 承接 |
| `packages-user/client-modules/src/render/ui/viewmap.tsx` | 能力由 `src/ui/viewmap.tsx` 承接 |
---
## Pattern Assignments
### 1. 目标 props 机制(所有 UI 组件的公共骨架)
**Analog:** `packages-user/client-modules/src/ui/title.tsx:60-64`(已迁移的「经 props 获取主对象」范本)
**机制源(只读基准):**
- `packages-user/client-base/src/types.ts:15-19` — `IGameUIPropsBase extends IUIDefaultPropsBase<T>` 携带 `readonly state: IClientBase`。
- `packages-user/client-modules/src/ui/func.ts:13-26` — `createUIPropsBase(state)` 返回 creator,产出 `{ instance, controller, state }`。
- `packages-user/client-modules/src/ui/types.ts:1-5` — 本地 `IUIPropsBase extends IUIDefaultPropsBase, IClientBaseExtended`(与 `IGameUIPropsBase` 同构)。
- `packages-user/client-modules/src/client.ts:128-130` — `sceneController` / `mainUIController` 各自 `setPropsBaseCreator(createUIPropsBase(this))`。
- `packages/system/src/ui/container.tsx:32-43` — 容器渲染时对每个实例调 `data.createPropsBase(v)` 并 `{...pb}` 展开到组件 props。
**Props 声明 + state 获取范式(照抄 `src/ui/title.tsx:60-68`):**
```tsx
export interface GameTitleProps extends DefaultProps, IUIDefaultPropsBase {}
const gameTitleProps = {
props: ['controller', 'instance', 'state']
} satisfies SetupComponentOptions<GameTitleProps>;
export const GameTitle = defineComponent<GameTitleProps>(props => {
const materials = props.state.materials; // ← 主对象经 props 获取,非 client 单例
// ...
});
```
**加载组件同款(`src/ui/load.tsx:25-32`):**
```tsx
export interface ILoadProps extends IUIDefaultPropsBase, DefaultProps {}
const loadSceneProps = {
props: ['controller', 'instance', 'state']
} satisfies SetupComponentOptions<ILoadProps>;
export const LoadScene = defineComponent<ILoadProps>(props => {
const loader = props.state.loader; // ← 旧 client.loader → props.state.loader
```
> **WIP 修正点(planner 须处理):** `src/ui/load.tsx:23` 现 `import { IUIDefaultPropsBase } from './types';`,但 `src/ui/types.ts` 只导出 `IUIPropsBase`(未导出 `IUIDefaultPropsBase`)⇒ 类型断链。目标写法应统一为 `props` 泛型 `extends IUIPropsBase`(本地)或 `IGameUIPropsBase`(`@user/client-base`),二选一后所有组件共用(见「Shared Patterns / Props 基类型统一」)。
---
### 2. `src/ui/main.tsx`(component, request-response)
**Analog:** `src/ui/title.tsx`(已迁移读法)+ 自身旧版 `render/ui/main.tsx`
**旧 import(`render/ui/main.tsx:1-33`,须改写):**
```tsx
import { ReplayingStatus } from './toolbar';
import { state } from '@user/data-state'; // ← data-state 单例
import { mainUIController } from './controller'; // ← 模块级单例
import { using } from '../renderer'; // ← 模块级单例
import { client } from '../../core'; // ← client 单例(本次要消除)
```
**Old → New 映射(本文件全部 `client` 读点):**
| 旧(`client.*` / 单例) | 旧位置 | 新(`props.state.*`,`IClientBase`) | 证据(新接口定义) |
|---|---|---|---|
| `client.mainMapRenderer` | `main.tsx:35` | `props.state.mainMapRenderer` | `client-base/src/types.ts:39` |
| `client.mainMapExtension` | `main.tsx:35` | `props.state.mainMapExtension` | `client-base/src/types.ts:43` |
| `client.flags` | `main.tsx:98` | `props.state.flags` | `data-base/src/types.ts:27` |
| `client.hero.attribute.getFinalAttribute(...)` | `main.tsx:102-110`(9 处) | `props.state.hero.attribute.getFinalAttribute(...)` | `data-base/src/hero/types.ts:921,928` + `:117` |
| `client.hero.attribute.addHook({ onUpdateAttribute })` | `main.tsx:141` | `props.state.hero.attribute.addHook({ onUpdateAttribute })` | `data-base/src/hero/types.ts:104-105`(`IReadonlyHeroAttribute extends IHookable`) |
| `state`(`@user/data-state` 单例) | `main.tsx:28,248` | `props.state`(同一 `IClientBase`,`extends ICoreState`) | `data-state/src/types.ts:26`;`client-base/src/types.ts:21` |
| `using`(模块级单例) | `main.tsx:31,155` | `props.state.using` | `client-base/src/types.ts:45` |
| `mainUIController`(模块级单例) | `main.tsx:29,281,284` | `props.state.mainUIController`(**切换与否待用户裁定**,见 Shared Patterns / 单例归属) | `client-base/src/types.ts:49` |
**生命周期/清理范式(`main.tsx:141-150`,新对象须照此卸载):**
```tsx
const attributeHook = client.hero.attribute.addHook({
onUpdateAttribute: () => {
updateStatus();
updateDataFallback();
}
});
onUnmounted(() => {
attributeHook.unload();
});
```
> 新版本将 `client.hero.attribute` 换为 `props.state.hero.attribute`;`addHook` 返回 `IHookController`(`.unload()` 契约源于 `@motajs/common` Hookable)。
**`map-render` 读点(`main.tsx:246-252`,注意既有 Phase 4 错配 M-08):**
```tsx
<map-render
renderer={mainMapRenderer}
// @ts-expect-error 需要重构
layerState={state.maps} // ← state 单例;目标 props.state.maps
extension={mainMapExtension}
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}
/>
```
> `state.maps` 为 `IMapState` 而 `layerState` 要求单层 `IGameMap`(Phase 4 04-RENDER-INTERFACE-AUDIT `#04-01-M-08` 已登记)。本任务只做单例→props 替换,**不解决该形状错配**(D-14 纯适配);保持 `@ts-expect-error` 原样,勿擅自扩范围。
**flags TODO 标记点(D-13):** `main.tsx:98-99`(`flags.getFieldValueDefaults('hideStatusBar', false)`)、`:119-121`(poison/weak/curse)、`:132`(hard)在替换为 `props.state.flags` 后,因 flags 尚无监听系统,更新不处理,须就近加 `// TODO:`(格式遵循 `dev.md:78`)。
---
### 3. `src/ui/title.tsx`(component, request-response)
**Analog:** 自身(已迁移)+ 旧版 `render/ui/title.tsx`;`client` 读点见 `render/ui/title.tsx:67`。
**已迁移读法(`src/ui/title.tsx:1-38,66-68` 状态):**
```tsx
import { ExitFullscreen, Fullscreen, SoundVolume } from '@user/client-base';
import { mainSetting, triggerFullscreen } from '@motajs/legacy-ui'; // ← legacy,超出本任务范围
import { saveLoad } from './save'; // ← 待迁移 save 模块
import { MainSceneUI } from './main'; // ← 待迁移 main 模块
import { adjustCover } from '@user/client-base';
// ...
export const GameTitle = defineComponent<GameTitleProps>(props => {
const materials = props.state.materials; // ← client.materials → props.state.materials
```
**必改点:**
- `render/ui/title.tsx:38` `import { client } from '../../core';` → 删除,改 `props.state.materials`(`:67`)。
- `src/ui/title.tsx:38` 现有 `import { IUIDefaultPropsBase } from './types';` 应为 `IUIPropsBase`(与 `load.tsx` 同类 WIP 类型断链)。
- `@motajs/legacy-ui`(`mainSetting` / `triggerFullscreen`,`src/ui/title.tsx:32`)为 legacy,**只报告不改**(D-14)。
---
### 4. `src/ui/statusBar.tsx` / `src/ui/toolbar.tsx`(component, request-response)
**Analog:** `src/ui/title.tsx`(`props.state.materials` 读法)
**`client.materials` 读点(须改写):**
| 文件:行 | 旧 | 新 |
|---|---|---|
| `render/ui/statusBar.tsx:15,119` | `import { client } from '../../core';` + `client.materials` | 删除 import,`props.state.materials` |
| `render/ui/toolbar.tsx:28,77,175` | `import { client } from '../../core';` + `client.materials`(两处) | 删除 import,`props.state.materials` |
**注意(toolbar):** `PlayingToolbar` / `ReplayingToolbar` 现声明为 `defineComponent<ToolbarProps>`(`render/ui/toolbar.tsx:72-76,172-173`),`ToolbarProps` 不含 `state`。要取 `props.state`,须把 props 泛型扩为携带主对象的基类型(见 Scripts / Props 基类型统一),并加 `'state'` 到 `props` 数组。此改动**属接口对齐,不属 UI 形态改动**(D-14)。
`statusBar.tsx` 内 `RightStatusBar` / `LeftStatusBar` 的 `StatusBarProps<T>` 同理需挂载主对象基类型。
`toolbar.tsx` 内 `mainUIController` 单例(`:23,94,97,115,120,126,195`)与 `render/action.ts` 的 `mainUIController` 单例归属:见 Shared Patterns / 单例归属(待用户裁定)。
---
### 5. `src/ui/settings.tsx`(component, request-response)
**Analog:** `src/ui/title.tsx`
**`client.flags` 读点(须改写):**
| 文件:行 | 旧 | 新 |
|---|---|---|
| `render/ui/settings.tsx:28,166,221` | `import { client } from '../../core';` + `const flags = client.flags;`(两处) | 删除 import,`props.state.flags` |
**legacy(只报告,不改):** `mainUi`(`@motajs/legacy-ui`,`render/ui/settings.tsx:17`)、`getVitualKeyOnce`(`:19`)。**flags TODO(D-13):** `settings.tsx:166-167` / `:221-222` 用 `flags.getFieldValue('__seed__')`,替换来源后加 `// TODO:` 注明 flags 更新未接(无监听系统)。
---
### 6. `src/ui/save.tsx` / `src/ui/statistics.tsx` / `src/ui/viewmap.tsx` / `src/ui/controller.tsx`(无 `client` 读点,纯迁移)
**Analog:** `src/ui/load.tsx`(已迁移文件的骨架)
- `render/ui/save.tsx`:**无 `client` 引用**(`git grep` 零命中);纯路径移动 + `import` 相对路径调整(`'../../shared'` → `'../shared'`,`'../use'` / `'../utils'` / `'../render'` 视目标层级调整)。
- `render/ui/statistics.tsx`:无 `client`;含 legacy `ItemState`(`@user/data-state`,`render/ui/statistics.tsx:11` + `@ts-expect-error`)+ 大量 `core.*`;本任务只迁移路径,**legacy 只报告不改**。
- `render/ui/viewmap.tsx`:无 `client`;含 `core.*` legacy(只报告)。
- `render/ui/controller.tsx`:`mainUIController` 单例 + `MainBackground` + `MainBackgroundUI` + `createMainController()`(`:11,20-33`)。`src/ui/background.tsx:3` 现 `import { MainBackgroundProps } from '../render';` —— 该类型定义在 **旧** `render/ui/controller.tsx:13-14`,迁移后须改指新 `./controller`(或把 `MainBackgroundProps` 落在新 controller / types)。这是**必处理的 import 断链**。
---
### 7. 新建 `IHeroAttr` 响应式对象(D-12,命名待批准)
**Analog(仅内联范式,无独立先例):** `render/ui/main.tsx:66-90`(`reactive` 状态对象)+ `:141-150`(`attribute.addHook` + `onUnmounted` 卸载)
**内联参照(`render/ui/main.tsx:66-90` 摘选):**
```tsx
const leftStatus: ILeftHeroStatus = reactive({
hp: 0, hpmax: 0, mana: 0, manamax: 0,
atk: 0, def: 0, mdef: 0, money: 0, exp: 0,
// ...
});
```
**监听参照(`render/ui/main.tsx:141-150`):**
```tsx
const attributeHook = props.state.hero.attribute.addHook({
onUpdateAttribute: () => { /* 同步 reactive 属性值 */ }
});
onUnmounted(() => attributeHook.unload());
```
**新接口证据(只读基准):**
- `packages-user/data-base/src/hero/types.ts:95-102` — `IHeroAttributeHooks.onUpdateAttribute(name, value)`。
- `packages-user/data-base/src/hero/types.ts:104-117` — `IReadonlyHeroAttribute extends IHookable<...>`,含 `getFinalAttribute(name)`。
- `packages-user/data-base/src/hero/types.ts:921-928` — `IHeroState.attribute: IReadonlyHeroAttribute<THero>`。
**约束(D-12 + AGENTS.md):**
- 命名(文件名、接口名、类名,含 `IHeroAttr` 是否沿用)**必须先向用户反馈并获批**后使用;注意与既有 `IHeroAttr`(`@user/data-common` 的**属性数据字典类型**,见 `data-base/src/types.ts:8,21` 泛型参数)及 `IHeroAttribute` 一族区分——**不得**复用/覆写既有 `IHeroAttr` 语义。
- 该类须遵循 `dev.md:100`「类必须先 `interface` 声明再 `implements`」;如需 `reactive` 包装,参照 `main.tsx` 的 `reactive(...)` 写法。
---
### 8. Barrel / 接线修改(D-10 引用改写)
**`src/ui/index.ts`(旧 `render/ui/index.ts:1-14` 的映射):**
```ts
// 旧 render/ui/index.ts
import { createMainController } from './controller';
export function createUI() { createMainController(); }
export * from './controller';
export * from './main';
// ... 共 8 条 export *
```
> **目标:** 新 `src/ui/index.ts` 须导出全部迁移模块(`controller` / `main` / `save` / `settings` / `statistics` / `statusBar` / `toolbar` / `viewmap` / `types` / `background` / `func`)。现 `src/ui/index.ts:1` 只有 `export * from './types';` ⇒ 补齐。`createUI()` 的存废取决于新系统接线(`render/index.tsx:31` 现调 `createUI()`),须在 planning 时对照 `client.ts:126-130` 的 `setPropsBaseCreator` 接线一并定夺。
**`src/render/index.tsx`(`git grep` 实测 3 处引用):**
| 行 | 旧 | 目标 |
|---|---|---|
| `:6` | `import { createUI } from './ui';` | `from '../ui'`(或按新 barrel 决策改) |
| `:11` | `import { LoadSceneUI } from './ui/load';` | `from '../ui/load'` |
| `:39` | `export * from './ui';` | **须评估 D-69**:`../ui` 非「同目录项」,barrel 硬约束下该转发应删除,改由 `client-modules/src/index.ts` 直连 `./ui` |
**`src/client.ts`(`git grep` 实测 2 处):**
| 行 | 旧 | 目标 |
|---|---|---|
| `:44` | `import { LoadSceneUI } from './render/ui/load';` | `from './ui/load'` |
| `:45` | `import { createUIPropsBase } from './ui/func';` | 不变(已是新路径) |
**`src/render/action.ts`(`git grep` 实测 1 处):**
| 行 | 旧 | 目标 |
|---|---|---|
| `:4-11` | `from './ui'` | `from '../ui'` |
**层级/import 相对路径规则(`dev.md:55` import 置首、无洞、无注释;`dev.md:52` 不转发导出):** 迁移后 `src/ui/*.tsx` 对共享常量的引用由 `'../../shared'` 变 `'../shared'`;对 `render` 能力的引用须指向**叶子模块** `'../render/use'`(`transitioned` 等,定义于 `render/use.ts:330`)或 `'../render/renderer'`(`mainRenderer` / `createApp` 等),**不得**经 `'../render'` barrel(`render/index.tsx`)—— `render/index.tsx` 自身 `import { createUI } from './ui'` / `export * from './ui'`,从 `src/ui/**` 再反向 `import ... from '../render'` 会形成 `render/index.tsx ↔ src/ui` 循环引用,违反 `dev.md:37`。`src/ui/load.tsx:18` 现有的 `import { transitioned } from '../render';` 即此类 barrel 引用,迁移/对齐时应改为 `from '../render/use'`(`transitioned` 实际归属)。须逐文件核对相对层级与符号归属,避免断链与环(`dev.md:37` 无循环引用)。
---
## Old → New Interface Mapping(D-11 要求,集中表)
> 本表为任务 1 的核心交付之一。**「新接口」列全部有 `IClientBase` 上的实证**;凡 `IClientBase` 无对应者单列「无对等(报告)」节,**不自造接口**。
| 旧(单例/全局) | 出现位置(迁移源) | 新接口(经 `props.state: IClientBase`) | 新接口定义证据 |
|---|---|---|---|
| `client.loader` | `render/ui/load.tsx:27,36` | `props.state.loader` | `data-state/src/types.ts:29`(`ICoreState.loader`) |
| `client.materials` | `render/ui/title.tsx:67`;`statusBar.tsx:119`;`toolbar.tsx:77,175` | `props.state.materials` | `client-base/src/types.ts:31` |
| `client.flags` | `render/ui/main.tsx:98`;`settings.tsx:166,221` | `props.state.flags` | `data-base/src/types.ts:27`(`IStateBase.flags`) |
| `client.hero.attribute.getFinalAttribute()` | `render/ui/main.tsx:102-110` | `props.state.hero.attribute.getFinalAttribute()` | `data-base/src/hero/types.ts:921,928,117` |
| `client.hero.attribute.addHook()` | `render/ui/main.tsx:141` | `props.state.hero.attribute.addHook()` | `data-base/src/hero/types.ts:104`(`IHookable`) |
| `client.mainMapRenderer` | `render/ui/main.tsx:35` | `props.state.mainMapRenderer` | `client-base/src/types.ts:39` |
| `client.mainMapExtension` | `render/ui/main.tsx:35` | `props.state.mainMapExtension` | `client-base/src/types.ts:43` |
| `state`(`@user/data-state` 单例) | `render/ui/main.tsx:28,248` | `props.state`(`IClientBase extends ICoreState`) | `client-base/src/types.ts:21`;`data-state/src/types.ts:26` |
| `using`(模块级单例) | `render/ui/main.tsx:31,155` | `props.state.using` | `client-base/src/types.ts:45` |
| `mainUIController`(模块级单例) | `render/ui/controller.tsx:11`;`main.tsx:29`;`statusBar.tsx:7`;`toolbar.tsx:23`;`render/action.ts:5` | `props.state.mainUIController`(**组件内可切换;非组件模块归属待裁定**) | `client-base/src/types.ts:49` |
| `sceneController`(模块级单例) | `render/scene.ts:3`;`render/index.tsx:8` | `props.state.sceneController`(组件内可切换;组合根归属待裁定) | `client-base/src/types.ts:47` |
### 无对等(报告,不造接口)
| 旧调用 | 位置 | 归属 | 处置 |
|---|---|---|---|
| `mainSetting` / `triggerFullscreen` / `mainUi` / `getVitualKeyOnce`(`@motajs/legacy-ui`) | `src/ui/title.tsx:32`;`render/ui/settings.tsx:17,19`;`render/ui/toolbar.tsx:14` | legacy UI | D-14:不移植,**只报告** |
| `generateBinary`(`@motajs/legacy-common`) | `render/ui/toolbar.tsx:20` | legacy common | **只报告** |
| `core.*`(含 `core.status.*` / `core.doSL` / `core.ui.closePanel` / `core.startGame` 等,遍布各 UI) | 全 UI 文件 | legacy 全局 | D-14 纯适配 + Phase 4 D-55:**只报告、不改、不登记为待办** |
| `ItemState`(`@user/data-state`,带 `@ts-expect-error`) | `render/ui/statistics.tsx:10-11,178` | 数据端缺失/改名 | **只报告**(Phase 4 `#04-01-M-07` 已登记) |
> **注意:** 上述 legacy 项均**不属于**「`client` 单例 → `IClientBase`」映射范畴;D-11 只要求替换 `client` 相关操作。planner 不得借任务 1 扩大范围处理 `core.*`(D-06 / D-14)。
---
## Shared Patterns
### Props 基类型统一(跨所有迁移组件)
**Source:** `packages-user/client-base/src/types.ts:15-19`(`IGameUIPropsBase`);`packages-user/client-modules/src/ui/types.ts:1-5`(`IUIPropsBase`);`packages-user/client-modules/src/ui/func.ts:13-26`(`createUIPropsBase`)
**Apply to:** 所有 `src/ui/*.tsx` 组件。
- 组件 props 须声明 `'controller' | 'instance' | 'state'` 三项(照抄 `src/ui/title.tsx:62-64`)。
- 组件内获取主对象一律 `props.state`,**禁止** `import { client }`。
- **待决(planner 须在计划中定一个并全仓统一):** props 泛型基类型取 `IUIPropsBase`(client-modules 本地)还是 `IGameUIPropsBase`(`@user/client-base`)。两者同构;`func.ts` / `client.ts` 现行用 `IGameUIPropsBase`,`types.ts` 定义 `IUIPropsBase`。修正 `src/ui/load.tsx:23` 与 `src/ui/title.tsx:38` 的 `import { IUIDefaultPropsBase } from './types'`(该符号并不由 `./types` 导出)。
### 单例归属(`mainUIController` / `sceneController` / `using`)
**Source:** `packages-user/client-base/src/types.ts:45,47,49`(`IClientBase` 已含三者字段);Phase 4 `04-15-RENDER-SINGLETON-AUDIT` `#04-15-F-02/F-04/F-05`
**Apply to:** `controller.tsx` / `main.tsx` / `statusBar.tsx` / `toolbar.tsx` / `render/action.ts` / `render/index.tsx`
- 组件内使用主对象字段是可行且受支持的(`IClientBase` 有字段);但**模块级单例(`controller.tsx:11`、`render/scene.ts:3`)的存废/迁移**是 Phase 4 遗留未决项,**须先经用户裁定**(AGENTS.md 命名/接口须先反馈)。
- 本任务**不得**擅自删除或改名这些单例;只做 `client` 单例的消除。
### 生命周期清理(hook 卸载)
**Source:** `render/ui/main.tsx:148-150`
**Apply to:** D-12 新响应式对象、以及任何 `addHook` 订阅。
```tsx
onUnmounted(() => {
attributeHook.unload();
});
```
### flags 更新占位(D-13)
**Source:** CONTEXT D-13
**Apply to:** `main.tsx` / `settings.tsx` 内所有 `flags` 读取后需响应更新的点。
- flags 无监听系统 ⇒ 不加监听,仅加 `// TODO:`(格式 `dev.md:78`),并注明「flags 更新未接」。
### 目录移动范式(D-10)
**Source:** `.planning/phases/04-render-adaptation/04-STRUCTURE-MIGRATION-IMPACT.md:59-110`(被移集合清单表)+ `:32-43`(六类判定口径 + 外部导入点 B 类)
**Apply to:** 全部迁移文件与 importer。
- 先出「现路径 → 目标路径」映射表(逐文件);再出「外部导入点」清单(每一处 `file:line`);最后核对 barrel 断链与 D-69。
- 迁移后**必须**删除源目录 `render/ui/**`,不得留双份。
### Barrel 硬约束(D-69)
**Source:** CONTEXT `<code_context>` Established Patterns;`.planning/phases/04-render-adaptation/04-UI-ADAPTATION-IMPACT.md:126-138`(D-69 判定)
**Apply to:** `src/ui/index.ts`、`src/render/index.tsx`、`src/client-modules/src/index.ts`。
- barrel 只允许 `export * from './<同目录项>'`;`render/index.tsx` 的 `export * from '../ui'` 属**上层转发**,违反 D-69 ⇒ 评估删除,改由 `client-modules/src/index.ts` 直连 `./ui`。
- 消费者直连包公共面,不经中间 barrel 转发。
### 代码规范(`dev.md`,迁移时同步遵守)
- 引入置首、import 间无空行/注释(`dev.md:55`)。
- 无 `import type`(`dev.md:54`);所有类须 `implements` 接口(`:100`);避免 `?.`(`:107-109`)。
- 不改任何既有 jsDoc 注释(AGENTS.md),新增注释遵循 `dev.md:72-86`。
---
## No Analog Found
| 对象 | Role | Data Flow | Reason |
|------|------|-----------|--------|
| `src/ui/<hero-attr>.ts`(D-12 的 `IHeroAttr` 响应式对象) | hook / composable | event-driven | 仓内无「独立响应式属性包装 + `addHook` 监听」的既有同类文件;仅可借 `render/ui/main.tsx:66-90,141-150` 的内联写法。**命名待用户批准**。 |
| flags 更新监听 | — | event-driven | 仓内无 flags 监听系统(D-13 明确);本任务不实现,仅标 TODO。 |
---
## Metadata
**Analog search scope:** `packages-user/client-modules/src/{render/ui,ui,render}`、`packages-user/client-base/src/{types.ts,save,components}`、`packages-user/data-{base,state}/src/{types.ts,hero}`、`packages/system/src/ui/**`、`.planning/phases/04-render-adaptation/**`、`dev.md`、`AGENTS.md`
**Files scanned:** 迁移源 11 + 迁移目标已存在 6 + 接线/barrel 3 + 接口基准 ~10 + Phase 4 文档 5
**Tracked-source 校验:** `render/ui/**`(11)与 `src/ui/**`(6)经 `git ls-files` 验证为 tracked;未引用任何 gitignored 镜像路径
**Pattern extraction date:** 2026-10-09