mirror of
https://github.com/motajs/template.git
synced 2026-10-09 22:10:23 +08:00
refactor: combat system
This commit is contained in:
parent
d306d11bce
commit
e25ef3fa93
@ -181,13 +181,15 @@ Plans:
|
||||
3. 移动端(窄屏)布局下,同一场景正常显示且可操作
|
||||
4. 数据端与渲染端保持双端分离——数据端无 DOM,仍可在 Node 环境跑回放验证
|
||||
|
||||
**Plans**: 13 已执行 + 1 已规划待执行(04-14) — 04-01..04-07 已执行(04-06 = material 接口影响范围清点·只读;04-07 = material 接口适应实施·第二步);04-08(TextureManager 新接口影响范围清点·只读,D-38/D-39/D-40/D-41/D-42 第一步)已执行;04-09(TextureManager 新接口适应实施·第二步·代码改动,D-43..D-50)已执行;04-10(渲染端结构性重构影响范围清点·只读,D-51..D-57 第一步)已执行;04-11(渲染端结构性重构·第二步·移植实施·代码改动,D-58..D-68)已执行;04-12(04-11 落地修正·桶导出边界 + 消费者直连 + shared 单文件·代码改动,D-69/D-70)已执行(异地执行,commit 81e4e98;`8b114c7 docs(04-12): sync tracking`);**04-13(部分 UI 的进一步接口适配·第一步·只读收集,D-71/D-72/D-73)已执行**(交付 `04-UI-ADAPTATION-IMPACT.md`;A7/B5/C2/D5/E4/F5 + 8 行修改点汇总);**04-14(部分 UI 的进一步接口适配·第二步·适配实施,D-74..D-78)已规划待执行**(执行前须 Task 0 汇报关卡等待用户「可以执行」);渲染适配与移动端/桌面端双布局(REND-01/REND-02)整体仍未完成
|
||||
**Plans**: 15 已执行 + 1 已规划待执行(04-16) — 04-01..04-07 已执行(04-06 = material 接口影响范围清点·只读;04-07 = material 接口适应实施·第二步);04-08(TextureManager 新接口影响范围清点·只读,D-38/D-39/D-40/D-41/D-42 第一步)已执行;04-09(TextureManager 新接口适应实施·第二步·代码改动,D-43..D-50)已执行;04-10(渲染端结构性重构影响范围清点·只读,D-51..D-57 第一步)已执行;04-11(渲染端结构性重构·第二步·移植实施·代码改动,D-58..D-68)已执行;04-12(04-11 落地修正·桶导出边界 + 消费者直连 + shared 单文件·代码改动,D-69/D-70)已执行(异地执行,commit 81e4e98;`8b114c7 docs(04-12): sync tracking`);**04-13(部分 UI 的进一步接口适配·第一步·只读收集,D-71/D-72/D-73)已执行**(交付 `04-UI-ADAPTATION-IMPACT.md`;A7/B5/C2/D5/E4/F5 + 8 行修改点汇总);**04-14(部分 UI 的进一步接口适配·第二步·适配实施,D-74..D-78)已执行**;**04-15(渲染端去单例化·第一步·只读收集,用户 2026-10-01 裁定)已执行**(交付 `04-RENDER-SINGLETON-AUDIT.md`,commit 6cff487);**04-16(渲染端去单例化·第二步·挂载 + 接口继承重构,用户 2026-10-01 裁定,F-01..F-07 已裁定)已规划待执行**(执行前须 Task 0 汇报关卡等待用户「可以执行」);渲染适配与移动端/桌面端双布局(REND-01/REND-02)整体仍未完成
|
||||
**Plan register note (2026-09-21):** 第五个增量 `04-05`(收口 `#04-01-M-06` / `#04-01-M-09`:状态栏 9 个数值属性改经 `client.hero.attribute.getFinalAttribute(...)` 读取 + 逐图层钩子类 `RendererLayerHook`,2026-09-20 执行)此前未登记进本路线图,本次一并补登;第六个增量 `04-06` = material 接口适应(D-30/D-31/D-32)**第一步·只读影响清点**,产出 `04-MATERIAL-INTERFACE-IMPACT.md`,第二步(实施)待用户审阅后另行规划。第七个增量 `04-07` = material 接口适应实施(**第二步·代码改动**,D-33..D-37),2026-09-22 已执行(10 文件原子提交)。**第八个增量 `04-08` = TextureManager 新接口适应(D-38..D-42)第一步·只读影响清点**,产出 `04-TEXTURE-INTERFACE-IMPACT.md`;第二步(实施)待用户审阅后另行规划。
|
||||
**Plan register note (2026-09-24):** 第十个增量 `04-10` = 渲染端结构性重构(用户裁定 2026-09-23 · D-51..D-57)**第一步·只读影响清点**:把 `packages-user/client-modules/src/render/` 下的通用内容(`render/components`、`render/elements`、`render/map` 三文件夹 + `render/utils/layout.ts`,规划日实测 **39** 文件)迁往渲染系统层 `packages-user/client-base/src/`(D-51/D-52;目标布局按 D-53:`client-base/src/{components,elements,map}/` + `client-base/src/layout/`,**不新建 `render/` 目录**);不移植 `render/utils/` 其余(`index.ts`/`saves.ts`/`use.ts`)与 `ui`/`fx`/`weather`/`render` 顶层文件(D-54);legacy 一律只报告(D-55);用户并发改动零触碰(D-56)。台账按 A 被移集合内部交叉引用 / B 外部导入点(含 `entry-client` / `entry-data` / `packages/legacy-ui` 经 `@user/client-modules` 公共面的间接依赖)/ C barrel 与导出面 / D 分层与循环依赖风险(`client-base` 不得反向依赖 `client-modules`,含 `package.json` 依赖现状)/ E legacy 命中(仅报告)/ F 未确定 六类分组,每行精确到文件或符号 + `file:line` 且 ≥2 锚点;产出 `04-STRUCTURE-MIGRATION-IMPACT.md`,生产代码零改动且**零命名变更**;**已规划待执行**,执行前须 Task 0 汇报关卡等待用户「可以执行」;第二步(移植实施)待用户审阅后另行规划;`REND-01` / `REND-02` 仍 Pending。
|
||||
**Plan register note (2026-09-24 · 04-11 第二步):** 第十一个增量 `04-11` = 渲染端结构性重构**第二步·移植实施(代码改动)**(用户裁定 2026-09-24 · D-58..D-68)。**移植按 MOVE 落地**:把 D-52 的 **39** 文件(`render/components` 13 / `render/elements` 5 / `render/map` 20 / `render/utils/layout.ts` 1)按 D-53 放进 `packages-user/client-base/src/{components,elements,map}/` 与 `client-base/src/layout/layout.ts`(并新增 `layout/index.ts`,D-61),**并删除 `client-modules/src/render/` 下的原件**;D-58 另新建 `client-base/src/shared/`(从 `client-modules/src/shared.ts` 摘取被移 `map` 文件所需的 10 个常量,原文件保留);D-63 `client-base/src/index.ts` 新增 `./components` / `./elements` / `./map` / `./layout` / `./shared` 五条桶导出,实现层消费者(`render/ui` 8 文件 11 处含 `toolbar.tsx` 两处深层导入、`fallback/ui.ts:2`、`render/index.tsx:5,45,46`、`client.ts:25`、`types.ts:2`)改指 `@user/client-base`;被移 `map` 文件的 shared 说明符改指 4 处(D-58);D-59 / D-68 的架构耦合(`render/renderer` 3 处 + `render/use.ts` 5 处)与 D-60 的 `render/utils/` 断链(`index.ts:1` / `saves.ts:2`)共 **10** 处以带原因的 `// @ts-expect-error` 暂记,**不反向引用、不解耦**;D-66 `client-base/package.json` 新增 5 条 `workspace:*` 依赖(`@motajs/animate` / `@motajs/common` / `@motajs/render-vue` / `@motajs/system` / `@user/data-common`)并执行 `pnpm i`;D-64 `?raw` 不处理、D-65 legacy 一律不管、D-62 命名一律不变;门禁 = 目标落位 / 移动已发生 / 桶导出 / 依赖与 lockfile / 消费者改指 / 反向引用零命中 / `@ts-expect-error` 落点(10 处新增 + 既有 7 条保留)/ 39 文件允许差异 / 实现层差异 / 范围与用户改动 / 无 `import type` / CRLF 共 12 道 + 计划文件范围门禁 + 人工复核(**D-68 明示不要求可运行、不要求零类型错误,故不设 `check:type` / `build` / TS 诊断数类门禁**);`autonomous: false`(Task 0 汇报关卡);第二步的后续余留(自包引用未验证 / 两行同源星导出 / 8 处反向耦合接线 / D-60 断链)由用户接手;`REND-01` / `REND-02` 仍 Pending。
|
||||
**Plan register note (2026-09-24 · 04-12 落地修正):** 第十二个增量 `04-12` = **04-11 落地修正·桶导出边界 + 消费者直连 + `shared` 单文件(代码改动)**(用户裁定 2026-09-24 · **D-69 / D-70**)。**D-69(桶导出边界原则,硬约束)**:一个 barrel(`index.ts` / `index.tsx`)**只允许 `export * from './<同目录下的文件或子文件夹>'`**、不得导出本文件夹之外的任何内容;反过来,任何需要被移内容的**消费端必须直接 `import ... from '@user/client-base'`**、**不得经任何 barrel 转发导出**。**D-70 的 6 条修正**:① `client-modules/src/render/utils/index.ts` **删除**指向 `./layout` 的越界转发 `export *` 及其 `@ts-expect-error`(保留 `./saves` / `./use`);② `render/ui/save.tsx` **拆分** `../utils` 导入(`adjustGrid` / `IGridLayoutData` → `@user/client-base`,`getSave` / `SaveData` 仍留 `../utils`)、`render/ui/title.tsx` 的 `adjustCover` → `@user/client-base`;③ `render/utils/saves.ts` 的 `getConfirm` / `waitbox` → `@user/client-base` 并**删除**其 `@ts-expect-error`;④ `client-base/src/shared/{shared.ts,index.ts}` **折成单文件** `client-base/src/shared.ts` 并删除 `shared/` 目录(`client-base/src/index.ts` 的 `export * from './shared'` 与 `map/*` 的 `'../shared'` / `'../../shared'` 说明符**不受影响**,无需改动);⑤ **逐字保留** `client-base/src/{components/{choices,input,misc,scroll,textboxTyper,tip}.tsx,elements/index.ts}` 内 **8** 处引用 `render/use.ts` / `render/renderer` 的架构耦合 `@ts-expect-error`(属 D-59 / D-67,留待用户收尾;`textboxTyper.ts` 另有 1 处既有 `无法推导` 标注同属保留,故这 7 文件合计 9 处);⑥ **不得**新增任何转发导出,`client-modules/src/render/index.tsx` **维持现状**(用户已删除越界两行,现合规)。旁注(仅报告):`packages/client/src/index.ts:1` 的 `export * from '@motajs/client-base'` 亦违反 D-69,但在 `packages/`(D-33 只报告)。改动面 = `client-base/src/shared.ts` + `client-modules/src/render/{utils/index.ts,utils/saves.ts,ui/save.tsx,ui/title.tsx}` 五个生产文件 + `files_deleted` 的 `client-base/src/shared/{shared.ts,index.ts}`;门禁 = 桶边界(8 个 in-scope barrel 的 `export * from` 全为 `./` 前缀 + `render/utils/index.ts` 不再转发 `./layout`)/ F1..F4 收口自检 / F5 的 8 处标注逐字保留 + porcelain 零命中 / 范围与用户改动 / CRLF + 人工复核(**不设 `check:type` / `build` / TS 诊断数类门禁**,D-68 的延续);`autonomous: false`(Task 0 汇报关卡等待用户「可以执行」);无依赖变更、不执行 `pnpm i`;本步不引入任何新命名;`REND-01` / `REND-02` 仍 Pending。
|
||||
**Plan register note (2026-09-30 · 04-13 第一步):** 第十三个增量 `04-13` = **部分 UI 的进一步接口适配·第一步·只读收集需要修改的点(只读)**(用户裁定 2026-09-25 · **D-71 / D-72 / D-73**)。**要解决的问题**:数据端经 `d8fa4b8 refactor: Delete global loading & hook object` 与加载系统重构后,渲染端部分 UI 仍以旧接口编写——① 全局 `loading` / `hook` 对象已删除,组件不得再依赖它们监听全局钩子、**必须在对应对象上监听**(残留:`render/ui/main.tsx:146-151` 的 `hook.on('statusBarUpdate', ...)`、`render/index.tsx:35-38` 的 `hook.on('restart', ...)`,及 `main.tsx:140-143` 的手工单次调用);② 加载系统重构,旧加载 UI(`render/ui/load.tsx` 的 `LoadScene` / `LoadSceneUI`)已不能用,仍调用 `loader.initSystemLoadTask()` / `loader.load()` / `loader.progress`(`:71,72,76-79,91-92,118-119,139`),须重新适配新加载系统(`@motajs/loader` 的 `LoadManager` + `packages-user/data-state/src/loader/**` 的 `MotaDataLoader` / `IMotaDataLoader`)。**D-72**:本目标至少两个计划——第一个只读收集(生产代码零改动),第二个及后续才执行;第一个计划的产出是后续执行计划的输入。**D-73**:沿用 D-33 / D-42 / D-48 / D-49 / D-55 / D-65 / D-56 / D-68,**D-69 桶导出边界继续适用**。**交付物** = `.planning/phases/04-render-adaptation/04-UI-ADAPTATION-IMPACT.md`(只读影响台账):元信息块 + `只读起始基线` / `背景` / `方法` / `基准变更(d8fa4b8)` / A 全局 loading / hook 使用点(UI 面)/ B 目标对象与钩子 API 证据 / C 旧加载 UI 清单与新加载系统 / D 分层与 barrel(D-69)检查 / E legacy 命中(仅报告)/ F 未能从阅读确定(未猜测)/ 修改点汇总(供后续执行计划消费)/ 处置;A–E 固定列序表 `| ID | 项目 | 命中处 file:line | 目标锚点 / 证据 file:line | 问题描述 | 影响 | 置信 |`,事实行每行 ≥2 个 `file:line` 锚点、证据行带 `[证据行]` 标记;每个 A 点给出「应改监听 / 使用的对象」+ 现行源码证据,或显式 undetermined(进 F 类)。**边界**:被查 = 渲染端部分 UI(`client-modules/src/render/ui/**` + `render/index.tsx`);`client.ts` / `client-modules/src/index.ts` / `entry-client/src/create.ts` 的 `loading` / `hook` 使用点**只报告**(非 UI,是否纳入由用户裁定);`packages/` 与 `src/` 仅报告(D-33)。**门禁**:只读基线逐条一致 + 台账结构 / 内容 / 证据纪律(≥2 锚点)+ 范围与用户改动 + 既有 `04-01`..`04-12` 产物保护 + CRLF + 人工复核(**不设 `check:type` / `build` / TS 诊断数类门禁**,D-68 的延续);`autonomous: false`(Task 0 汇报关卡等待用户「可以执行」);**本步为只读、不引入任何命名变更**;`REND-01` / `REND-02` 仍 Pending。
|
||||
**Plan register note (2026-09-30 · 04-14 第二步):** 第十四个增量 `04-14` = **部分 UI 的进一步接口适配·第二步·适配实施(代码改动)**(用户裁定 2026-09-30 · **D-74 / D-75 / D-76 / D-77 / D-78**)。**要解决的问题**:`04-13` 只读台账确认,数据端 `d8fa4b8 refactor: Delete global loading & hook object` 与加载系统重构后,渲染端部分 UI 仍以已删全局 `loading` / `hook` 与旧加载接口编写。**D-74..D-78 逐条落地**:① **D-78(tracer)** `render/ui/main.tsx` 删除全局 `hook` 状态栏监听残留(`:145-152`)与手工单次调用垫片(`:140-143`),改为在**勇士属性对象** `client.hero.attribute` 上 `addHook({ onUpdateAttribute })` 并在 `onUnmounted` 注销(接口 `packages-user/data-base/src/hero/types.ts:95-106`;触发点 `packages-user/data-base/src/hero/attribute.ts:89`),新增局部常量 `attributeHook`;② **D-74 / D-75** `render/ui/load.tsx` 以 `client.loader.start()`(`AsyncIterable`)取代旧「系统加载任务初始化 + 加载 + `progress` 迭代」三件套,进度 / 字节 / 任务读数一律取自 `client.loader.manager`(`ILoadManager`)的 `getLoadedTasks` / `getAddedTasks` / `getTotalByte` / `getLoadedByte` / `getByteRatio`,并移除 `:75` 失效 `@ts-expect-error`;③ **D-76** 删除 `render/index.tsx:35-38` 被注释 `restart` 监听块;④ **D-77** 删除 `client.ts:88-92,124-127` 两段被注释兼容层(含「兼容层」标签)与 `entry-client/src/create.ts:16,31,34-41` 被注释接线及模块创建函数块(`client-modules/src/index.ts` 经 `git grep` 实证**零残留、不编辑**)。**沿用约束** D-33 / D-42 / D-48 / D-49 / D-55 / D-56 / D-65 / D-68 / **D-69(本步零新增转发、不改任何 barrel)**。**开点(未裁定、默认不处置)**:旧加载 UI 的装配 / 可达性 —— `createGameRenderer`(`render/index.tsx:13`)无调用者、`LoadSceneUI` 仅在 `:25` 被 `sceneController.open`,本计划默认**不重新接线**,须用户另行裁定。**命名**:仅新增局部常量 `attributeHook`(Task 0 获批);不新增 / 改名任何公共 / 受保护 / 私有成员。**门禁**:编辑集最终形状 + 残留清零 + 范围(`material/` / `packages/` / `src/` / 数据端 / `packages/common` 零改动)+ 用户改动保留(`packages/common/src/{hook.ts,types.ts}` 与 `04-CONTEXT.md`,D-56)+ 白名单提交 + 既有 `04-01`..`04-13` 产物保护 + CRLF + 人工复核(**不设 `check:type` / `build` / TS 诊断数类门禁**,D-68 的延续);`autonomous: false`(Task 0 汇报关卡等待用户「可以执行」);无依赖变更、不执行 `pnpm i`;`REND-01` / `REND-02` 仍 Pending。
|
||||
**Plan register note (2026-10-01 · 04-15 第一步):** 第十五个增量 `04-15` = **渲染端去单例化·第一步·只读收集(只读,用户 2026-10-01 裁定)**。**用户裁定**:渲染端(`packages-user/client-modules` 与 `packages-user/client-base`)**不得再保留单例**;现行一切单例须改为**挂载在主类上**(`ClientCore` / `client`),且各子系统须**继承 / 实现 `IClientBaseExtended`(`client-base/src/types.ts:27`)或 `ICoreStateExtended`(`data-state/src/types.ts:45`)**,镜像数据端模式(数据端单例 `data-state/src/ins.ts:11`;既有先例 `client-base/src/material/types.ts:94,218,406,439`)。**两步走**:第一步(本计划)= 只读清点全部单例(枚举 / 分类 / 消费者 / 目标映射;生产代码零改动、不引入任何命名变更);第二步(后续 04-16)= 挂载 + 接口继承重构。**交付物** = `04-RENDER-SINGLETON-AUDIT.md`(元信息块 + `只读起始基线` / `背景` / `方法` / `枚举口径` / A 模块级实例化对象 / B 可变模块级状态与全局注册表 / C 全局访问器与框架级全局引用 / D 顶层副作用模块 / E 主类关系与目标挂载面映射 / F 未能从阅读确定(未猜测)/ 单例→目标映射汇总 / 处置;事实行每行 ≥2 个 `file:line` 锚点,证据行 `[证据行]` 标记)。**沿用约束** D-33 / D-42 / D-48 / D-49 / D-55 / D-56 / D-65 / D-68 / D-69。**门禁** = 只读基线一致(含用户未提交 `data-system/src/combat/context.ts` 的正面对照 + 哈希)+ 台账结构 / 内容 / 证据纪律 + 范围与用户改动 + 既有 `04-01`..`04-14` 产物保护 + CRLF + 人工复核(**不设 `check:type` / `build` / TS 诊断数类门禁**,D-68 的延续);`autonomous: false`(Task 0 汇报关卡等待用户「可以执行」);**本步为只读、不引入任何命名变更**;`REND-01` / `REND-02` 仍 Pending。
|
||||
|
||||
**Plan register note (2026-09-23):** 第九个增量 `04-09` = TextureManager 新接口适应**第二步(实施·代码改动)**(用户裁定 D-43..D-50:`renderWithoutCheck` 三处调用点改新 `render(tile, connection)`;`BlockCls` / `cls` 判定改为 `tileType === TileType.Autotile`、不做映射表;`extension/hero.ts` 勇士字面量填 `tileType: TileType.Unknown`;`MapRenderer` 不再自建 `AutotileProcessor` 而取 `manager.autotile`、保留 `readonly autotile` 字段与 `IMapRenderer` 签名;不改 `material/**` 与加载面;不考虑 legacy 兼容)。对象 = `packages-user/client-modules/src/render/map/{vertex.ts,renderer.ts,extension/hero.ts}` 的 **13 条** texture 归属 TS 诊断(vertex 6 / renderer 5 / hero 2),目标归零;`moving.ts` 与 `render/map/types.ts` 经核实无需改动。**已规划待执行**,执行前须 Task 0 汇报关卡等待用户「可以执行」;`REND-01` / `REND-02` 仍 Pending(本步仅为 REND-01 的 texture 子切片)。
|
||||
|
||||
Plans:
|
||||
@ -244,6 +246,14 @@ Plans:
|
||||
|
||||
- [x] 04-14-PLAN.md — 部分 UI 的进一步接口适配·**第二步·适配实施(代码改动,D-74 / D-75 / D-76 / D-77 / D-78)**:`render/ui/main.tsx` 状态栏改在勇士属性对象 `client.hero.attribute` 上注册 `onUpdateAttribute` 钩子(tracer,删除全局 `hook` 残留与手工调用垫片,新增局部 `attributeHook`);`render/ui/load.tsx` 改依 `client.loader.start()` + `client.loader.manager.get*`;删除 `render/index.tsx:35-38` 的 `restart` 注释块与 `client.ts:88-92,124-127` / `entry-client/src/create.ts:16,31,34-41` 的非 UI 残留(`client-modules/src/index.ts` 零残留不编辑);默认不重新接线旧加载 UI 装配点(开点,待用户裁定);`autonomous: false`(Task 0 汇报关卡等待用户「可以执行」);门禁全静态(不设 `check:type` / `build` / TS 诊断数类门禁,D-68 的延续);`REND-01` / `REND-02` 仍 Pending
|
||||
|
||||
**Wave 14** *(blocked on Wave 13 completion)*
|
||||
|
||||
- [x] 04-15-PLAN.md — 渲染端去单例化·**第一步·只读收集(只读,用户 2026-10-01 裁定)**:收集 `packages-user/client-modules` 与 `packages-user/client-base` 内**全部单例**(模块级实例化对象 / 可变模块级状态 / 全局注册表 / 全局访问器 / 框架级全局引用 / 顶层副作用模块),产出 `04-RENDER-SINGLETON-AUDIT.md`(元信息块 + `只读起始基线` / `背景` / `方法` / `枚举口径` / A 模块级实例化对象 / B 可变模块级状态与全局注册表 / C 全局访问器与框架级全局引用 / D 顶层副作用模块 / E 主类关系与目标挂载面映射 / F 未能从阅读确定(未猜测)/ 单例→目标映射汇总 / 处置;每个单例映射到「挂主类 `ClientCore` / `client` + `IClientBaseExtended`(`client-base/src/types.ts:27`)/ `ICoreStateExtended`(`data-state/src/types.ts:45`)之一 + 既有先例证据」,无法确定者进 F 类;事实行每行 ≥2 个 `file:line` 锚点);**本步只读、生产代码零改动、不引入任何命名变更**;门禁 = 只读基线一致(含用户未提交 `data-system/src/combat/context.ts` 的正面对照 + 哈希)+ 台账结构 / 内容 / 证据纪律 + 范围与用户改动 + 既有 `04-01`..`04-14` 产物保护 + CRLF + 人工复核(**不设 `check:type` / `build` / TS 诊断数类门禁**,D-68 的延续);`autonomous: false`(Task 0 汇报关卡等待用户「可以执行」);第二步(04-16:挂载 + 接口继承重构)待用户审阅后另行规划;`REND-01` / `REND-02` 仍 Pending
|
||||
|
||||
**Wave 15** *(blocked on Wave 14 completion)*
|
||||
|
||||
- [ ] 04-16-PLAN.md — 渲染端去单例化·**第二步·挂载 + 接口继承重构(代码改动,用户 2026-10-01 裁定,F-01..F-07 已裁定)**:镜像数据端分层(单一 `ClientCore implements IClientCore extends IClientSystem extends IClientBase`;字段声明在层接口):把 client-modules 的 `using` / `sceneController` / `mainUIController` 在 `ClientCore` 构造器中实例化并挂为 `IClientCore` 字段(F-02);重复实例以 `ClientCore` 为准(F-03);`GameUI` 不挂、`UIController` 挂(F-04);`texture`(F-01)/ `DEFAULT_FONT` / 所有 `GameUI` 保留不动(F-04/F-07);client-base 层与 `client-system` 不改;`this.state` 可调用处迁移、其余先不管;不新增全局 `client` 引用(D-23)、不建转发别名(dev.md);门禁全静态(不设 `check:type` / `build` / TS 诊断数类门禁,D-68 的延续);`autonomous: false`(Task 0 汇报关卡等待用户「可以执行」);`REND-01` / `REND-02` 仍 Pending
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 5: Legacy 移植
|
||||
@ -437,7 +447,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7
|
||||
| 1. 事件系统 | 13/13 | In Progress| |
|
||||
| 2. 寻路系统 | 5/5 | In Progress| |
|
||||
| 3. 数据端完成 | 19/19 | Complete | 2026-09-12 |
|
||||
| 4. 渲染适配与双布局 | 14/14 | In Progress| |
|
||||
| 4. 渲染适配与双布局 | 15/16 | In Progress| |
|
||||
| 5. Legacy 移植 | 0/TBD | Not started | - |
|
||||
| 6. 单元测试 | 18/18 | In Progress| |
|
||||
| 7. 数据端缺陷修复 | 15/16 | 暂缓 (Deferred) | - |
|
||||
|
||||
519
.planning/phases/04-render-adaptation/04-15-PLAN.md
Normal file
519
.planning/phases/04-render-adaptation/04-15-PLAN.md
Normal file
@ -0,0 +1,519 @@
|
||||
---
|
||||
phase: 04-render-adaptation
|
||||
plan: 15
|
||||
type: execute
|
||||
wave: 14
|
||||
depends_on:
|
||||
- 04-01
|
||||
- 04-02
|
||||
- 04-03
|
||||
- 04-04
|
||||
- 04-05
|
||||
- 04-06
|
||||
- 04-07
|
||||
- 04-08
|
||||
- 04-09
|
||||
- 04-10
|
||||
- 04-11
|
||||
- 04-12
|
||||
- 04-13
|
||||
- 04-14
|
||||
files_modified:
|
||||
- .planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md
|
||||
autonomous: false
|
||||
requirements:
|
||||
- REND-01
|
||||
- REND-02
|
||||
estimate:
|
||||
tokens: 45000
|
||||
raw_tokens: 45000
|
||||
tasks: 5
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求)**:Task 0 必须先向用户汇报「要解决的问题(渲染端 `packages-user/client-modules` 与 `packages-user/client-base` 内仍存在单例——模块顶层实例化对象 `export const x = new X()`;用户 2026-10-01 裁定:渲染端不得再有单例,全部须挂到主类 `ClientCore` / `client` 上,且各单例对应子系统须继承 / 实现 `IClientBaseExtended` 或 `ICoreStateExtended`,镜像数据端既有模式)」「两步走:第一步(本计划 04-15)= 只读清点全部单例;第二步(后续 04-16,本计划不规划)= 实际重构」「**范围仅限单例**:可变模块级状态 / 全局注册表 / 全局访问器(`Mota.require` / `window.*` / `core.*`)/ 顶层副作用 / 非 `new` 导出(`Symbol.for`、类静态 `Map`、解构导出、裸函数)一律**不在本步范围**」「大致解决方案(只读审计文档 `04-RENDER-SINGLETON-AUDIT.md`,A / E / F 分类 + 单例→目标映射汇总,A 事实行每行 ≥2 个 `file:line` 锚点,生产代码零改动、不引入任何命名变更)」,然后必须**停下等待**用户明确回复「可以执行」四个字;收到前不得执行任何任务、不得派发执行子代理。无论触发方式(`/gsd-execute-phase`、`/gsd-manager`、`/gsd-autonomous`、子代理/执行器派发)或配置模式(`.planning/config.json` 的 `mode: yolo`、`workflow.auto_advance`)如何,均不得自动放行。"
|
||||
- "**只读硬约束(生产代码零改动)**:本 run 生产源码改动为 **0**——`git status --porcelain -- packages packages-user src` 在 Task 1 步骤 (0a) 起逐行原样记录进台账 `## 只读起始基线` 一节,并在 (0a) 另记 `git hash-object -- packages-user/data-system/src/combat/context.ts` 的 40 位哈希为台账机读标记行 `<!-- baseline-hash-combat-context: <40hex> -->`;Task 1 / Task 2 / Task 3 / Task 4 的每个生产树门禁**正向确认基线**——(a) ` M packages-user/data-system/src/combat/context.ts` 行在场、(b) 再无其它条目、(c) 当前 `git hash-object` 与标记行一致,三者任一不满足即失败(用户改动被回滚或再编辑同样失败);`files_modified` 只含 `04-RENDER-SINGLETON-AUDIT.md` 一个交付物;本 `04-15-PLAN.md` 与 `04-15-SUMMARY.md` 属计划 / 执行簿记,不算生产改动。**不得**修改、移动、复制、预建任何 `packages-user/**` / `packages/**` / `src/**` 生产文件。"
|
||||
- "**枚举完整(两个包内每个单例都在案,且仅限单例)**:台账必须枚举 `packages-user/client-modules/src/**` 与 `packages-user/client-base/src/**` 内**全部**单例(定义:模块顶层实例化对象,即 `export const x = new X()`,或模块顶层 `new X()` 存于模块级 `const` 并导出),逐条给出:定义 `file:line`、符号、导出机制、构造类、全部消费者 `file:line`、当前归属 / 挂载、与 `ClientCore` / `client` 的关系。规划日实测(`git grep -n -E \"export const [A-Za-z0-9_]+ = new \" -- packages-user/client-modules packages-user/client-base`)共 **30** 条:client-modules **24**(`core.ts:6` `client`;`render/renderer.ts:18,20,35,43` `rafExcitation` / `excitationDivider` / `mainRenderer` / `using`;`render/scene.ts:3` `sceneController`;`render/ui/controller.tsx:11,29` `mainUIController` / `MainBackgroundUI`;`render/ui/load.tsx:161` `LoadSceneUI`;`render/ui/main.tsx:312` `MainSceneUI`;`render/ui/save.tsx:462` `SaveUI`;`render/ui/settings.tsx:663,665,667,669,671,673,678` 七个 settings UI;`render/ui/statistics.tsx:302` `StatisticsUI`;`render/ui/statusBar.tsx:488,489` `leftStatusBarUI` / `rightStatusBarUI`;`render/ui/title.tsx:484` `GameTitleUI`;`render/ui/viewmap.tsx:556` `ViewMapUI`;`shared.ts:105` `DEFAULT_FONT`)与 client-base **6**(`elements/cache.ts:280` `texture`;`components/choices.tsx:796,798` `ConfirmBoxUI` / `ChoicesUI`;`components/input.tsx:602` `InputBoxUI`;`components/misc.tsx:591,592` `WaitBoxUI` / `BackgroundUI`)。**执行日以当日 `git grep` 复核为准;若当日实测发现更多 `export const x = new X()`,一律补入;不得遗漏。**"
|
||||
- "**范围仅限单例(非 `new` 一律 OUT)**:不属于单例、不得登记为 A 类的对象包括且不限于:可变模块级状态(`render/use.ts` 的 `nowOrientation` / `orientationHooks` / `transitionMap`)、全局符号(`action/hotkey.ts:5` `Symbol.for('@key_main')`)、类静态注册表 / 计数器(`WeatherController.extern` / `.weathers`、`RenderColorTransition.key`)、全局访问器(`Mota.require`、`window.*`、`core.*`)、顶层副作用(`gameKey` 注册链 / `window.addEventListener` / 模块级 `bindExcitation`)、非 `new` 导出(`render/renderer.ts:45` 的 `export const { createApp, render, tagManager } = createRendererFor(...)` 解构导出,及任何裸 / 解构函数)。**这些一律不写入本台账的任何小节,也不设任何门禁。**"
|
||||
- "**A 表结构与「只登记不判定」纪律**:`## A 模块级实例化对象` 与 `## E 主类关系与目标挂载面映射` 各一张**固定列序**表 `| ID | 符号 / 项目 | 定义 file:line | 类型 / 导出机制 | 全部消费者 file:line | 当前归属 / 挂载 | 与 ClientCore / client 的关系 | 目标挂载 / 接口(证据或 undetermined) | 置信 |`;A 事实行每行至少 **2** 个完整 `文件:行` 锚点(识别正则接受 `.(tsx?|md|json):\\d+`);**证据行**(零命中 / 对照扫描,仅在 ID 单元格带 `[证据行]` 标记)改为必须携带核对命令(`git grep`)与原样结果、有真实锚点时至少 1 个,**豁免 ≥2 计数**;**非证据行带 `[证据行]` 标记即违规**。静态阅读不能定论者一律进 F 类,**不得写成错配 / 缺失 / 残留 / 待办**。"
|
||||
- "**目标模式映射(每个单例 → 挂主类 + `IClientBaseExtended` / `ICoreStateExtended` 之一 + 先例)**:台账必须为每个单例给出目标模式:① 挂到主类(`ClientCore` / `client`);② 该单例对应子系统接口应继承 / 实现 `IClientBaseExtended`(`packages-user/client-base/src/types.ts:27`,携带 `readonly state: IClientBase`;`IClientBase extends ICoreState` 同文件 `:8`)**或** `ICoreStateExtended`(`packages-user/data-state/src/types.ts:45`,携带 `readonly state: ICoreState`),并说明二者取舍;③ 附**先例** `file:line`——**渲染 / 系统层**既有先例 `packages-user/client-base/src/material/types.ts:94,218,406,439`(`IAutotileProcessor` / `ITextureManager` / `IAssetBuilder` / `TrackedAssetData` 四个**渲染 / 系统层接口** `extends` 的是**数据端**的 `ICoreStateExtended`,本身不是数据端实体)、**数据端**先例 `packages-user/data-state/src/ins.ts:11`(`export const state = new CoreState({...})`)、数据端扩展接口 `packages-user/data-state/src/types.ts:45`。**无法从阅读确定目标者一律写「undetermined」并进 F 类,不得臆测。**"
|
||||
- "**F 类(未能从阅读确定·未猜测)仅含单例相关项且只登记不判定**,必须覆盖:(a) 外部包(`@motajs/render` / `@motajs/render-vue` / `@motajs/system`)所有产出的 A 单例(`mainRenderer` / `using` / `sceneController` / `mainUIController`)能否挂到主类;(b) `render/renderer.ts` 的 `rafExcitation` / `excitationDivider` / `mainRenderer` 与 `ClientCore` 构造器内自建的 `rafExcitation` / `excitationDivider` / `renderer`(`client.ts:90,91,108`)**重复单例**时的权威归属;(c) `GameUI` / `UIController` 实例单例(各 `*UI`)是否属「须挂主类」的对象;(d) 目标接口取 `IClientBaseExtended` 还是 `ICoreStateExtended`;(e) `IClientBaseExtended` 现行无实现类;(f) 无消费者的 `statusBar` UI(`leftStatusBarUI` / `rightStatusBarUI`)。**不得把本类任何一条写成错配、缺失、残留或待办;不得加入任何非单例(`window` / `core` / `Mota.require` / 静态注册表 / 顶层副作用)条目。**"
|
||||
- "**第二步(04-16 重构)不被规划**:台账只清点、定位、映射,**不含**代码改动方案、不含具体挂载写法 / import 改写、不产出 `04-16-PLAN.md` 或任何新 `*-PLAN.md`;`## 处置` 明写「第一步只读清点完成;第二步(挂载 + 接口继承重构)待用户审阅本文件后另行规划」(用户 2026-10-01 两步走裁定)。"
|
||||
- "**本步不引入任何命名变更**:只读清点不新增 / 改名 / 删除任何公共、受保护、私有成员、方法、字段、文件或目录;若第二步确需新增命名(如挂载字段名、接口名),台账只**登记为待用户批准**并明写「待用户批准,不在本步引入」(AGENTS.md 命名先获批)。"
|
||||
- "**无运行时要求 + REND 保持 Pending(D-68 延续)**:验证一律为**静态**(porcelain 正向基线 + 台账结构 / 锚点 / 分类关键词 + 禁用路径 porcelain 为空 + CRLF + 既有产物保护 + ROADMAP 登记),**不得**出现任何以「`pnpm check:type` 通过」「`pnpm build` 成功」「TS 诊断数归零」为条件的门禁或验收项;`REND-01` / `REND-02` 在本 run 后仍为 **Pending**,`.planning/REQUIREMENTS.md` 零改动。"
|
||||
artifacts:
|
||||
- path: .planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md
|
||||
provides: "渲染端去单例化(用户 2026-10-01 裁定)**第一步·只读收集单例**:元信息块 + `只读起始基线` / `背景` / `方法` / `枚举口径` / `A 模块级实例化对象` / `E 主类关系与目标挂载面映射` / `F 未能从阅读确定(未猜测)` / `单例→目标映射汇总(供 04-16 消费)` / `处置`;A 表固定列序,事实行每行 ≥2 个 `file:line` 锚点,证据行带 `[证据行]` 标记与核对命令;供用户审阅后裁定第二步的挂载面与接口继承方案"
|
||||
- path: .planning/phases/04-render-adaptation/04-15-SUMMARY.md
|
||||
provides: "本增量执行记录:只读基线 / 台账落点 / 门禁实测 / 人工复核结论 / 无运行时验证声明 / 第二步(04-16)待规划"
|
||||
- path: .planning/ROADMAP.md
|
||||
provides: "Phase 4 的 `Wave 14` + `04-15` register note(计划阶段登记;本写入不修改该文件,登记由编排器完成)"
|
||||
key_links:
|
||||
- "`packages-user/client-modules/src/core.ts:6`(`export const client = new ClientCore({...})` 主单例)↔ `packages-user/client-modules/src/client.ts:41`(`class ClientCore extends CoreState implements IClientCore`,`:43-56` 的 `save` / `materials` / `rafExcitation` / `excitationDivider` / `renderer` / `mainMapRenderer` / `mainMapExtension` / `audioContext` / `soundPlayer` / `bgmPlayer` 字段)—— 去单例化的**主挂载类**"
|
||||
- "`packages-user/client-base/src/types.ts:27`(`export interface IClientBaseExtended { readonly state: IClientBase; }`)/ `:8`(`IClientBase extends ICoreState`)↔ `packages-user/data-state/src/types.ts:45`(`ICoreStateExtended { readonly state: ICoreState; }`)—— 单例对应子系统接口继承 / 实现的两个目标接口"
|
||||
- "`packages-user/client-base/src/material/types.ts:94,218,406,439`(`IAutotileProcessor` / `ITextureManager` / `IAssetBuilder` / `TrackedAssetData` 均 `extends ICoreStateExtended`)↔ `packages-user/client-base/src/material/manager.ts` / `builder.ts` / `autotile.ts`(D-35 的「`state` 统一通过构造器传入」先例)—— **渲染 / 系统层既有先例**(这四个是渲染 / 系统层接口,`extends` 的是数据端 `ICoreStateExtended`,本身不是数据端实体),作为目标模式证据"
|
||||
- "`packages-user/data-state/src/ins.ts:11`(`export const state = new CoreState({...})` 数据端单例)—— 用户「镜像数据端模式」的对照锚点"
|
||||
- "`packages-user/client-modules/src/render/renderer.ts:18,20,35,43`(`rafExcitation` / `excitationDivider` / `mainRenderer` / `using`)↔ `packages-user/client-modules/src/client.ts:90,91,108`(`ClientCore` 构造器内自建同名激励源与 `MotaRenderer`)—— 重复单例的归属未定(F 类 (b))"
|
||||
- "`packages-user/client-base/src/elements/cache.ts:280`(`export const texture = new TextureCache()` 模块级实例化对象,tracer 对象)↔ `packages-user/data-state/src/types.ts:45` 与 `packages-user/client-base/src/material/types.ts:218` —— tracer 的目标模式证据链"
|
||||
- "`git grep -n -E \"export const [A-Za-z0-9_]+ = new \" -- packages-user/client-modules packages-user/client-base`(规划日实测 **30** 条)↔ 台账 `## A` 表的 `#04-15-A-NN` 行集合 ↔ Task 3 的完整性反查 `[证据行]`(A-01.. 与之对齐)"
|
||||
- "`git status --porcelain -- packages packages-user src`(Task 1 步骤 (0a) 逐行记录;规划日实测以当日为准)↔ Task 1 / 2 / 3 / 4 门禁的正向基线断言(` M packages-user/data-system/src/combat/context.ts` 行在场 + 再无其它条目 + 当前 `git hash-object -- <该文件>` 与台账 `<!-- baseline-hash-combat-context: <40hex> -->` 标记一致)↔ 只读(生产代码零改动)证明"
|
||||
assumptions:
|
||||
- "**FLAGGED ASSUMPTION(specless fallback)— REND-01 edge unresolved:** 未提供 REND-01(渲染端适配新数据层接口)的验收边界;本计划**不发明**阈值,只交付渲染端去单例化的第一步只读清点台账(仅限单例)。用户须在推进第二步与 REND-01 余下部分前补齐边界判据。"
|
||||
- "**FLAGGED ASSUMPTION(specless fallback)— REND-02 edge unresolved:** 未提供 REND-02(移动端与桌面端双布局)的验收边界;本计划不涉及双布局,也不发明其验收阈值。"
|
||||
- "**规划日实测(2026-10-01)单例清单(须用户在审阅时确认,执行日以当日实测为准):** `git grep -n -E \"export const [A-Za-z0-9_]+ = new \" -- packages-user/client-modules packages-user/client-base` 实测 **30** 条——client-modules **24** / client-base **6**(明细见 truths 第 3 条)。**执行日一律以当日 `git grep` 定位,不得按规划日行号硬写。**"
|
||||
- "**执行日行号可能漂移:** 用户会并发改其它文件(沿 D-56 精神);本计划引用的 `file:line` 是 2026-10-01 的规划日锚点。执行时一律按符号名 / 文本检索定位,任一与规划日不同即以当日实测写入台账并在 SUMMARY 说明差异。"
|
||||
- "**无运行时验证可得(D-68 的延续):** 本步为只读收集,不产生任何运行时行为(不启动渲染、不改代码、不新增测试),承载体为静态门禁 + 人工复核;**不得**以 `pnpm check:type` / `pnpm build` / TS 诊断数之类的条件冒充门禁。"
|
||||
- "**第二步(04-16)不被本计划规划:** 本计划只清点与映射;任何挂载写法、接口新增 / 改名、字段命名均属第二步,且须用户先批准命名(AGENTS.md),本步一概不做。"
|
||||
- "**`IClientBaseExtended` 现行实现情况未知:** 规划日 `git grep` 显示 `IClientBaseExtended` 仅在 `client-base/src/types.ts:27` 定义、无实现类(`ICoreStateExtended` 则在 `material/types.ts` 有 4 处实现先例);该差异是否影响目标接口取舍进 F 类 (e),由用户裁定。"
|
||||
prohibitions:
|
||||
- "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理(AGENTS.md 的第一个 block 点约束;不因触发方式或 `yolo` / `auto_advance` 模式而放宽)。"
|
||||
- "不得修改任何生产源码:`packages/**`、`packages-user/**`、`src/**` 的生产文件一律零改动;不得新增 / 删除 / 改写任何 `.ts` / `.tsx` 生产文件;**不得移动、不得复制、不得预建**任何目标文件或目录(挂载重构属第二步,本步不规划)。"
|
||||
- "**范围仅限单例**:不得把任何非单例对象登记为 A 类或写入台账任何小节——包括但不限于可变模块级状态、全局注册表 / 类静态 `Map` / 计数器(`WeatherController.extern` / `.weathers`、`RenderColorTransition.key`)、全局符号(`Symbol.for`)、全局访问器(`Mota.require` / `window.*` / `core.*`)、顶层副作用(`gameKey` 注册链 / `window.addEventListener` / 模块级 `bindExcitation`)、非 `new` 导出(`render/renderer.ts:45` 的解构导出 `createApp` / `render` / `tagManager`、裸 / 解构函数)。"
|
||||
- "不得触碰、暂存、提交用户的并发未提交改动:`packages-user/data-system/src/combat/context.ts`(用户当前正在编辑,**必须原样保留**)与 `.planning/phases/04-render-adaptation/04-CONTEXT.md`(如仍为未提交)一律不修改、不暂存、不提交;提交前必须以 `git status` 逐条核对暂存面。"
|
||||
- "不得改写 `packages-user/data-*`(数据端)任何生产文件(仅作只读基准);不得触碰 `src/**`、其它 `packages-user/*`、`packages/**`(D-33 只报告)、任何测试文件、任何 legacy 面。"
|
||||
- "不得改动 `package.json` / `pnpm-lock.yaml` / `tsconfig.json` / `.eslintrc`;不得新增依赖;不得执行 `pnpm i`;不得改任何 barrel(D-69)——本步不触发任何桶导出改动。"
|
||||
- "不得修改任何既有 jsDoc / 注释文本(AGENTS.md);**不得引入任何命名变更**(不新增 / 改名 / 删除任何公共、受保护、私有成员、方法、字段、文件或目录);如第二步确需命名,只登记进 F 类 / 映射汇总并明写「待用户批准,不在本步引入」,不得自行命名。"
|
||||
- "不得把任何 legacy 内容当成问题或待办:`core.*` / `Mota.require` / `window.*` 等既非单例、也属 legacy,一律不写入本台账、不设门禁、不修复、不登记为待办。"
|
||||
- "不得改写 / 覆盖 / 改号 `04-01`..`04-14` 的既有 `*-PLAN.md` 与 `*-SUMMARY.md`;不得改写既有 `04-CONTEXT.md` / `04-UI-ADAPTATION-IMPACT.md`;不得产出 `04-16+` 或任何新计划文件。除本 `04-15-PLAN.md`、`04-RENDER-SINGLETON-AUDIT.md`、`04-15-SUMMARY.md` 与 `.planning/ROADMAP.md` 的登记外,不得写任何其它文件(含 `.planning/STATE.md` / `WINDOWS.md` / `REQUIREMENTS.md`)。"
|
||||
- "不得给出挂载重构方案:不得给出具体 `import` / 调用改写写法、不得给出新接口 / 字段设计、不得裁定目标接口(`IClientBaseExtended` vs `ICoreStateExtended`)。"
|
||||
- "不得使用 `import type`(dev.md 项目规则);不得新增、删除或修改任何测试文件与既有测试。"
|
||||
- "不得把任何门禁建立在「全仓 / 范围内 TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上,也不得因这类检查失败而阻塞交付(D-68)。"
|
||||
- "不得以「无运行时」为由弱化验证:每条 `<automated>` 必须有 `<fails_when>`;确实无法运行的门禁必须显式声明为不可得并降级为静态 + 人工,不得伪造绿。"
|
||||
---
|
||||
<objective>
|
||||
Phase 4 第十五个增量(用户裁定 2026-10-01)—— **渲染端去单例化·第一步(只读收集单例)**。
|
||||
|
||||
**要解决的问题:**
|
||||
|
||||
用户裁定:渲染端(`packages-user/client-modules` 与 `packages-user/client-base`)**不得再保留单例**。**本步的「单例」定义唯一且严格**:模块顶层实例化对象,即 `export const x = new X()`(或模块顶层 `new X()` 存于模块级 `const` 并导出)。用户明确要求**只**收集这类单例,**不**收集可变模块级状态、全局注册表 / 类静态 `Map`、全局访问器(`Mota.require` / `window.*` / `core.*`)、顶层副作用、全局符号(`Symbol.for`)或任何非 `new` 导出(解构导出、裸函数)。
|
||||
|
||||
当前一切以单例形式存在的对象,第二步须改为**挂载在主类上**(`ClientCore` / `client` 实例),且这些单例对应子系统须**继承 / 实现 `IClientBaseExtended` 或 `ICoreStateExtended`**,即子系统接口携带一个指向主对象的 `readonly state` 引用——镜像数据端既有模式(数据端单例 `packages-user/data-state/src/ins.ts:11`;扩展接口 `packages-user/data-state/src/types.ts:45`;渲染端既有先例 `packages-user/client-base/src/material/types.ts:94,218,406,439` 四个接口 `extends ICoreStateExtended`)。
|
||||
|
||||
**两步走(用户 2026-10-01 裁定):** 本目标**至少两个计划**——**第一步(本计划 04-15)= 只读清点全部单例**(枚举、定位消费者、映射目标模式;生产代码零改动、不引入任何命名变更);**第二步(后续 04-16)才真正执行挂载 + 接口继承重构**。第一步的产出是第二步的输入。**本计划不规划 04-16。**
|
||||
|
||||
**大致解决方案(本计划 = 04-15,单一计划文件,只读):**
|
||||
|
||||
产出唯一交付物 `.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md`。台账结构(模型参照 `04-UI-ADAPTATION-IMPACT.md` / `04-STRUCTURE-MIGRATION-IMPACT.md`,**仅保留单例相关小节**):
|
||||
|
||||
| 节 | 内容 | 类别 |
|
||||
|---|---|---|
|
||||
| 元信息块 + 只读起始基线 | 日期 / 范围 / 性质 / 基准 HEAD / 目标模式 / 下一步;Task 1 (0a) 的 porcelain 原样快照 + `git hash-object` 机读标记 | — |
|
||||
| 背景 | 用户 2026-10-01 裁定要点(去单例化 / 挂主类 / 接口继承 / 两步走 / **仅限单例**)+ 「本步只清点、不修改、不规划第二步」 | — |
|
||||
| 方法 | 枚举面(两个包)/ 核对手法(`git grep -n -E "export const [A-Za-z0-9_]+ = new "`)/ 证据纪律(A 事实行 ≥2 锚点、证据行 `[证据行]` + 核对命令)/ 只登记不判定 | — |
|
||||
| 枚举口径 | **单例定义唯一**(模块顶层实例化对象 `export const x = new X()`)+ 显式排除清单(可变模块级状态 / 全局注册表 / 全局访问器 / 顶层副作用 / 非 `new` 导出) | — |
|
||||
| A 模块级实例化对象 | 每个 `export const x = new X()` 一行:定义 / 符号 / 导出机制 / 构造类 / 全部消费者 / 当前归属 / 与 `ClientCore` 关系 | A |
|
||||
| E 主类关系与目标挂载面映射 | 每个单例 → 是否 / 如何挂到 `ClientCore` / `client`;目标接口 `IClientBaseExtended` 或 `ICoreStateExtended`;先例锚点;`client.ts:43-56` 现有主类字段对照 | E |
|
||||
| F 未能从阅读确定(未猜测) | (a) 外部包 A 单例能否挂主类;(b) 重复 renderer 实例归属;(c) `GameUI` / `UIController` 实例是否纳入;(d) 目标接口取舍;(e) `IClientBaseExtended` 无实现类;(f) 无消费者的 `statusBar` UI | F |
|
||||
| 单例→目标映射汇总(供 04-16 消费) | 固定列表:定义 `file:line` / 符号 / 类型 / 目标(挂主类 + 目标接口,或 undetermined)/ 先例 / 归属包 / 置信 | — |
|
||||
| 处置 | 只读完成;第二步(04-16)待用户审阅后另行规划 | — |
|
||||
|
||||
**范围与硬边界:** 被查对象 = `packages-user/client-modules/src/**` 与 `packages-user/client-base/src/**` 内的全部 `export const x = new X()` 单例及其消费者;**只读基准** = `packages-user/data-*`(接口 / 单例先例,不修改)、`packages/**` 与 `src/**`(仅报告)。**零触碰** = 数据端一切生产文件(含用户当前未提交的 `packages-user/data-system/src/combat/context.ts`,必须原样保留)、`packages/**`、`src/**`、任何测试文件、任何 legacy 面、任何 `package.json` / lockfile。生产源码零改动;本步**只读**,不引入任何命名变更,不规划第二步。
|
||||
|
||||
**任务切分(Task 0 汇报关卡 + Task 1 tracer + 2 个横向铺开任务 + Task 4 收口):** 委托明确指定 `04-15` 为唯一新计划,故落在**单一计划文件**内。Task 1(tracer)= **`client-base/src/elements/cache.ts:280` 的 `texture = new TextureCache()`** 这一个清晰单例**端到端**走完台账每一节(定义 → 消费者 → 当前挂载 → 目标接口证据 → undetermined → 映射汇总),先用一个真实单例证明台账格式与证据纪律;Task 2 = client-modules 的 A 单例横向铺开(`client` / renderer 实例簇 / `sceneController` / `mainUIController` / `MainBackgroundUI` / 各 `GameUI` 实例 / `DEFAULT_FONT`);Task 3 = client-base 的 A 单例铺开(`texture` 补消费者 / `ConfirmBoxUI` / `ChoicesUI` / `InputBoxUI` / `WaitBoxUI` / `BackgroundUI`)+ `E` 映射定稿 + `git grep` 完整性反查(A-01.. 对齐)+ `## 单例→目标映射汇总` 定稿;Task 4 = 收口(只读基线一致 + 台账结构 / 锚点门禁 + 范围门禁 + 既有产物保护 + ROADMAP 登记 + CRLF + 人工复核 + `04-15-SUMMARY.md` + 白名单提交)。
|
||||
|
||||
## Source Audit(多源覆盖审计)
|
||||
|
||||
| SOURCE | ID | Feature/Requirement | Plan | Status | Notes |
|
||||
|--------|----|---------------------|------|--------|-------|
|
||||
| GOAL | — | Phase 4:「渲染端通过新数据层接口驱动,并同时支持移动端与桌面端布局」 | 04-15 | COVERED(增量·只读收集子切片) | 本 run 只清点渲染端单例(去单例化第一步);挂载重构与双布局属后续增量(见两条 FLAGGED ASSUMPTION) |
|
||||
| REQ | REND-01 | 渲染端适配新数据层接口 | 04-15 | COVERED(子切片·只读前置;需求保持 Pending) | 「渲染端不得有单例、全部挂主类、子系统继承扩展接口」是 REND-01 的架构切片;本 run 不移交任何接口行为;验收边界未提供 → FLAGGED ASSUMPTION |
|
||||
| REQ | REND-02 | 渲染端同时支持移动端与桌面端布局 | 04-15 | COVERED(不涉及) | 本 run 范围不含双布局;验收边界未提供 → FLAGGED ASSUMPTION |
|
||||
| RESEARCH | — | (本阶段无 `04-RESEARCH.md`) | — | N/A | Phase 4 未产出 RESEARCH.md;等价输入为既有台账 `04-UI-ADAPTATION-IMPACT.md` / `04-STRUCTURE-MIGRATION-IMPACT.md`(格式模板)与 `04-13-PLAN.md` / `04-14-PLAN.md`(只读 / 关卡风格) |
|
||||
| CONTEXT | 用户裁定 2026-10-01 | 渲染端不得再有单例(仅「模块顶层 `export const x = new X()`」);全部挂 `ClientCore` / `client`;子系统继承 / 实现 `IClientBaseExtended` / `ICoreStateExtended`;镜像数据端模式;两步走(第一步只读清点) | 04-15 | COVERED | Task 1(tracer `texture`)+ Task 2(client-modules)+ Task 3(client-base + 映射);`## 处置` 明写第二步待规划;非单例类别全部 OUT |
|
||||
| CONTEXT | D-68(延续) | 不要求可运行、不以 TS 诊断数 / `check:type` / `build` 为门禁 | 04-15 | COVERED(延续) | 全部门禁为静态;prohibitions 明列 |
|
||||
| CONTEXT | D-69 | 桶导出边界原则(硬约束) | 04-15 | COVERED(本步不触发) | 本步不改任何 barrel;prohibitions 明列 |
|
||||
| CONTEXT | D-33 | 范围外一律不改、不追平全部类型错误 | 04-15 | COVERED(边界) | `packages/` 与 `src/` 只报告;无「诊断归零」验收项 |
|
||||
| CONTEXT | D-42 / D-48 | `client-base/src/material/` 归用户、AI 不得改 | 04-15 | COVERED(只读基准) | `material/types.ts` 仅作目标模式先例引用;范围门禁断言 `material/` porcelain 为空 |
|
||||
| CONTEXT | D-49 / D-55 / D-65 | 不考虑任何 legacy | 04-15 | COVERED(非单例·不登记) | `core.*` / `Mota.require` / `window.*` 属 legacy 且非单例,一律不写入台账 |
|
||||
| CONTEXT | D-56(精神) | 用户并发改动零触碰 | 04-15 | COVERED | Task 1 (0a) 逐行记录基线 + `git hash-object` 标记;Task 4 断言 `packages-user/data-system/src/combat/context.ts` 未提交改动仍在且哈希一致 |
|
||||
| LEDGER | `04-UI-ADAPTATION-IMPACT.md` / `04-STRUCTURE-MIGRATION-IMPACT.md` | 先在只读台账模板(元信息块 + 固定列序表 + 分类 + 处置) | 04-15 | COVERED(格式模板) | 沿用其结构、证据纪律与 `[证据行]` 标记规则 |
|
||||
| Deferred Ideas | — | 阶段 4 后续步骤(挂载重构 04-16、双布局、其余渲染适配);`packages` 全部、`legacy-plugin-*`、`data-fallback` | — | DEFERRED | 未写入任何任务 |
|
||||
|
||||
## Estimate Basis(估算依据)
|
||||
|
||||
`estimate-calibration` 返回 `factor: 1`、`applied: false`、`sample_count: 0`、`confidence: low`(项目无历史样本),故 `tokens == raw_tokens` 并如实标注 `confidence: low`。按「执行者实际触及量」估算(`estimate.tasks: 5` = Task 0 汇报关卡 + 4 个工作任务):Task 0 只读汇报并等待用户确认(约 5k,不写文件);Task 1(tracer)起始只读基线 + 台账骨架 + `texture` 一条线端到端(约 11k);Task 2 client-modules 单例铺开(约 11k);Task 3 client-base 单例铺开 + 映射定稿 + 完整性反查(约 10k);Task 4 收口门禁 + 人工复核 + `04-15-SUMMARY.md` + 白名单提交(约 8k);合计约 45k。**本计划刻意不做多计划拆分**:委托明确要求只产出 `04-15` 一份只读计划。
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/phases/04-render-adaptation/04-CONTEXT.md
|
||||
@.planning/phases/04-render-adaptation/04-UI-ADAPTATION-IMPACT.md
|
||||
@.planning/phases/04-render-adaptation/04-13-PLAN.md
|
||||
@.planning/phases/04-render-adaptation/04-14-PLAN.md
|
||||
@packages-user/client-modules/src/core.ts
|
||||
@packages-user/client-modules/src/client.ts
|
||||
@packages-user/client-base/src/types.ts
|
||||
@AGENTS.md
|
||||
@dev.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报「渲染端去单例化·第一步(只读收集单例)」的问题、用户 2026-10-01 裁定、枚举面与台账结构、源→目标映射方式、范围与硬边界、验证方式,等待用户回复「可以执行」</name>
|
||||
<precondition>用户裁定(2026-10-01:渲染端不得再有单例、全部挂主类 `ClientCore` / `client`、子系统继承 / 实现 `IClientBaseExtended` / `ICoreStateExtended`、镜像数据端模式、两步走、**仅限「模块顶层 `export const x = new X()`」单例**)已由用户下达(以用户本轮对话 / `.planning/phases/04-render-adaptation/04-CONTEXT.md` 内实际存在为准);`packages-user/client-modules/src/core.ts:6` 仍为 `export const client = new ClientCore({...})`;`packages-user/client-base/src/types.ts:27` 仍含 `IClientBaseExtended`;`packages-user/data-state/src/types.ts:45` 仍含 `ICoreStateExtended`;`packages-user/client-base/src/material/types.ts` 仍含 4 处 `extends ICoreStateExtended` 先例。</precondition>
|
||||
<files>(只读汇报,不修改任何文件)</files>
|
||||
<read_first>
|
||||
- AGENTS.md(「任何计划的执行,其第一个 block 点必须是向我汇报计划内容并等待我确认」;只有明确回复「可以执行」四个字之后才允许开始执行;「任何公共、受保护、私有方法、字段等内容的命名必须先向我反馈」;「不得擅自修改所有 jsDoc 注释,不得擅自修改任何已有注释」;「除非我明确要求,否则任何修改必须经由 GSD 标准流程」)
|
||||
- .planning/phases/04-render-adaptation/04-CONTEXT.md(既有裁定 D-33 / D-42 / D-48 / D-49 / D-55 / D-65 / D-68 / D-69;用户 2026-10-01 去单例化裁定的落点)
|
||||
- .planning/phases/04-render-adaptation/04-14-PLAN.md 与 04-13-PLAN.md(上一 / 上二增量 Task 0 汇报关卡的结构与口径,本关卡对齐版式)
|
||||
- .planning/phases/04-render-adaptation/04-UI-ADAPTATION-IMPACT.md 与 04-STRUCTURE-MIGRATION-IMPACT.md(台账格式 / 证据纪律模板:元信息块 + 固定列序表 + 分类 + 处置)
|
||||
- packages-user/client-modules/src/core.ts 与 client.ts(主单例与主类字段);packages-user/client-base/src/types.ts(`IClientBaseExtended`);packages-user/data-state/src/types.ts:45(`ICoreStateExtended`);packages-user/client-base/src/material/types.ts:94,218,406,439(先例)
|
||||
- dev.md(「模块原则」的**无副作用**与**无循环引用**;「双端分离」系统层 / 实现层;「其他要求」的 CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
**停下并向用户汇报,等待明确回复后才继续;汇报本身只读,不写任何文件、不改任何生产代码、不派发子代理。**
|
||||
|
||||
① **要解决的问题**:渲染端 `packages-user/client-modules` 与 `packages-user/client-base` 内仍有**单例**——即模块顶层实例化对象 `export const x = new X()`。规划日 `git grep -n -E "export const [A-Za-z0-9_]+ = new " -- packages-user/client-modules packages-user/client-base` 实测 **30** 条:client-modules **24**(`core.ts:6` `client`;`render/renderer.ts:18,20,35,43` `rafExcitation` / `excitationDivider` / `mainRenderer` / `using`;`render/scene.ts:3` `sceneController`;`render/ui/controller.tsx:11,29` `mainUIController` / `MainBackgroundUI`;`render/ui/*.tsx` 的 `LoadSceneUI` / `MainSceneUI` / `SaveUI` / 七个 settings UI / `StatisticsUI` / `leftStatusBarUI` / `rightStatusBarUI` / `GameTitleUI` / `ViewMapUI`;`shared.ts:105` `DEFAULT_FONT`)与 client-base **6**(`elements/cache.ts:280` `texture`;`components/choices.tsx:796,798` / `input.tsx:602` / `misc.tsx:591,592` 的 `GameUI` 实例)。用户 2026-10-01 裁定:**渲染端不得再有单例**。
|
||||
|
||||
② **锁定裁定(须逐条汇报原文要点)**:
|
||||
- **仅限单例**:本步**只**收集「模块顶层实例化对象 `export const x = new X()`」这一类。**不属于单例、一律 OUT**:可变模块级状态(`render/use.ts` 的 `nowOrientation` / `orientationHooks` / `transitionMap`)、全局注册表 / 类静态 `Map` / 计数器(`WeatherController.extern` / `.weathers`、`RenderColorTransition.key`)、全局符号(`action/hotkey.ts:5` `Symbol.for('@key_main')`)、全局访问器(`Mota.require` / `window.*` / `core.*`)、顶层副作用(`gameKey` 注册链 / `window.addEventListener` / 模块级 `bindExcitation`)、非 `new` 导出(`render/renderer.ts:45` 的解构导出 `createApp` / `render` / `tagManager` 及任何裸 / 解构函数)。这些**不写入台账任何小节、不设门禁**。
|
||||
- **去单例化**:现行一切单例,第二步须**挂到主类上**(`ClientCore` / `client`),主类允许多例(与 D-12「无全局单例、全部挂主类」一致)。
|
||||
- **接口继承 / 实现**:单例对应子系统须**继承 / 实现 `IClientBaseExtended`(`client-base/src/types.ts:27`)或 `ICoreStateExtended`(`data-state/src/types.ts:45`)**,即子系统接口携带 `readonly state` 指向主对象,**镜像数据端既有模式**(数据端单例 `data-state/src/ins.ts:11`;渲染端先例 `client-base/src/material/types.ts:94,218,406,439`)。
|
||||
- **两步走**:第一步(本计划 04-15)= **只读清点全部单例**(枚举 / 消费者 / 目标映射;生产代码零改动、不引入任何命名变更);第二步(后续 04-16)= 实际挂载 + 接口继承重构。**本计划不规划 04-16。**
|
||||
- **沿用约束**:D-33(范围外不改、不追平全部类型错误)、D-42 / D-48(`client-base/src/material/**` 归用户、AI 不得改)、D-49 / D-55 / D-65(不考虑任何 legacy)、D-56 精神(不触碰用户并发改动)、D-68(不要求可运行、不得以 `check:type` / `build` / TS 诊断数作为门禁)、D-69(桶导出边界,本步不触发)。
|
||||
|
||||
③ **源 → 目标映射方式(须原样呈报)**:本计划 = 04-15,唯一交付物 = `.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md`(只读台账),结构 = 元信息块 + `只读起始基线` / `背景` / `方法` / `枚举口径` / `A 模块级实例化对象` / `E 主类关系与目标挂载面映射` / `F 未能从阅读确定(未猜测)` / `单例→目标映射汇总(供 04-16 消费)` / `处置`;A / E 固定列序表 `| ID | 符号 / 项目 | 定义 file:line | 类型 / 导出机制 | 全部消费者 file:line | 当前归属 / 挂载 | 与 ClientCore / client 的关系 | 目标挂载 / 接口(证据或 undetermined) | 置信 |`;事实行每行 ≥2 个 `file:line` 锚点,证据行带 `[证据行]` 标记与核对命令;每个单例映射到「挂主类 + `IClientBaseExtended` / `ICoreStateExtended` 之一 + 先例锚点」,无法确定者写 undetermined 进 F 类(**只登记不判定**)。
|
||||
|
||||
④ **范围与硬边界(须原样呈报)**:被查对象 = `packages-user/client-modules/src/**` 与 `packages-user/client-base/src/**` 内全部 `export const x = new X()` 单例及其消费者;**只读基准** = `packages-user/data-*`(接口 / 单例先例,不修改)、`packages/**` 与 `src/**`(仅报告)。**零触碰** = 数据端一切生产文件(**尤其用户当前未提交的 `packages-user/data-system/src/combat/context.ts`,必须原样保留**)、`packages/**`、`src/**`、任何测试文件、任何 legacy 面、任何 `package.json` / lockfile。**生产源码零改动;无依赖变更、不执行 `pnpm i`。** **本步为只读台账、不引入任何命名变更**(不新增 / 改名 / 删除任何公共、受保护、私有成员、方法、字段、文件或目录);第二步如确需命名,台账只登记「待用户批准,不在本步引入」。
|
||||
|
||||
⑤ **验证方式(须原样呈报,并声明本步无运行时要求)**:D-68 的延续——本步**没有「保证能运行」的要求**。证明方式全部为**静态**:① **只读基线门禁**:`git status --porcelain -- packages packages-user src` 与 Task 1 (0a) 逐行一致,且每个生产树门禁**正向确认** ` M packages-user/data-system/src/combat/context.ts` 行在场、无其它条目、当前 `git hash-object` 与台账标记一致(回滚 / 再编辑均失败);`.planning/REQUIREMENTS.md` 零改动;② **台账结构门禁**:要求小节齐备、A / E 表存在、固定列序;③ **证据纪律门禁**:A 事实行每行 ≥2 个 `file:line` 锚点(证据行豁免)、`[证据行]` 标记未误用;④ **内容门禁**:`core.ts:6` 主单例、client-modules 与 client-base 两侧全部候选单例、`IClientBaseExtended` / `ICoreStateExtended` 映射与先例、F 类未确定、映射汇总均在案;⑤ **范围门禁**:`material/` / `packages/` / `src/` porcelain 为空;⑥ **既有产物保护**:`04-01`..`04-14` 的 `*-PLAN.md` / `*-SUMMARY.md` porcelain 为空、phase 目录内无 `04-16+`;⑦ **ROADMAP 登记门禁**:`.planning/ROADMAP.md` 的 Phase 4 段含 `04-15` / `Wave 14`;⑧ **CRLF 门禁**(台账与 SUMMARY);⑨ **人工复核**。**不得使用** `pnpm check:type` / `pnpm build` / 全仓(或范围内)TS 诊断数作为门禁或验收项。
|
||||
|
||||
⑥ **编号与 wave(须原样告知)**:本计划为 **04-15**(第十五个增量),`wave: 14`,`depends_on` 列 `04-01`..`04-14`;phase 目录内既有 `04-01`..`04-14` 的 `*-PLAN.md` 与 `*-SUMMARY.md` **一律不得改写 / 覆盖 / 改号**,也不得产出 `04-16+`。本计划估算 **45k tokens**(`estimate.tasks: 5`),在单计划 smart-zone 预算内。
|
||||
|
||||
⑦ **盖章效力(须原样告知)**:本计划为**只读**增量,**生产代码改动为 0**;只写一个台账文档(+ 计划 / SUMMARY 簿记 + ROADMAP 登记)。无任何代码改动需回退。
|
||||
|
||||
⑧ **关卡性质与失败语义(必须原样告知用户)**:无论触发方式(`/gsd-execute-phase`、`/gsd-manager`、`/gsd-autonomous`、子代理/执行器派发)或配置模式(`.planning/config.json` 的 `mode: yolo`、`workflow.auto_advance`)如何,**执行者 / 子代理都必须在此停下并等待**用户明确回复「可以执行」四个字;未收到这四个字前不得执行任何任务、不得派发执行子代理,也不得以「自动化模式」为由自动放行。用户未回复「可以执行」即继续,或回复的是其它任何内容(含修订意见、沉默、自动化模式下的自动推进、子代理的自行放行),一律视为**未通过本关卡**:必须立即停止执行、保持只读、不写任何文件,直到用户明确回复「可以执行」四个字后方可继续。
|
||||
|
||||
⑨ **若用户给出修订意见**(含要求把某类非单例(如可变模块级状态 / 全局访问器 / 顶层副作用)重新纳入 / 要求纳入更多包 / 否决单例口径 / 不同意台账结构 / 直接给出某单例的目标接口裁定):退出本计划、修订 `04-15-PLAN.md` 后重新汇报,不得自行改道执行。
|
||||
</action>
|
||||
<decision>是否按本计划(04-15:只读收集渲染端 `client-modules` + `client-base` 的**全部单例**——仅「模块顶层实例化对象 `export const x = new X()`」,逐条枚举定义 / 消费者 / 当前挂载 / 与 `ClientCore` 的关系,并映射到「挂主类 + `IClientBaseExtended` / `ICoreStateExtended` 之一 + 先例」,产出 `04-RENDER-SINGLETON-AUDIT.md`;可变模块级状态 / 全局注册表 / 全局访问器 / 顶层副作用 / 非 `new` 导出一律 OUT;生产代码零改动、不引入任何命名变更、无运行时要求、不规划第二步)开始执行</decision>
|
||||
<context>
|
||||
AGENTS.md 要求任何计划的第一个 block 点是向用户汇报计划内容并等待用户确认。`.planning/config.json` 为 `mode: yolo`,因此该关卡必须显式声明为 `blocking-human` 并置 `autonomous: false`,否则计划层面不提供任何 block 点。本次存在会改变台账范围 / 判断的边界(「单例」的严格定义;哪些非单例对象被排除;外部包框架级全局 `mainRenderer` / `using` / `sceneController` / `mainUIController` 能否挂主类;`GameUI` / `UIController` 实例是否属「须挂主类」的对象;`render/renderer.ts` 与 `ClientCore` 构造器内重复的激励源 / 渲染器以哪一份为权威;目标接口选 `IClientBaseExtended` 还是 `ICoreStateExtended`),以及一条会放宽验收的既有裁定(D-68:不要求可运行、不要求零类型错误)必须让用户明确知晓,避免执行者事后用「类型不干净」为由扩大范围或阻塞交付。用户当前有未提交改动 `packages-user/data-system/src/combat/context.ts`,本计划必须完全不触碰它。
|
||||
</context>
|
||||
<options>
|
||||
<option id="execute">
|
||||
<name>可以执行</name>
|
||||
<pros>按本计划只读收集:产出 `04-RENDER-SINGLETON-AUDIT.md` 台账(仅单例),不产生任何生产代码改动、不引入任何命名变更;后续第二步(04-16)据此挑选工作集</pros>
|
||||
<cons>无(只读台账;正确性由静态门禁 + 人工复核共同证明)</cons>
|
||||
</option>
|
||||
<option id="widen-scope">
|
||||
<name>可以执行,但把某类非单例一并纳入</name>
|
||||
<pros>把可变模块级状态 / 全局注册表 / 全局访问器 / 顶层副作用等一并纳入正式收集范围</pros>
|
||||
<cons>超出用户「只收集单例」的裁定范围;需修订本计划并重新汇报</cons>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>修订单例口径 / 枚举面 / 台账结构后再执行</name>
|
||||
<pros>避免带着错误的口径或结构落地后再返工</pros>
|
||||
<cons>退出本计划并修订 PLAN.md 后重新汇报,增加一轮往返</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>回复「可以执行」四个字(修订意见请按 action ⑨ 处理)</resume-signal>
|
||||
<verify>
|
||||
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对用户回复原文;本关卡为人工关卡,无自动命令,失败语义写在 Task 0 的 action ⑧ 与 acceptance_criteria 中)</human-check>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 用户回复中明确包含「可以执行」四个字
|
||||
- 汇报内容涵盖:要解决的问题(渲染端两个包内的单例 `export const x = new X()`,规划日实测 30 条)、用户 2026-10-01 裁定(去单例化 / 挂主类 / 继承或实现 `IClientBaseExtended` / `ICoreStateExtended` / 镜像数据端模式 / 两步走 / **仅限单例**)、枚举面与台账结构(元信息块 + 9 小节 + A / E / F 三类)、源→目标映射方式、范围与硬边界(数据端 / `packages/` / `src/` / 用户未提交 `combat/context.ts` 零触碰 + 无依赖变更 + 本步不引入任何新命名)、验证方式
|
||||
- 汇报中已原样告知**排除清单**(可变模块级状态 / 全局注册表 / 全局访问器 / 顶层副作用 / 非 `new` 导出一律 OUT)
|
||||
- 汇报中已原样告知 **D-68 的延续**(没有「保证能运行」的要求;不设 `check:type` / `build` / TS 诊断数类门禁)与 **本步不引入任何新命名**
|
||||
- 汇报中已原样告知 **编号与 wave**(04-15 / wave 14 / `depends_on` 04-01..04-14)以及既有 `04-01`..`04-14` 产物不得改写 / 覆盖 / 改号、不得产出 `04-16+`
|
||||
- 汇报中已原样告知**关卡性质**:执行者 / 子代理必须停下等待,不因触发方式或 `yolo` / `auto_advance` 模式自动放行
|
||||
</acceptance_criteria>
|
||||
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
|
||||
</task>
|
||||
<task type="tracer">
|
||||
<name>Task 1(tracer):一条线端到端走通 —— 以 `client-base/src/elements/cache.ts:280` 的 `texture = new TextureCache()` 为唯一真实单例,逐节落成台账骨架(定义 → 消费者 → 当前挂载 → 目标接口证据 → undetermined → 映射汇总)</name>
|
||||
<precondition>Task 0 已闭环:用户在对话中明确回复「可以执行」四个字(未收到则不得开始本任务)</precondition>
|
||||
<files>.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md</files>
|
||||
<read_first>
|
||||
- packages-user/client-base/src/elements/cache.ts(`class TextureCache` 定义 `:48-278`、`export const texture = new TextureCache()` `:280`——本任务的 tracer 对象)
|
||||
- packages-user/client-base/src/elements/index.ts(`texture` 经 barrel 的导出面)
|
||||
- packages-user/client-base/src/index.ts:1-9(8 条同目录 `export *`,`./elements` 在列——`texture` 的间接导出面)
|
||||
- packages-user/client-base/src/types.ts:8,27(`IClientBase extends ICoreState`;`IClientBaseExtended { readonly state: IClientBase; }`——目标接口)
|
||||
- packages-user/data-state/src/types.ts:45(`ICoreStateExtended { readonly state: ICoreState; }`——目标接口)
|
||||
- packages-user/client-base/src/material/types.ts:10,94,218,406,439(`ICoreStateExtended` 的 4 处 `extends` 先例——目标模式证据)
|
||||
- packages-user/client-base/src/material/manager.ts 与 builder.ts / autotile.ts(D-35「`state` 统一通过构造器传入」先例)
|
||||
- packages-user/data-state/src/ins.ts:11(数据端单例 `state`,对照锚点)
|
||||
- .planning/phases/04-render-adaptation/04-UI-ADAPTATION-IMPACT.md(台账版式:元信息块 / 方法 / 表 / 处置)与 04-STRUCTURE-MIGRATION-IMPACT.md(证据纪律:`[证据行]` 标记与 ≥2 锚点规则)
|
||||
- dev.md(「模块原则」的无副作用 / 不转发导出;「其他要求」的 CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(0a) **起始基线快照(只读前置,先做这一步)**:执行 `git status --porcelain -- packages packages-user src`,把输出**原样逐行**记为「只读起始基线」写入台账 `## 只读起始基线`:先写 bullet `- **只读起始基线(packages / packages-user / src)**:`,其后逐行抄录(每条一行,行首两字符状态码;输出为空则写 `(无)`)。规划日实测应含用户未提交的 `packages-user/data-system/src/combat/context.ts`(以当日实测为准)。另记 `git rev-parse HEAD` 与 `git rev-parse --abbrev-ref HEAD` 当日值;并执行 `git hash-object -- packages-user/data-system/src/combat/context.ts`,把其 40 位哈希**原样**写成台账 `## 只读起始基线` 的机读标记行 `<!-- baseline-hash-combat-context: <40hex> -->`(ASCII,供 Task 1/2/3/4 生产树门禁解析比对);同时断言 ` M packages-user/data-system/src/combat/context.ts` 这一行在场。**不得回滚、暂存、提交或修改**任何既有改动。
|
||||
|
||||
(1) **建台账骨架(元信息块 + 各小节标题 + 固定表头)**:用 Write 工具创建交付物 `.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md`(CRLF)。写入:
|
||||
- 标题 `# 渲染端去单例化·只读清点(第一步)`;
|
||||
- **元信息块**(固定 6 行):`**日期:**`(当日)/ `**范围:**`(被查 = `packages-user/client-modules/src/**` + `packages-user/client-base/src/**` 内全部**单例**(`export const x = new X()`)及其消费者;只读基准 = `packages-user/data-*`;`packages/` 与 `src/` 仅报告(D-33))/ `**性质:**`(只读清点(去单例化第一步),未修改任何生产代码,未引入任何命名变更)/ `**基准:**`(当日 HEAD 与分支)/ `**目标模式:**`(挂主类 `ClientCore` / `client` + 单例对应子系统继承 / 实现 `IClientBaseExtended` 或 `ICoreStateExtended`,镜像数据端模式)/ `**下一步:**`(第二步 04-16 未规划,待用户审阅本文件后另行规划);
|
||||
- `## 只读起始基线`(填 (0a) 内容,含 `<!-- baseline-hash-combat-context: ... -->` 标记行);
|
||||
- `## 背景`(用户 2026-10-01 裁定要点 + 「本步只清点单例、不修改、不规划第二步」);
|
||||
- `## 方法`(枚举面两个包;核对手法 `git grep -n -E "export const [A-Za-z0-9_]+ = new "` / 阅读;证据纪律:A 事实行 ≥2 个 `file:line` 锚点、证据行带 `[证据行]` 标记 + 核对命令;只登记不判定);
|
||||
- `## 枚举口径`(**单例定义唯一**:模块顶层实例化对象 `export const x = new X()`;**显式排除清单**:可变模块级状态 / 全局注册表 / 类静态 `Map` / 计数器 / 全局符号 / 全局访问器(`Mota.require` / `window.*` / `core.*`)/ 顶层副作用 / 非 `new` 导出(解构导出、裸函数));
|
||||
- `## A 模块级实例化对象`、`## E 主类关系与目标挂载面映射` 两节,各写入固定表头 `| ID | 符号 / 项目 | 定义 file:line | 类型 / 导出机制 | 全部消费者 file:line | 当前归属 / 挂载 | 与 ClientCore / client 的关系 | 目标挂载 / 接口(证据或 undetermined) | 置信 |` 与 `|---|---|---|---|---|---|---|---|---|`;`## F 未能从阅读确定(未猜测)` 写「条目形如 `- **#04-15-F-NN**:现象 + 为什么读不出来 + 需要用户裁决的点。**只登记,不判定。**」;
|
||||
- `## 单例→目标映射汇总(供 04-16 消费)`(固定表头 `| 定义 file:line | 符号 | 类型 | 目标(挂主类 + 目标接口,或 undetermined) | 先例 file:line | 归属包 | 置信 |`)与 `## 处置`(只读完成;第二步待用户审阅后另行规划)。
|
||||
**不得写入任何 `## B` / `## C` / `## D` 类小节。**
|
||||
|
||||
(2) **tracer 行 —— A 类(`texture` 模块级实例化对象)**:在 `## A` 表填入以 `#04-15-A-01` 起编号的事实行,逐条覆盖 `client-base/src/elements/cache.ts:280`(`export const texture = new TextureCache()`)的定义锚点 `:48`(`class TextureCache`);用 `git grep` 定位其**全部消费者**并逐条给出 `文件:行`(含经 `elements/index.ts` barrel 与 `client-base/src/index.ts` 的间接导出面);每行 `定义 file:line` + `消费者 file:line` 至少 2 个锚点。**若当日实测消费者为空或形态不同,按当日实测记录,不得按规划日臆测。**
|
||||
|
||||
(3) **tracer 行 —— E / F / 映射汇总(先只填 tracer 相关)**:
|
||||
- `## E`:登记 `texture` 的当前归属(`client-base/src/elements/cache.ts` 模块级导出、经 `@user/client-base` 公共面暴露)与「与 `ClientCore` / `client` 的关系」(规划日实测 `ClientCore` 未持有 `texture` 字段,`client.ts:43-56` 无对应成员——以当日实测为准);给出目标接口候选并附证据 —— `ICoreStateExtended`(`packages-user/data-state/src/types.ts:45`)或 `IClientBaseExtended`(`packages-user/client-base/src/types.ts:27`);渲染 / 系统层先例 `packages-user/client-base/src/material/types.ts:94,218,406,439`(四个接口 `extends` 数据端 `ICoreStateExtended`)与数据端先例 `packages-user/data-state/src/ins.ts:11`;**无法确定选哪一个即写 undetermined 并交叉引用 F 类,不得臆测。**
|
||||
- `## F`:填入 tracer 相关未确定项(如 `texture` 消费者画像 / 是否须挂主类 / 目标接口取舍)。
|
||||
- `## 单例→目标映射汇总`:填 `texture` 对应行(定义 / 符号 / 类型 / 目标或 undetermined / 先例 / 归属包 / 置信)。
|
||||
|
||||
(4) **CRLF 与收口**:确认台账为 CRLF;本任务只允许创建 / 编辑该台账文件,**不得**改任何生产文件、不得新增计划文件、不得派发会写文件的子代理;**不得**执行 `pnpm check:type` / `pnpm build`。
|
||||
</action>
|
||||
<reversibility rating="reversible">本任务只创建一个 `.planning/` 下的只读台账文档,无任何生产代码 / 契约 / 命名变更,全部内容可用 git 回退或重跑本计划重建。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const f='.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md';if(!fs.existsSync(f)){console.error('MISSING '+f);process.exit(1)}const t=fs.readFileSync(f,'utf8');const need=['只读起始基线','背景','方法','枚举口径','A 模块级实例化对象','E 主类关系与目标挂载面映射','F 未能从阅读确定','单例→目标映射汇总','处置'];const miss=need.filter(k=>t.indexOf(k)<0);if(miss.length>0){console.error('SECTION MISSING :: '+miss.join(','));process.exit(1)}const forbid=['## B 可变模块级状态','## C 全局访问器','## D 顶层副作用'];const bad=forbid.filter(k=>t.indexOf(k)>=0);if(bad.length>0){console.error('OUT-OF-SCOPE SECTION PRESENT :: '+bad.join(','));process.exit(1)}if(t.indexOf('#04-15-A-01')<0){console.error('TRACER ROW A-01 MISSING');process.exit(1)}if(t.indexOf('baseline-hash-combat-context:')<0){console.error('BASELINE HASH MARKER MISSING');process.exit(1)}console.log('OK ledger skeleton (A/E/F) + tracer row + hash marker');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "MISSING" / "SECTION MISSING" / "OUT-OF-SCOPE SECTION PRESENT" / "TRACER ROW A-01 MISSING" / "BASELINE HASH MARKER MISSING"(台账未创建、要求小节缺失、误含 B/C/D 小节、tracer 行 `#04-15-A-01` 未落盘、或基线哈希标记缺失)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md','utf8').replace(/\r\n/g,'\n');const rows=t.split('\n').filter(l=>/^\|\s*#04-15-A-\d+/.test(l));const bad=[];for(const r of rows){const ev=r.indexOf('[证据行]')>=0;const n=(r.match(/[A-Za-z0-9_.\/-]+\.(?:tsx?|md|json):\d+/g)||[]).length;if(!ev&&n<2)bad.push('ROW <2 anchors :: '+r.slice(0,70))}if(rows.length<1)bad.push('TOO FEW ROWS ('+rows.length+')');if(bad.length>0){console.error('EVIDENCE DISCIPLINE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK anchor discipline: '+rows.length+' A rows');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "EVIDENCE DISCIPLINE FAILED" / "ROW <2 anchors" / "TOO FEW ROWS"(A 事实行少于 2 个 `file:line` 锚点,或 A 行不足 1 条)</fails_when>
|
||||
<automated>node -e "const{execSync}=require('child_process');const fs=require('fs');const LEDGER='.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md';const FILE='packages-user/data-system/src/combat/context.ts';const WANT=' M '+FILE;const o=execSync('git status --porcelain -- packages-user/client-modules packages-user/client-base packages-user/data-state packages-user/data-system packages packages/ src',{encoding:'utf8'}).replace(/\r/g,'');const lines=o.split('\n').map(l=>l.replace(/[ \t]+$/,'')).filter(l=>l.length>0);const t=fs.readFileSync(LEDGER,'utf8');const m=t.match(/<!-- baseline-hash-combat-context: ([0-9a-f]{40}) -->/);const bad=[];if(!m)bad.push('BASELINE HASH MARKER MISSING');if(lines.indexOf(WANT)<0)bad.push('USER EDIT MISSING');const extra=lines.filter(l=>l!==WANT);if(extra.length>0)bad.push('PRODUCTION TREE TOUCHED :: '+extra.join(' ; '));if(m){const cur=execSync('git hash-object -- '+FILE,{encoding:'utf8'}).trim();if(cur!==m[1])bad.push('USER EDIT HASH DRIFT '+cur+' vs '+m[1]);}if(bad.length>0){console.error('PRODUCTION GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK read-only baseline confirmed (user edit present, hash matches, no other changes)');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "PRODUCTION GATE FAILED" / "BASELINE HASH MARKER MISSING" / "USER EDIT MISSING" / "PRODUCTION TREE TOUCHED" / "USER EDIT HASH DRIFT"(用户未提交的 `combat/context.ts` 不在场(被回滚 / 提交)/ 被再编辑(哈希漂移)/ 台账缺 `<!-- baseline-hash-combat-context: ... -->` 标记 / 出现其它生产改动 —— 违反只读硬约束与 D-33 / D-42 / D-48 / D-56 精神)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const f='.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md';const s=fs.readFileSync(f,'utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;if(lf!==crlf){console.error('NON-CRLF '+f);process.exit(1)}console.log('OK CRLF ledger');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "NON-CRLF"(台账出现非 CRLF 换行,违反 dev.md「换行使用 CRLF 格式」)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `04-RENDER-SINGLETON-AUDIT.md` 存在,含标题 + 6 行元信息块 + `只读起始基线` / `背景` / `方法` / `枚举口径` / `A 模块级实例化对象` / `E 主类关系与目标挂载面映射` / `F 未能从阅读确定` / `单例→目标映射汇总` / `处置`;**无 B / C / D 小节**
|
||||
- `## 只读起始基线` 逐行原样记录当日起始 porcelain(含用户未提交的 `combat/context.ts`,以当日实测为准)并含 `<!-- baseline-hash-combat-context: ... -->` 标记行
|
||||
- `## A` 至少有 `#04-15-A-01`(`texture = new TextureCache()` 的定义 + 全部消费者),每行 ≥2 个 `file:line` 锚点
|
||||
- `## E` 为 `texture` 给出目标接口候选(`IClientBaseExtended` / `ICoreStateExtended`)与先例 `file:line` 证据,或显式 undetermined
|
||||
- `## F` 有 tracer 相关条目;`## 单例→目标映射汇总` 有 `texture` 对应行
|
||||
- 台账为 CRLF;生产源码零改动(除用户既有 `combat/context.ts` 外 porcelain 为空);未新增计划文件;未派发会写文件的子代理
|
||||
</acceptance_criteria>
|
||||
<done>端到端一条线走通:`texture` 这一个真实单例已在台账每一节留下带 `file:line` 锚点的痕迹(定义 → 消费者 → 当前归属 / 与主类关系 → 目标接口证据 → undetermined → 映射汇总);门禁(结构 / 锚点 / 只读 / CRLF)全绿;台账无 B/C/D 小节;后续任务只需按同一形态横向铺开其余单例</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2:横向铺开 client-modules 单例 —— `client` / renderer 实例簇 / `sceneController` / `mainUIController` / `MainBackgroundUI` / 各 `GameUI` 实例 / `DEFAULT_FONT`</name>
|
||||
<precondition>Task 0 已闭环(用户已回复「可以执行」)且 Task 1 已交付(台账骨架 + tracer 行 `#04-15-A-01` 落盘)</precondition>
|
||||
<files>.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md</files>
|
||||
<read_first>
|
||||
- packages-user/client-modules/src/core.ts:1-11(`:3`「逐渐弱化 ClientCore 的单例概念」TODO;`:6` `export const client = new ClientCore({...})` 主单例)
|
||||
- packages-user/client-modules/src/client.ts:41-138(`class ClientCore extends CoreState implements IClientCore`;`:43-56` 主类字段;`:66,71-73,79,90-91,106-116` 构造器内实例化)
|
||||
- packages-user/client-modules/src/render/renderer.ts:18,20,35,43(`rafExcitation` / `excitationDivider` / `mainRenderer` / `using` 四个 `new` 单例;`:45` 的解构导出 `createApp` / `render` / `tagManager` 属**非单例**,不登记)
|
||||
- packages-user/client-modules/src/render/scene.ts:3(`sceneController = new UIController('main-scene')`)与 render/ui/controller.tsx:11,29(`mainUIController = new UIController('main-ui')` / `MainBackgroundUI = new GameUI(...)`)
|
||||
- packages-user/client-modules/src/render/ui/{load.tsx:161,main.tsx:312,save.tsx:462,statistics.tsx:302,statusBar.tsx:488-489,title.tsx:484,viewmap.tsx:556,settings.tsx:663-678}(各 `new GameUI(...)` 实例)
|
||||
- packages-user/client-modules/src/shared.ts:105(`export const DEFAULT_FONT = new Font('Verdana', 16)`)
|
||||
- packages-user/client-base/src/types.ts:27 与 packages-user/data-state/src/types.ts:45(目标接口)/ packages-user/client-base/src/material/types.ts:94,218,406,439(先例)
|
||||
- dev.md(「模块原则」的无副作用 / 不转发导出 / 无循环引用;「其他要求」的 CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(0a) **复验起始基线**:再次执行 `git status --porcelain -- packages packages-user src` 并与台账 `## 只读起始基线` 逐行比对;不一致则在台账 `## 处置` 记差异,**不得**改用户改动。
|
||||
|
||||
(1) **A 类 —— client-modules 的模块级实例化对象**:在 `## A` 表追加事实行(`#04-15-A-02` 起),逐条覆盖(带定义 `file:line` + 消费者 `file:line`,每行 ≥2 锚点):`core.ts:6`(`client` 主单例——标注为「主挂载类实例本身」);`render/renderer.ts:18,20,35,43`(`rafExcitation` / `excitationDivider` / `mainRenderer` / `using`);`render/scene.ts:3`(`sceneController`);`render/ui/controller.tsx:11,29`(`mainUIController` / `MainBackgroundUI`);各 UI `GameUI` 实例(`load.tsx:161` `LoadSceneUI` / `main.tsx:312` `MainSceneUI` / `save.tsx:462` `SaveUI` / `settings.tsx:663,665,667,669,671,673,678` / `statistics.tsx:302` `StatisticsUI` / `statusBar.tsx:488,489` `leftStatusBarUI` / `rightStatusBarUI` / `title.tsx:484` `GameTitleUI` / `viewmap.tsx:556` `ViewMapUI`);`shared.ts:105`(`DEFAULT_FONT`)。用 `git grep -n` 定位每项的全部消费者。**只登记,不判定。** **不得登记 `render/renderer.ts:45` 的解构导出或任何非 `new` 导出。**
|
||||
|
||||
(2) **E 类 —— 主类关系与目标挂载面映射(client-modules 侧)**:为 (1) 的每一项给出:当前归属 / 挂载;与 `ClientCore` / `client` 的关系(对照 `client.ts:43-56` 现有字段——如 `render/renderer.ts` 的 `mainRenderer` 与 `ClientCore.renderer`(`client.ts:108`)为两个不同实例,登记为「重复单例,归属未定」并交叉引用 F 类 (b));目标(挂主类 + `IClientBaseExtended` 或 `ICoreStateExtended`,附 `packages-user/client-base/src/types.ts:27` / `packages-user/data-state/src/types.ts:45` 与 `packages-user/client-base/src/material/types.ts:94,218,406,439` 证据)。**无法确定写 undetermined。**
|
||||
|
||||
(3) **F 类 —— 登记单例相关未确定项**:追加 `#04-15-F-0N`(覆盖 (a) `mainRenderer` / `using` / `sceneController` / `mainUIController` 等外部包单例能否挂主类;(b) `render/renderer.ts` 与 `client.ts:90,91,108` 重复单例的权威归属;(c) 各 `*UI`(`GameUI` / `UIController` 实例)是否属「须挂主类」对象)。逐条写「现象 + 为什么读不出来 + 需要用户裁决的点」,**只登记不判定**;**不得加入任何非单例条目**。
|
||||
|
||||
(4) **同步更新 `## 单例→目标映射汇总`**:为 (1) 各项追加对应行(定义 / 符号 / 类型 / 目标或 undetermined / 先例 / 归属包 / 置信)。**只汇总本台账已登记条目。**
|
||||
|
||||
(5) **CRLF 与收口**:只允许编辑该台账文件;**不得**改任何生产文件、不得新增计划文件、不得派发会写文件的子代理;**不得**执行 `pnpm check:type` / `pnpm build`。
|
||||
</action>
|
||||
<reversibility rating="reversible">本任务只向 `.planning/` 下的只读台账追加事实行,无任何生产代码 / 契约 / 命名变更,可用 git 回退或重跑本计划重建。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md','utf8');const need=['core.ts:6','render/renderer.ts:18','render/renderer.ts:20','render/renderer.ts:35','render/renderer.ts:43','render/scene.ts:3','render/ui/controller.tsx:11','render/ui/controller.tsx:29','LoadSceneUI','MainSceneUI','SaveUI','StatisticsUI','GameTitleUI','ViewMapUI','DEFAULT_FONT','shared.ts:105','#04-15-F-0','单例→目标映射汇总'];const miss=need.filter(k=>t.indexOf(k)<0);if(miss.length>0){console.error('CLIENT-MODULES EXPANSION MISSING :: '+miss.join(' | '));process.exit(1)}console.log('OK client-modules A expansion');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "CLIENT-MODULES EXPANSION MISSING"(client-modules 某个 A 单例(`client` / renderer 实例簇 / scene / UI 控制器 / 各 `GameUI` 实例 / `DEFAULT_FONT`)未落盘,或 F 类 / 映射汇总缺失)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md','utf8').replace(/\r\n/g,'\n');const rows=t.split('\n').filter(l=>/^\|\s*#04-15-A-\d+/.test(l));const bad=[];for(const r of rows){const ev=r.indexOf('[证据行]')>=0;const n=(r.match(/[A-Za-z0-9_.\/-]+\.(?:tsx?|md|json):\d+/g)||[]).length;if(!ev&&n<2)bad.push('ROW <2 anchors :: '+r.slice(0,70))}const f=t.split('\n').filter(l=>/^- \*\*#04-15-F-\d+/.test(l));if(rows.length<10)bad.push('TOO FEW ROWS ('+rows.length+')');if(f.length<3)bad.push('F rows <3 ('+f.length+')');if(bad.length>0){console.error('EVIDENCE DISCIPLINE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK anchor discipline: '+rows.length+' A rows, '+f.length+' F entries');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "EVIDENCE DISCIPLINE FAILED" / "ROW <2 anchors" / "TOO FEW ROWS" / "F rows"(A 事实行锚点不足,A 行少于 10,或 F 类少于 3 条)</fails_when>
|
||||
<automated>node -e "const{execSync}=require('child_process');const fs=require('fs');const LEDGER='.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md';const FILE='packages-user/data-system/src/combat/context.ts';const WANT=' M '+FILE;const o=execSync('git status --porcelain -- packages-user/client-modules packages-user/client-base packages-user/data-state packages-user/data-system packages packages/ src',{encoding:'utf8'}).replace(/\r/g,'');const lines=o.split('\n').map(l=>l.replace(/[ \t]+$/,'')).filter(l=>l.length>0);const t=fs.readFileSync(LEDGER,'utf8');const m=t.match(/<!-- baseline-hash-combat-context: ([0-9a-f]{40}) -->/);const bad=[];if(!m)bad.push('BASELINE HASH MARKER MISSING');if(lines.indexOf(WANT)<0)bad.push('USER EDIT MISSING');const extra=lines.filter(l=>l!==WANT);if(extra.length>0)bad.push('PRODUCTION TREE TOUCHED :: '+extra.join(' ; '));if(m){const cur=execSync('git hash-object -- '+FILE,{encoding:'utf8'}).trim();if(cur!==m[1])bad.push('USER EDIT HASH DRIFT '+cur+' vs '+m[1]);}if(bad.length>0){console.error('PRODUCTION GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK read-only baseline confirmed (user edit present, hash matches, no other changes)');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "PRODUCTION GATE FAILED" / "BASELINE HASH MARKER MISSING" / "USER EDIT MISSING" / "PRODUCTION TREE TOUCHED" / "USER EDIT HASH DRIFT"(用户未提交的 `combat/context.ts` 不在场(被回滚 / 提交)/ 被再编辑(哈希漂移)/ 台账缺标记 / 出现其它生产改动 —— 违反只读硬约束)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const f='.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md';const s=fs.readFileSync(f,'utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;if(lf!==crlf){console.error('NON-CRLF '+f);process.exit(1)}console.log('OK CRLF ledger');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "NON-CRLF"(台账出现非 CRLF 换行)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `## A` 追加 client-modules 侧全部 A 单例事实行(`core.ts:6` / renderer 实例簇 / `sceneController` / `mainUIController` / `MainBackgroundUI` / 各 `GameUI` 实例 / `DEFAULT_FONT`),每行 ≥2 锚点;**未登记任何非 `new` 导出**
|
||||
- `## E` 为上述每处给出「与 `ClientCore` / `client` 的关系」与目标接口候选(附证据)或显式 undetermined
|
||||
- `## F` 新增 ≥3 条**单例相关**未确定项,均写「现象 + 读不出原因 + 需用户裁决的点」,无「错配 / 缺失 / 残留 / 待办」措辞;无非单例条目
|
||||
- `## 单例→目标映射汇总` 已同步;台账为 CRLF;生产源码零改动(除用户既有 `combat/context.ts` 外)
|
||||
</acceptance_criteria>
|
||||
<done>client-modules 单例横向铺开完成:`client`、renderer 实例簇、scene / UI 控制器与各 `GameUI` 实例、`DEFAULT_FONT` 均在案(带 `file:line`);E 类给出与主类关系与目标接口证据或 undetermined;F 类新增 ≥3 条;门禁(内容 / 锚点 / 只读 / CRLF)全绿</done>
|
||||
</task>
|
||||
<task type="auto">
|
||||
<name>Task 3:铺开 client-base 单例(`texture` 补消费者 / `ConfirmBoxUI` / `ChoicesUI` / `InputBoxUI` / `WaitBoxUI` / `BackgroundUI`)+ E 映射定稿 + `git grep` 完整性反查 + `## 单例→目标映射汇总` 定稿</name>
|
||||
<precondition>Task 0 已闭环(用户已回复「可以执行」)且 Task 1 / Task 2 已交付(台账骨架 + tracer 行 + client-modules 铺开)</precondition>
|
||||
<files>.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md</files>
|
||||
<read_first>
|
||||
- packages-user/client-base/src/elements/cache.ts:48,280 与 elements/index.ts(`texture` 定义 + barrel;Task 1 已登记,本任务补消费者若有新增)
|
||||
- packages-user/client-base/src/components/{choices.tsx:796,798,input.tsx:602,misc.tsx:591,592}(`new GameUI(...)` 实例:`ConfirmBoxUI` / `ChoicesUI` / `InputBoxUI` / `WaitBoxUI` / `BackgroundUI`)与 components/index.ts(导出面)
|
||||
- packages-user/client-base/src/material/types.ts:10,94,218,406,439 与 manager.ts / builder.ts / autotile.ts(`ICoreStateExtended` 先例;`state` 构造器注入)
|
||||
- packages-user/client-base/src/types.ts:8,27 与 packages-user/data-state/src/types.ts:45(目标接口);packages-user/data-state/src/ins.ts:11(数据端单例)
|
||||
- packages-user/client-base/src/index.ts:1-9(8 条同目录 `export *`;D-69 边界——本步不改任何 barrel,仅作只读参照)
|
||||
- .planning/phases/04-render-adaptation/04-STRUCTURE-MIGRATION-IMPACT.md(只报告措辞)与 04-UI-ADAPTATION-IMPACT.md(台账写法)
|
||||
- dev.md(「模块原则」的无副作用 / 不转发导出 / 无循环引用;「其他要求」的 CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(0a) **复验起始基线**:与 `## 只读起始基线` 逐行比对;不一致在 `## 处置` 记差异,**不得**改用户改动。
|
||||
|
||||
(1) **A 类 —— client-base 模块级实例化对象铺开**:在 `## A` 表补入 `client-base` 侧事实行(带定义 + 消费者 `file:line`):`elements/cache.ts:280`(tracer 已登记,此处只补消费者若有新增);`components/{choices.tsx:796,798,input.tsx:602,misc.tsx:591,592}` 的 `new GameUI(...)` 实例(`ConfirmBoxUI` / `ChoicesUI` / `InputBoxUI` / `WaitBoxUI` / `BackgroundUI`)。
|
||||
|
||||
(2) **E 类 —— 目标模式映射定稿(client-base 侧)**:为 `texture` 及 client-base 各单例给出目标(挂主类 + `IClientBaseExtended` 或 `ICoreStateExtended`),附渲染 / 系统层先例 `packages-user/client-base/src/material/types.ts:94,218,406,439`(四个渲染 / 系统层接口 `extends` 数据端 `ICoreStateExtended`)与数据端先例 `packages-user/data-state/src/ins.ts:11`;并**显式对照** `IClientBaseExtended`(`packages-user/client-base/src/types.ts:27`,携带 `readonly state: IClientBase`,`IClientBase extends ICoreState`)与 `ICoreStateExtended`(`packages-user/data-state/src/types.ts:45`)的取舍;**无法确定写 undetermined 并交叉引用 F 类。**
|
||||
|
||||
(3) **T 类 —— `git grep` 完整性反查(`[证据行]`)**:执行 `git grep -n -E "export const [A-Za-z0-9_]+ = new " -- packages-user/client-modules packages-user/client-base`,把命令与原样输出作为一条 `[证据行]` 登记;并逐条对齐台账 `## A` 表的 `#04-15-A-NN` 行集合与 grep 结果(A-01.. 与之对齐),**证实无遗漏、无多登**(多登即把非 `new` 导出写入,须删)。规划日实测 **30** 条(client-modules 24 / client-base 6);执行日以当日实测为准。
|
||||
|
||||
(4) **F 类 —— 收口**:补入单例相关未确定项 (d) 目标接口取舍 / (e) `IClientBaseExtended` 无实现类 / (f) 无消费者的 `statusBar` UI(`leftStatusBarUI` / `rightStatusBarUI`);确认 F 类条目均只涉及 A 单例,**无任何非单例条目**。
|
||||
|
||||
(5) **`## 单例→目标映射汇总` 定稿**:以固定列序表逐条汇总**本台账已登记**的全部单例:`定义 file:line | 符号 | 类型 | 目标(挂主类 + 目标接口,或 undetermined) | 先例 file:line | 归属包 | 置信 |`。**只汇总已登记事实,不得新增未登记事实、不得给代码写法。** 写明两个包内单例总数(客户端 / 系统层分列;规划日实测 24 + 6 = 30),供用户审阅。
|
||||
|
||||
(6) **`## 处置` 定稿**:写「本文件是渲染端去单例化(用户 2026-10-01 裁定)第一步(只读收集单例)的交付物;本步未修改任何生产代码、未引入任何命名变更;相对 `## 只读起始基线` 零变化(用户既有 `combat/context.ts` 改动未被触碰);第二步(04-16:挂载 + 接口继承重构)未规划,待用户审阅本文件后另行规划;本文件**仅登记单例**,不含可变模块级状态 / 全局访问器 / 顶层副作用等非单例,也不含挂载方案 / import 改写方案 / 新 `*-PLAN.md`;`REND-01` / `REND-02` 保持 Pending」。如当日实测与规划日数值有差异,按 assumptions 的「以当日实测为准」如实写明。
|
||||
|
||||
(7) **CRLF 与收口**:只允许编辑该台账文件;**不得**改任何生产文件、不得新增计划文件、不得派发会写文件的子代理;**不得**执行 `pnpm check:type` / `pnpm build`。
|
||||
</action>
|
||||
<reversibility rating="reversible">本任务只向 `.planning/` 下的只读台账补齐 client-base 单例与映射汇总,无任何生产代码 / 契约 / 命名变更,可用 git 回退或重跑本计划重建。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md','utf8');const need=['elements/cache.ts:280','ConfirmBoxUI','ChoicesUI','InputBoxUI','WaitBoxUI','BackgroundUI','IClientBaseExtended','ICoreStateExtended','material/types.ts:94','data-state/src/ins.ts:11','单例→目标映射汇总','处置'];const miss=need.filter(k=>t.indexOf(k)<0);if(miss.length>0){console.error('CLIENT-BASE/MAPPING MISSING :: '+miss.join(' | '));process.exit(1)}console.log('OK client-base + mapping coverage');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "CLIENT-BASE/MAPPING MISSING"(client-base 单例锚点、两个目标接口、先例、映射汇总或处置缺失)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md','utf8').replace(/\r\n/g,'\n');const rows=t.split('\n').filter(l=>/^\|\s*#04-15-A-\d+/.test(l));const bad=[];for(const r of rows){const ev=r.indexOf('[证据行]')>=0;const n=(r.match(/[A-Za-z0-9_.\/-]+\.(?:tsx?|md|json):\d+/g)||[]).length;if(!ev&&n<2)bad.push('ROW <2 anchors :: '+r.slice(0,70))}const f=t.split('\n').filter(l=>/^- \*\*#04-15-F-\d+/.test(l));if(rows.length<15)bad.push('TOO FEW A ROWS ('+rows.length+')');if(f.length<5)bad.push('F rows <5 ('+f.length+')');if(t.indexOf('目标(挂主类 + 目标接口,或 undetermined)')<0&&t.indexOf('目标(挂主类')<0)bad.push('MAPPING SUMMARY HEADER MISSING');if(bad.length>0){console.error('FINAL DISCIPLINE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK final: '+rows.length+' A rows, '+f.length+' F entries, summary present');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "FINAL DISCIPLINE FAILED" / "ROW <2 anchors" / "TOO FEW A ROWS" / "F rows" / "MAPPING SUMMARY HEADER MISSING"(A 事实行锚点不足 / A 行少于 15 / F 类 < 5 / 映射汇总表头缺失)</fails_when>
|
||||
<automated>node -e "const{execSync}=require('child_process');const fs=require('fs');const LEDGER='.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md';const FILE='packages-user/data-system/src/combat/context.ts';const WANT=' M '+FILE;const req=execSync('git status --porcelain -- .planning/REQUIREMENTS.md',{encoding:'utf8'}).replace(/[ \t\r\n]+$/,'');const o=execSync('git status --porcelain -- packages-user/client-modules packages-user/client-base packages-user/data-state packages-user/data-system packages packages/ src',{encoding:'utf8'}).replace(/\r/g,'');const lines=o.split('\n').map(l=>l.replace(/[ \t]+$/,'')).filter(l=>l.length>0);const t=fs.readFileSync(LEDGER,'utf8');const m=t.match(/<!-- baseline-hash-combat-context: ([0-9a-f]{40}) -->/);const bad=[];if(req.length>0)bad.push('REQUIREMENTS.md TOUCHED');if(!m)bad.push('BASELINE HASH MARKER MISSING');if(lines.indexOf(WANT)<0)bad.push('USER EDIT MISSING');const extra=lines.filter(l=>l!==WANT);if(extra.length>0)bad.push('PRODUCTION TREE TOUCHED :: '+extra.join(' ; '));if(m){const cur=execSync('git hash-object -- '+FILE,{encoding:'utf8'}).trim();if(cur!==m[1])bad.push('USER EDIT HASH DRIFT '+cur+' vs '+m[1]);}if(bad.length>0){console.error('SCOPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK scope: read-only, REQUIREMENTS untouched, user edit present + hash matches');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SCOPE GATE FAILED" / "REQUIREMENTS.md TOUCHED" / "BASELINE HASH MARKER MISSING" / "USER EDIT MISSING" / "PRODUCTION TREE TOUCHED" / "USER EDIT HASH DRIFT"(`REQUIREMENTS.md` 被改 / 用户并发 `combat/context.ts` 被回滚或再编辑 / 台账缺标记 / 生产文件被改 —— 违反 D-33 / D-56 精神 / 只读约束)</fails_when>
|
||||
<automated>node -e "const{execSync}=require('child_process');const g=execSync('git grep -n -E \"export const [A-Za-z0-9_]+ = new \" -- packages-user/client-modules packages-user/client-base',{encoding:'utf8'}).trim();const n=g.length?g.split(/\r?\n/).length:0;const fs=require('fs');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md','utf8');const a=(t.match(/^\|\s*#04-15-A-\d+/gm)||[]).length;if(a<n){console.error('A ROWS FEWER THAN GREP :: rows='+a+' grep='+n);process.exit(1)}console.log('OK reverse-check: A rows='+a+' >= grep new-exports='+n);"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "A ROWS FEWER THAN GREP"(台账 A 行数少于 `git grep -E "export const [A-Za-z0-9_]+ = new "` 的实测条数 —— 有单例遗漏)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const f='.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md';const s=fs.readFileSync(f,'utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;if(lf!==crlf){console.error('NON-CRLF '+f);process.exit(1)}console.log('OK CRLF ledger');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "NON-CRLF"(台账出现非 CRLF 换行)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `## A` 完整覆盖两个包的模块级实例化对象(client-modules 侧 + client-base 侧),并有 `git grep -E "export const [A-Za-z0-9_]+ = new "` 完整性反查 `[证据行]`,A-01.. 与 grep 结果对齐、无多登非 `new` 导出
|
||||
- `## E` 为每个单例给出挂主类 + 目标接口(`IClientBaseExtended` / `ICoreStateExtended`)与先例 `file:line` 证据,或显式 undetermined
|
||||
- `## F` ≥ 5 条,均为「现象 + 读不出原因 + 需用户裁决的点」,且**仅含单例条目**
|
||||
- `## 单例→目标映射汇总` 以固定列序表汇总全部已登记单例并写明两个包内单例总数;`## 处置` 明写只读完成 + 仅登记单例 + 第二步待规划 + REND 保持 Pending
|
||||
- 台账为 CRLF;生产源码零改动(除用户既有 `combat/context.ts` 外);`.planning/REQUIREMENTS.md` 零改动
|
||||
</acceptance_criteria>
|
||||
<done>client-base 铺开与 A / E / F 收口完成:两个包内全部单例(`export const x = new X()`)在案(带 `file:line`),目标接口映射与先例齐备,`git grep` 完整性反查通过(无遗漏、无多登),F 类仅含单例未确定项、映射汇总定稿;门禁(内容 / 锚点 / 范围 / 只读 / CRLF)全绿;台账可供用户审阅与第二步 04-16 消费</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4:收口 —— 只读基线一致 + 台账结构 / 锚点门禁 + 范围与用户改动门禁 + 既有产物保护 + ROADMAP 登记 + CRLF + 人工复核 + `04-15-SUMMARY.md` + 白名单提交</name>
|
||||
<precondition>Task 0 已闭环(用户已回复「可以执行」)且 Task 1 / 2 / 3 均已交付(台账骨架 + tracer 行 + client-modules 铺开 + client-base 铺开与映射汇总定稿)</precondition>
|
||||
<files>.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md, .planning/phases/04-render-adaptation/04-15-SUMMARY.md</files>
|
||||
<read_first>
|
||||
- .planning/phases/04-render-adaptation/04-10-SUMMARY.md 与 04-13-SUMMARY.md(只读增量的 SUMMARY 版式:只读基线 / 台账落点 / 门禁实测 / 人工复核结论 / 无运行时验证声明 / Known Stubs / Next Phase Readiness)
|
||||
- .planning/phases/04-render-adaptation/04-15-PLAN.md(本计划的 must_haves(truths / artifacts / key_links / assumptions / prohibitions)、台账结构表与逐条门禁)
|
||||
- .planning/ROADMAP.md(Phase 4 的 `**Plans:**` 行、`**Wave 13**` 条目——本步登记 `04-15` / `Wave 14` 的样式基准;本计划不在此文件内写其它内容)
|
||||
- .planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md(本计划的唯一交付物)
|
||||
- dev.md(「其他要求」的 CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **只读基线门禁**:执行 `git status --porcelain -- packages packages-user src`,与台账 `## 只读起始基线` 逐行比对,断言**逐条一致**(除用户既有 `packages-user/data-system/src/combat/context.ts` 外不新增、不消失、状态码不变);断言 `.planning/REQUIREMENTS.md` porcelain 为空;断言 `combat/context.ts` 的既有未提交改动**仍在**且 `git hash-object` 与台账标记一致(未被回滚 / 提交 / 再编辑)。
|
||||
|
||||
(2) **台账结构门禁**:断言标题 + 6 行元信息块 + `只读起始基线` / `背景` / `方法` / `枚举口径` / `## A 模块级实例化对象` / `## E 主类关系与目标挂载面映射` / `## F 未能从阅读确定(未猜测)` / `## 单例→目标映射汇总(供 04-16 消费)` / `## 处置` 全部在场;**断言无 `## B` / `## C` / `## D` 小节**;A / E 表为固定列序 `| ID | 符号 / 项目 | 定义 file:line | 类型 / 导出机制 | 全部消费者 file:line | 当前归属 / 挂载 | 与 ClientCore / client 的关系 | 目标挂载 / 接口(证据或 undetermined) | 置信 |`。
|
||||
|
||||
(3) **证据纪律门禁**:A 事实行每行 ≥2 个 `file:line` 锚点(带 `[证据行]` 标记的行豁免 ≥2 计数、但须含核对命令与原样结果);`[证据行]` 标记未出现在非证据行;F 类 ≥ 5 条;A 行总数 ≥ 15(规划日实测 30,执行日以当日 `git grep` 反查为准)。
|
||||
|
||||
(4) **内容门禁**:断言台账含关键锚点 —— `core.ts:6`、`render/renderer.ts:18,20,35,43`、`render/scene.ts:3`、`render/ui/controller.tsx:11,29`、`shared.ts:105`(`DEFAULT_FONT`)、`elements/cache.ts:280`、`ConfirmBoxUI`、`ChoicesUI`、`InputBoxUI`、`WaitBoxUI`、`BackgroundUI`、`IClientBaseExtended`(`client-base/src/types.ts:27`)、`ICoreStateExtended`(`data-state/src/types.ts:45`)、`material/types.ts:94,218,406,439`、`data-state/src/ins.ts:11`、`## 单例→目标映射汇总`;且 `## F` 明写「未猜测 / 只登记不判定」、`## 处置` 明写仅登记单例 + 第二步待规划 + REND 保持 Pending。
|
||||
|
||||
(5) **范围与用户改动门禁**:断言 `git status --porcelain -- packages-user/client-base/src/material packages packages/ src` 为空;本计划改动面之外零漂移(除 `.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md`、`04-15-SUMMARY.md`、`04-15-PLAN.md`、`.planning/ROADMAP.md` 外无新条目)。**并断言 `.planning/ROADMAP.md` 的 Phase 4 段含 `04-15` 与 `**Wave 14**`**——该登记由编排器在计划阶段完成(本计划写入不修改 ROADMAP),本收口确认其已就位,使广告出的 ROADMAP artifact 有可验证后置条件。
|
||||
|
||||
(6) **既有产物保护门禁**:断言 `.planning/phases/04-render-adaptation/` 下 `04-01`..`04-14` 的 `*-PLAN.md` 与 `*-SUMMARY.md` 的 porcelain 为空;断言目录内**不存在** `04-16+` 或任何新计划文件。
|
||||
|
||||
(7) **CRLF 收口**:确认台账 `04-RENDER-SINGLETON-AUDIT.md` 与 `04-15-SUMMARY.md` 为 CRLF(计划文件 `04-15-PLAN.md` 亦为 CRLF,与既有 `*-PLAN.md` 一致,不属本门禁范围)。
|
||||
|
||||
(8) **人工代码复核(不可自动化,如实记录结论)**:逐条打开当日源码确认 —— 台账每一条 A 事实行锚点指向真实存在的 `export const x = new X()` 及其消费者;E 类目标接口证据的 `file:line` 指向真实声明(`IClientBaseExtended` / `ICoreStateExtended` / `material/types.ts` 先例);`git grep` 完整性反查(`export const ... = new `)与台账 A 行集合一致、无遗漏、无多登非 `new` 导出;F 类**无一条**越过「只登记不判定」的边界,且**无一条**涉及非单例(`window` / `core` / `Mota.require` / 静态注册表 / 顶层副作用)。发现偏差即回到相应任务修正。把复核结论(通过 / 偏差及修正)写入 SUMMARY。
|
||||
|
||||
(9) **写 `04-15-SUMMARY.md`**:按 04-10 / 04-13 的只读增量版式写 —— frontmatter(`phase` / `plan: 15` / `tags` / `requires` / `provides` / `affects` / `actuals` / `key-files`(created / modified / deleted)/ `key-decisions` / `requirements-completed: []` / `duration` / `completed` / `status`)+ 只读起始基线(Task 1 (0a) 的原文 + `git hash-object` 标记)+ Accomplishments(A / E / F 与映射汇总的落点表 + 两个包内单例总数)+ Task Commits + 门禁实测结果 + 人工复核结论 + Decisions Made + Deviations(如有)+ Known Stubs(目标接口 undetermined 项、外部包单例归属、重复 renderer 实例、`GameUI` 实例是否纳入、无消费者的 `statusBar` UI 等)+ Next Phase Readiness(第二步 04-16 待用户审阅后另行规划)。**如实声明本步为只读收集、仅登记单例、无运行时验证(D-68 的延续),`REND-01` / `REND-02` 保持 Pending。**
|
||||
|
||||
(10) **白名单提交(单次)**:核对 `git status`,**只**暂存本计划的 `files_modified`(`04-RENDER-SINGLETON-AUDIT.md`)与 `04-15-SUMMARY.md`(ROADMAP 的登记若在计划阶段已完成则不在本任务重复提交;若本任务需补登 ROADMAP,则一并只暂存 `.planning/ROADMAP.md`)——绝不暂存 `packages-user/data-system/src/combat/context.ts` 或任何其它文件;以 `docs(04-15): 渲染端去单例化只读清点(第一步)` 为提交信息提交。**不得**借提交顺带格式化 / 重排无关文件;**不得** amend 任何既有提交。
|
||||
|
||||
(11) **不写其它 `.planning/` 文件**:本任务除 `04-15-SUMMARY.md` 外**不得**写 `.planning/STATE.md` / `WINDOWS.md` / `REQUIREMENTS.md`;ROADMAP 的 `04-15` / `Wave 14` 登记已在计划阶段完成(本任务不重复改 ROADMAP)。
|
||||
</action>
|
||||
<reversibility rating="reversible">本任务是收口(门禁 + 文档 + 一次白名单提交),唯一新增生产面为零;提交内容全部是只读台账与 SUMMARY,可用 git revert 整体回退。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md','utf8');const need=['只读起始基线','背景','方法','枚举口径','A 模块级实例化对象','E 主类关系与目标挂载面映射','F 未能从阅读确定','单例→目标映射汇总','处置','#04-15-A-01','core.ts:6','render/renderer.ts:18','render/scene.ts:3','render/ui/controller.tsx:11','shared.ts:105','elements/cache.ts:280','ConfirmBoxUI','ChoicesUI','InputBoxUI','WaitBoxUI','BackgroundUI','IClientBaseExtended','ICoreStateExtended','material/types.ts:94','data-state/src/ins.ts:11'];const miss=need.filter(k=>t.indexOf(k)<0);if(miss.length>0){console.error('LEDGER CONTENT MISSING :: '+miss.join(' | '));process.exit(1)}const forbid=['## B 可变模块级状态','## C 全局访问器','## D 顶层副作用'];const bad=forbid.filter(k=>t.indexOf(k)>=0);if(bad.length>0){console.error('OUT-OF-SCOPE SECTION PRESENT :: '+bad.join(','));process.exit(1)}console.log('OK ledger structure + all singleton anchors');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "LEDGER CONTENT MISSING" / "OUT-OF-SCOPE SECTION PRESENT"(台账结构 / 单例锚点 / 目标接口 / 先例 / 映射汇总关键内容缺失,或误含 B/C/D 小节)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md','utf8').replace(/\r\n/g,'\n');const rows=t.split('\n').filter(l=>/^\|\s*#04-15-A-\d+/.test(l));const bad=[];for(const r of rows){const ev=r.indexOf('[证据行]')>=0;const n=(r.match(/[A-Za-z0-9_.\/-]+\.(?:tsx?|md|json):\d+/g)||[]).length;if(!ev&&n<2)bad.push('ROW <2 anchors :: '+r.slice(0,70))}const f=t.split('\n').filter(l=>/^- \*\*#04-15-F-\d+/.test(l));if(rows.length<15)bad.push('TOO FEW A ROWS ('+rows.length+')');if(f.length<5)bad.push('F rows <5 ('+f.length+')');if(bad.length>0){console.error('EVIDENCE DISCIPLINE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK final discipline: '+rows.length+' A rows, '+f.length+' F entries');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "EVIDENCE DISCIPLINE FAILED" / "ROW <2 anchors" / "TOO FEW A ROWS" / "F rows"(A 事实行锚点不足 / A 行少于 15 / F 类 < 5)</fails_when>
|
||||
<automated>node -e "const{execSync}=require('child_process');const fs=require('fs');const LEDGER='.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md';const FILE='packages-user/data-system/src/combat/context.ts';const WANT=' M '+FILE;const bad=[];const mat=execSync('git status --porcelain -- packages-user/client-base/src/material',{encoding:'utf8'}).replace(/[ \t\r\n]+$/,'');if(mat.length>0)bad.push('MATERIAL TOUCHED :: '+mat);const pkg=execSync('git status --porcelain -- packages packages/ src',{encoding:'utf8'}).replace(/[ \t\r\n]+$/,'');if(pkg.length>0)bad.push('FORBIDDEN PATH TOUCHED :: '+pkg);const req=execSync('git status --porcelain -- .planning/REQUIREMENTS.md',{encoding:'utf8'}).replace(/[ \t\r\n]+$/,'');if(req.length>0)bad.push('REQUIREMENTS.md TOUCHED');const o=execSync('git status --porcelain -- packages-user',{encoding:'utf8'}).replace(/\r/g,'');const lines=o.split('\n').map(l=>l.replace(/[ \t]+$/,'')).filter(l=>l.length>0);const t=fs.readFileSync(LEDGER,'utf8');const m=t.match(/<!-- baseline-hash-combat-context: ([0-9a-f]{40}) -->/);if(!m)bad.push('BASELINE HASH MARKER MISSING');if(lines.indexOf(WANT)<0)bad.push('USER EDIT MISSING');const extra=lines.filter(l=>l!==WANT);if(extra.length>0)bad.push('UNEXPECTED USER TREE :: '+extra.join(' ; '));if(m){const cur=execSync('git hash-object -- '+FILE,{encoding:'utf8'}).trim();if(cur!==m[1])bad.push('USER EDIT HASH DRIFT '+cur+' vs '+m[1]);}if(bad.length>0){console.error('SCOPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK scope: material/packages/src clean; REQUIREMENTS untouched; user edit present + hash matches');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SCOPE GATE FAILED" / "MATERIAL TOUCHED" / "FORBIDDEN PATH TOUCHED" / "REQUIREMENTS.md TOUCHED" / "BASELINE HASH MARKER MISSING" / "USER EDIT MISSING" / "UNEXPECTED USER TREE" / "USER EDIT HASH DRIFT"(`material/` / `packages/` / `src/` 被改、`REQUIREMENTS.md` 被改、用户 `combat/context.ts` 被回滚或再编辑、台账缺标记、或用户文件之外出现 `packages-user` 改动 —— D-42 / D-48 / D-33 / D-56 精神边界被破)</fails_when>
|
||||
<automated>node -e "const{execSync}=require('child_process');const o=execSync('git status --porcelain -- .planning/phases/04-render-adaptation',{encoding:'utf8'}).replace(/\s+$/,'');const rows=o.length?o.split(/\r?\n/):[];const existing=rows.filter(r=>/04-(0[1-9]|1[0-4])-(PLAN|SUMMARY)\.md$/.test(r));const newPlan=rows.filter(r=>/04-(1[6-9]|[2-9][0-9])-/.test(r));const bad=[];if(existing.length>0)bad.push('EXISTING ARTIFACT DIRTY :: '+existing.join(' ; '));if(newPlan.length>0)bad.push('NEW PLAN FILE :: '+newPlan.join(' ; '));if(bad.length>0){console.error('ARTIFACT PROTECTION FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK existing 04-01..04-14 artifacts untouched; no 04-16+ plan');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "ARTIFACT PROTECTION FAILED" / "EXISTING ARTIFACT DIRTY" / "NEW PLAN FILE"(改写 / 覆盖了既有 `04-01`..`04-14` 产物,或产出了 `04-16+` 计划文件)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('.planning/ROADMAP.md','utf8');const bad=[];if(t.indexOf('04-15')<0)bad.push('ROADMAP 04-15 MISSING');if(t.indexOf('Wave 14')<0)bad.push('ROADMAP Wave 14 MISSING');if(bad.length>0){console.error('ROADMAP REGISTRATION FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK ROADMAP has 04-15 / Wave 14 registration');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "ROADMAP REGISTRATION FAILED" / "ROADMAP 04-15 MISSING" / "ROADMAP Wave 14 MISSING"(编排器尚未在 `.planning/ROADMAP.md` 的 Phase 4 段登记 `04-15` / `Wave 14` —— 广告出的 ROADMAP artifact 缺可验证后置条件)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const files=['.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md','.planning/phases/04-render-adaptation/04-15-SUMMARY.md'];const bad=[];for(const f of files){if(!fs.existsSync(f)){bad.push('MISSING '+f);continue}const s=fs.readFileSync(f,'utf8');const lf=(s.match(/\n/g)||[]).length;const crlf=(s.match(/\r\n/g)||[]).length;if(lf!==crlf)bad.push('NON-CRLF '+f)}if(bad.length>0){console.error('CLOSURE FILE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK CRLF ledger + SUMMARY present');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "CLOSURE FILE GATE FAILED" / "MISSING" / "NON-CRLF"(台账或 SUMMARY 缺失 / 非 CRLF)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const f='.planning/phases/04-render-adaptation/04-15-SUMMARY.md';if(!fs.existsSync(f)){console.error('SUMMARY MISSING');process.exit(1)}const t=fs.readFileSync(f,'utf8');const need=['04-15','去单例化','只读','Pending','04-16'];const miss=need.filter(k=>t.indexOf(k)<0);if(miss.length>0){console.error('SUMMARY INCOMPLETE :: '+miss.join(','));process.exit(1)}console.log('OK 04-15-SUMMARY.md present and keyed');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SUMMARY MISSING" / "SUMMARY INCOMPLETE"(`04-15-SUMMARY.md` 未写或缺少关键章节 / 标识)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- **只读基线门禁**:当日起始 porcelain 与台账 `## 只读起始基线` 逐条一致(除用户既有 `combat/context.ts` 外);`.planning/REQUIREMENTS.md` 零改动;用户 `combat/context.ts` 未提交改动仍在且哈希一致
|
||||
- **台账结构 / 内容门禁**:9 小节 + 固定列序表 + 单例 / 目标接口 / 先例 / 映射汇总关键锚点全部在场;**无 B / C / D 小节**
|
||||
- **证据纪律门禁**:A 事实行每行 ≥2 个 `file:line` 锚点(证据行豁免);F 类 ≥ 5 条;A 行 ≥ 15
|
||||
- **范围门禁**:`material/` / `packages/` / `src/` porcelain 为空
|
||||
- **既有产物保护**:`04-01`..`04-14` 的 `*-PLAN.md` / `*-SUMMARY.md` porcelain 为空;无 `04-16+`
|
||||
- **ROADMAP 登记**:`.planning/ROADMAP.md` 的 Phase 4 段含 `04-15` / `Wave 14`
|
||||
- **CRLF**:台账 / SUMMARY 均为 CRLF
|
||||
- `04-15-SUMMARY.md` 已写(含只读起始基线 / A / E / F 落点 / 门禁实测 / 人工复核结论 / 无运行时验证声明 / 待规划第二步)
|
||||
- 已按白名单完成单次提交(只含 `files_modified` 条目 + `04-15-SUMMARY.md`,ROADMAP 如需补登则一并);未写其它 `.planning/` 文件;未派发会写文件的子代理
|
||||
</acceptance_criteria>
|
||||
<done>收口完成:只读基线一致、台账结构 / 内容 / 锚点门禁全绿、范围与用户改动门禁通过、既有 `04-01`..`04-14` 产物零触碰、ROADMAP `04-15` / `Wave 14` 登记就位、台账与 SUMMARY 均 CRLF;`04-15-SUMMARY.md` 落盘;白名单单次提交完成;`REND-01` / `REND-02` 保持 Pending;第二步(04-16:挂载 + 接口继承重构)待用户审阅后另行规划</done>
|
||||
</task>
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| (无新增外部边界) | 本计划是纯只读清点(读源码 + 写一个 `.planning/` 文档),不引入外部输入 / 网络 / 进程边界;不新增依赖、不安装包、不执行 `pnpm i` |
|
||||
| 仓库工作树 → git 提交 | 用户的并发未提交改动(`packages-user/data-system/src/combat/context.ts` 与 `.planning/phases/04-render-adaptation/04-CONTEXT.md`)与本次改动共处同一工作树;若暂存面把控不当,会把用户的改动混入本计划提交 |
|
||||
| 台账内容 → 后续执行计划(04-16) | 台账若把「读不出来」写成「错配 / 缺失 / 残留 / 待办」,或把非单例(模块级可变状态 / 全局访问器 / 顶层副作用)混入,会误导第二步执行者扩大范围或臆造挂载方案 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-04-15-01 | Tampering | 只读硬约束(生产代码零改动) | high | mitigate | Task 1 / 2 / 3 / 4 的 `<verify>` 均含**正向基线**断言:(a) ` M packages-user/data-system/src/combat/context.ts` 行在场、(b) 再无其它生产条目、(c) 当前 `git hash-object -- <该文件>` 与台账 `<!-- baseline-hash-combat-context: <40hex> -->` 标记一致(用户改动被回滚或被再编辑同样失败);prohibitions 明列不得改任何生产文件 |
|
||||
| T-04-15-02 | Tampering | 提交暂存面(`git add`) | medium | mitigate | Task 4 步骤 (10) 明示只暂存 `files_modified` 与 `04-15-SUMMARY.md`(及必要的 ROADMAP 登记);Task 4 的 `<verify>` 断言用户 `combat/context.ts` 未提交改动仍在(未被提交 / 回滚);prohibitions 明列 |
|
||||
| T-04-15-03 | Tampering | 既有产物(`04-01`..`04-14`)与 `04-CONTEXT.md` | medium | mitigate | Task 4 的「既有产物保护门禁」断言既有 `*-PLAN.md` / `*-SUMMARY.md` porcelain 为空、无 `04-16+`;prohibitions 明禁改写 / 覆盖 / 改号 |
|
||||
| T-04-15-04 | Spoofing | 台账证据可信度(臆测替代阅读;漏项冒充完整;非单例混入) | medium | mitigate | 证据纪律门禁要求 A 事实行 ≥2 个真实 `file:line` 锚点;Task 3 以 `git grep -n -E "export const [A-Za-z0-9_]+ = new "` 做完整性反查(A 行 ≥ grep 条数)并留 `[证据行]`;Task 1 / Task 4 断言无 B/C/D 小节;F 类「未猜测且仅单例」边界由人工复核核验;Task 4 步骤 (8) 逐条复核并记录结论 |
|
||||
| T-04-15-05 | Repudiation | 「无运行时」被用来弱化验证 | low | mitigate | 每条 `<automated>` 必带 `<fails_when>`;D-68 明示禁止 `check:type` / `build` / TS 诊断数类门禁;prohibitions 明列不得伪造绿 |
|
||||
| T-04-15-SC | Tampering | npm/pip/cargo 安装 | low | accept | 本计划无任何依赖新增 / 变更、不执行 `pnpm i`、不触及任何 `package.json`,故无供应链安装面;`T-04-15-SC` 保留为「本 run 不适用」的显式登记 |
|
||||
</threat_model>
|
||||
|
||||
## Verification Runnability(PowerShell 5.1 可运行性)
|
||||
|
||||
**执行者必读。** 仓库开发环境为 Windows / PowerShell 5.1。把本计划 `<automated>` 里的内联 `node -e "..."` 命令原样粘贴到 PowerShell 5.1 时,其中的内嵌引号、`$`(如正则尾锚 `$`)、CJK 字符与 `→` 可能被 shell 转义 / 编码破坏,使命令被 mangle 而非按其语义失败。**执行者必须把每条门禁的原样语义写入仓库外的临时 `.cjs` 文件(例如 `%TEMP%\opencode\04-15-<gate>.cjs`)并运行 `node <该文件>`**,从而绕开 PowerShell 的引号 / `$` / 编码处理;**判定语义与对应的 `<fails_when>` 一字不变**——不得为 shell 兼容而删减断言、放宽条件或改变失败判据。该做法与 04-12 / 04-14 SUMMARY 记录的先例一致。
|
||||
|
||||
<verification>
|
||||
**Static-only(D-68 的延续:不设 `check:type` / `build` / TS 诊断数类门禁)**
|
||||
|
||||
- [ ] Task 0:用户明确回复「可以执行」四个字(`blocking-human` 关卡)
|
||||
- [ ] Task 1 门禁:台账骨架 + 9 小节(A/E/F)+ tracer 行 `#04-15-A-01` 落盘;无 B/C/D 小节;A 事实行 ≥2 锚点;生产树正向基线确认(` M` 行在场 + 无其它条目 + `git hash-object` 与台账标记一致);CRLF
|
||||
- [ ] Task 2 门禁:client-modules 侧单例 A 行 + E 目标映射 + F-0N 落盘;锚点纪律(A 行 ≥10、F ≥3);生产树正向基线确认;CRLF
|
||||
- [ ] Task 3 门禁:client-base 侧单例锚点 + 两个目标接口 + 先例 + `git grep` 完整性反查 + `## 单例→目标映射汇总` 表头与内容在场;A 行 ≥15、F 类 ≥ 5;范围门禁(`REQUIREMENTS.md` 零改动 + 用户 `combat/context.ts` 正向基线确认);CRLF
|
||||
- [ ] Task 4 门禁:只读基线一致 + 台账结构 / 内容 / 锚点(无 B/C/D)+ 范围(含用户改动正向基线)+ 既有产物保护 + ROADMAP `04-15` / `Wave 14` 登记 + CRLF + `04-15-SUMMARY.md` 落盘
|
||||
- [ ] 人工代码复核结论写入 SUMMARY(含「A 行与 `git grep` 对齐、无多登非 `new` 导出、F 类无非单例条目」)
|
||||
- [ ] 单次白名单提交只含本计划条目;`REND-01` / `REND-02` 保持 Pending
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- **只读收集完成**:唯一交付物 `04-RENDER-SINGLETON-AUDIT.md` 落盘,A / E / F 三类 + `单例→目标映射汇总` 齐备(**无 B / C / D 小节**),A 事实行每行 ≥2 个 `file:line` 锚点;生产代码改动为 **0**
|
||||
- **单例枚举完整(仅单例)**:`packages-user/client-modules/src/**` 与 `packages-user/client-base/src/**` 内全部 `export const x = new X()` 单例均逐条在案(定义 / 符号 / 导出机制 / 构造类 / 全部消费者 / 当前挂载 / 与 `ClientCore` 的关系),并由 `git grep -E "export const [A-Za-z0-9_]+ = new "` 完整性反查确认;**无非单例条目**(不含可变模块级状态 / 全局注册表 / 全局访问器 / 顶层副作用 / 非 `new` 导出)
|
||||
- **目标模式映射齐备**:每个单例给出挂主类 + 目标接口(`IClientBaseExtended` 或 `ICoreStateExtended`)+ 先例 `file:line`(渲染 / 系统层先例 `material/types.ts` 与数据端先例 `ins.ts:11`),或显式 undetermined(进 F 类,仅单例相关)
|
||||
- **只读基线正向确认**:每个生产树门禁断言用户 `combat/context.ts` 的 ` M` 行在场、无其它生产条目、且 `git hash-object` 与台账标记一致(回滚 / 再编辑均失败)
|
||||
- **ROADMAP 登记就位**:Task 4 断言 `.planning/ROADMAP.md` 的 Phase 4 段含 `04-15` / `Wave 14`(登记由编排器在计划阶段完成)
|
||||
- **范围零外溢**:数据端(含用户未提交 `combat/context.ts`)/ `packages/` / `src/` / 其它 `package.json` / 测试文件全部零触碰;用户改动被保留且未被提交
|
||||
- **命名零变更**:不引入任何新公共 / 受保护 / 私有命名;台账只登记,不为任何挂载字段 / 接口拍板
|
||||
- **无运行时要求**:全部证明为静态门禁 + 人工复核;`REND-01` / `REND-02` 保持 Pending,`.planning/REQUIREMENTS.md` 零改动
|
||||
- **第二步不被规划**:不产出 `04-16+` 或任何新 `*-PLAN.md`;`## 处置` 明写第二步待用户审阅后另行规划
|
||||
</success_criteria>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
| Artifact | Kind | Purpose |
|
||||
|---|---|---|
|
||||
| `.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md` | 新增(只读台账) | 渲染端去单例化第一步交付物(仅单例):元信息块 + 9 小节 + A / E / F 三类 + 映射汇总,每个单例带 `file:line` 且有目标接口证据或 undetermined |
|
||||
| `.planning/phases/04-render-adaptation/04-15-SUMMARY.md` | 新增(文档) | 只读收集记录 + 门禁实测 + 人工复核 + 无运行时验证声明 + 第二步待规划 |
|
||||
| `.planning/ROADMAP.md`(Wave 14 + register note) | 修改(计划阶段完成,编排器所有) | `04-15` 的增量登记(本计划写入不修改该文件;登记由编排器在计划阶段完成,Task 4 以可验证后置条件断言其已就位) |
|
||||
| 未改动(显式保留) | — | `packages-user/client-modules/src/**`、`packages-user/client-base/src/**`、`packages-user/data-*/src/**`(含用户未提交 `data-system/src/combat/context.ts`)、`packages/**`、`src/**`、`.planning/REQUIREMENTS.md` |
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/04-render-adaptation/04-15-SUMMARY.md` when done
|
||||
</output>
|
||||
597
.planning/phases/04-render-adaptation/04-16-PLAN.md
Normal file
597
.planning/phases/04-render-adaptation/04-16-PLAN.md
Normal file
@ -0,0 +1,597 @@
|
||||
---
|
||||
phase: 04-render-adaptation
|
||||
plan: 16
|
||||
type: execute
|
||||
wave: 15
|
||||
depends_on:
|
||||
- 04-01
|
||||
- 04-02
|
||||
- 04-03
|
||||
- 04-04
|
||||
- 04-05
|
||||
- 04-06
|
||||
- 04-07
|
||||
- 04-08
|
||||
- 04-09
|
||||
- 04-10
|
||||
- 04-11
|
||||
- 04-12
|
||||
- 04-13
|
||||
- 04-14
|
||||
- 04-15
|
||||
files_modified:
|
||||
- packages-user/client-modules/src/types.ts
|
||||
- packages-user/client-modules/src/client.ts
|
||||
- .planning/phases/04-render-adaptation/04-16-SUMMARY.md
|
||||
files_deleted: []
|
||||
autonomous: false
|
||||
requirements:
|
||||
- REND-01
|
||||
- REND-02
|
||||
estimate:
|
||||
tokens: 40000
|
||||
raw_tokens: 40000
|
||||
tasks: 5
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求)**:Task 0 必须先向用户汇报「要解决的问题(渲染端 `packages-user/client-modules` 与 `packages-user/client-base` 内仍有单例——模块顶层实例化对象 `export const x = new X()`;04-15 只读台账清点 30 条)」「**本会话固定的设计(option B:镜像数据端——单一具体类实现整条接口链;`IClientBase` 不加字段;忽略 `client-system`;不给 `ClientCore` 加 `state` 字段)**」「本次会话锁定裁定 F-01..F-07 + `DEFAULT_FONT`」「**全部将新增的公共字段命名(`using` / `sceneController` / `mainUIController`,均加在 `IClientCore` 与 `ClientCore` 上)**」「源→目标映射(含 staged 保留项)」「消费者改写规则(仅 `this.state` 可转换者改写;其余不动;禁 alias、禁新 `import { client }`)」「范围与硬边界」「验证方式」,然后必须**停下等待**用户明确回复「可以执行」四个字;收到前不得执行任何任务、不得派发执行子代理。无论触发方式(`/gsd-execute-phase`、`/gsd-manager`、`/gsd-autonomous`、子代理/执行器派发)或配置模式(`.planning/config.json` 的 `mode: yolo`、`workflow.auto_advance`)如何,均不得自动放行。"
|
||||
- "**设计 = option B(镜像数据端)**:数据端为「单一具体类 `CoreState implements ICoreState` 实现整条链 `IDataCommon → IStateBase → IStateSystem → ICoreState`,每层接口声明该层 readonly 字段,无 `DataCommon` / `StateBase` / `StateSystem` 类」。渲染端镜像之——**单一具体类 `ClientCore` 实现整条链**(`IClientCore extends IClientSystem extends IClientBase`);**client-modules 层的字段加在 `IClientCore`(`packages-user/client-modules/src/types.ts:13`)并在 `ClientCore` 构造器(`packages-user/client-modules/src/client.ts:41,58`)构造 / 挂载**;**client-base 层当前无在步单例可挂(`texture` 按 F-01 保留、5 个 `GameUI` 按 F-04 / F-07 保留),故 `IClientBase`(`packages-user/client-base/src/types.ts:8`)不加任何字段——显式声明为零改动**;**`IClientSystem` / `client-system` 包一律忽略、不修改**;**不给 `ClientCore` 加 `state` 字段**(`IClientBaseExtended` / `ICoreStateExtended` 是给需要回指的子系统实现的,不是顶层类)。"
|
||||
- "**挂载三个外部包子系统到主类(F-02 / F-04)**:`using`(类型 `IRendererUsing`,来源 `@motajs/render-vue`;旧符号 `render/renderer.ts:43`)、`sceneController`(类型 `UIController`,`@motajs/system`;旧符号 `render/scene.ts:3`)、`mainUIController`(类型 `UIController`,`@motajs/system`;旧符号 `render/ui/controller.tsx:11`)三个字段加在 `IClientCore` 与 `ClientCore` 上,并在 `ClientCore` 构造器内 `new`(F-02:constructed in the `ClientCore` constructor)。`UIController` 实例**必须挂载**(F-04);全部 `GameUI` 实例(各 `*UI`、`MainBackgroundUI`)**不得挂载、保持原样**(F-04 / F-07)。"
|
||||
- "**Staged 迁移 + 消费者改写规则(用户原话:能直接通过 `this.state` 调用的直接改了,不能通过这个办法调用的先不管)**:模块级 `export const x = new X()` 站点与其导出**在非 `this.state` 消费者仍需时予以保留(staged)**——本轮**不删除** `render/renderer.ts` / `render/scene.ts` / `render/ui/controller.tsx` 的模块级站点;**不创建任何转发别名**(禁 `export const x = client.x`,dev.md 不转发导出);**不新增任何 `import { client }` 全局使用**(D-23)。仅当某消费者本身是暴露 `this.state` 且其类型链声明了被挂字段的类时,才把该站点改写为 `this.state.<field>`;否则**保持原样**。规划日实测:`packages-user/client-modules/src` 与 `packages-user/client-base/src` 内消费 `using` / `sceneController` / `mainUIController` 的站点**均非**暴露 `this.state` 的类(唯一带 `state` 的类为 `client-base/src/material/{autotile,builder,manager}.ts`,其 `state: ICoreState` 不含被挂字段)→ 预期本轮 **0 处** `this.state` 改写;执行日必须实测复核。"
|
||||
- "**F-03(重复实例,staged)**:`render/renderer.ts:18,20,35` 的模块级 `rafExcitation` / `excitationDivider` / `mainRenderer` 与 `ClientCore` 构造器内自建的 `rafExcitation` / `excitationDivider` / `renderer`(`client.ts:90,91,108`)重复。裁定:**`ClientCore` 的实例为权威**(New / `this.state` 访问一律走主类字段);模块级重复按消费者规则处置——其消费者(`render/index.tsx` 的 `mainRenderer`、`renderer.ts:46` 的 `createRendererFor(mainRenderer)`)**均非 `this.state` 可转换站点**,且移除 `mainRenderer` 会连带删掉 `createApp` 的来源(又不得用 alias / `import { client }`),故**本轮保留为 staged**,删除推迟到消费者迁移步骤。本轮**不修改** `render/renderer.ts`。"
|
||||
- "**保持不动(F-01 / F-04 / F-07 / `DEFAULT_FONT`)**:`texture`(`client-base/src/elements/cache.ts:280`)、`DEFAULT_FONT`(`client-modules/src/shared.ts:105`)、全部 `GameUI` 实例(各 `*UI` 与 `MainBackgroundUI`)、`client` 本身、以及全部现有消费者一律不触碰;`MainBackgroundUI` 与 `createMainController()` 保持原样。"
|
||||
- "**范围与边界零外溢(D-33 / D-42 / D-48 / D-49 / D-55 / D-65 / D-69)**:改动面仅限 `packages-user/client-modules/src/types.ts`、`packages-user/client-modules/src/client.ts`(若执行日发现可转换的 `this.state` 消费者则须先停关卡重报,见 Task 3)与 `04-16-SUMMARY.md`;`packages-user/client-base` 全包(含 `IClientBase`、`material/**`)、`packages-user/client-system`、数据端(含用户未提交 `packages-user/data-system/src/combat/context.ts`)、其它 `packages-user/*`、`packages/**`、`src/**`、任何测试 / legacy 面、任何 `package.json` / lockfile、任何 barrel(D-69)**零触碰**;不新增依赖、不执行 `pnpm i`;除获批的三个新增字段外不得新增 / 改名 / 删除公共成员。"
|
||||
- "**无运行时要求 + REND 保持 Pending(D-68 延续)**:验证一律为**静态**(文件内容断言 + 模块级 staged 站点保留断言 + 唯一 / 逐符号站点反查 + `git status --porcelain` 范围断言 + 用户改动 / 哈希基线 + CRLF + 既有产物保护 + ROADMAP 登记 + 人工复核),**不得**出现任何以「`pnpm check:type` 通过」「`pnpm build` 成功」「TS 诊断数归零」为条件的门禁或验收项;`REND-01` / `REND-02` 在本 run 后仍为 **Pending**,`.planning/REQUIREMENTS.md` 零改动。静态不可证之处(类型可满足性、运行时装配、staged 并存实例的最终收敛)如实声明为不可静态证明。"
|
||||
artifacts:
|
||||
- path: packages-user/client-modules/src/types.ts
|
||||
provides: "在 `IClientCore`(`extends IClientSystem`)上新增 `readonly using: IRendererUsing` / `readonly sceneController: UIController` / `readonly mainUIController: UIController` 三个只读字段;**不改** `extends`、**不加** `state`、**不改** `IClientSystem` / `client-system`"
|
||||
- path: packages-user/client-modules/src/client.ts
|
||||
provides: "`ClientCore` 新增三个只读字段并在构造器内 `new`(`using` 在 `this.renderer` 之后;`sceneController` / `mainUIController` 以既有字符串 id `'main-scene'` / `'main-ui'`);既有 `rafExcitation` / `excitationDivider` / `renderer` 保持权威(F-03)"
|
||||
- path: .planning/phases/04-render-adaptation/04-16-SUMMARY.md
|
||||
provides: "本增量执行记录:只读 / 用户改动基线(含 `combat/context.ts` 哈希标记)+ 新增命名 + 源→目标映射(含 staged 保留项)+ `this.state` 扫描结论 + 门禁实测 + 人工复核 + 无运行时验证声明 + 静态不可证项 + staged 并存后果"
|
||||
- path: .planning/ROADMAP.md
|
||||
provides: "Phase 4 的 `Wave 15` + `04-16-PLAN.md` register note(计划阶段登记;本写入不修改该文件,登记由编排器完成)"
|
||||
key_links:
|
||||
- "`packages-user/data-state/src/core.ts:77`(单一具体类 `CoreState implements ICoreState`)↔ 接口链 `packages-user/data-common/src/types.ts:50` `IDataCommon` → `packages-user/data-base/src/types.ts:17` `IStateBase extends IDataCommon` → `packages-user/data-system/src/types.ts:7` `IStateSystem extends IStateBase` → `packages-user/data-state/src/types.ts:26` `ICoreState extends IStateSystem,...` —— **option B 的镜像依据**(每层只声明字段,单一类实现整链)"
|
||||
- "渲染端链:`packages-user/client-base/src/types.ts:8`(`IClientBase extends ICoreState`)→ `packages-user/client-system/src/types.ts:3`(`IClientSystem extends IClientBase`)→ `packages-user/client-modules/src/types.ts:13`(`IClientCore extends IClientSystem`);单一具体类 `packages-user/client-modules/src/client.ts:41`(`class ClientCore extends CoreState implements IClientCore`)—— 新增字段落在最上层 `IClientCore` / `ClientCore`;下层不加字段"
|
||||
- "`packages-user/client-modules/src/client.ts:90,91,108`(构造器权威 `rafExcitation` / `excitationDivider` / `renderer`)↔ `packages-user/client-modules/src/render/renderer.ts:18,20,35,43`(模块级 staged 重复)—— F-03:权威在 `ClientCore`,模块级保留为 staged"
|
||||
- "`@motajs/render-vue` `IRendererUsing`(`packages/render-vue/src/types.ts:67`;`RendererUsing implements IRendererUsing` `packages/render-vue/src/use.ts:15`)↔ `@motajs/system` `UIController`(`packages/system/src/ui/controller.ts:82`)—— 新增字段的外部类型来源(`packages/`,D-33 只读)"
|
||||
- "staged 消费者(本轮**不改**):`packages-user/client-modules/src/render/index.tsx:5,8,10,22,23,26`(`mainRenderer` / `sceneController` / `createApp`)、`render/ui/main.tsx:29,31,155,281,284`(`mainUIController` / `using`)、`render/action.ts:5,17-36`、`render/ui/statusBar.tsx:7,380`、`render/ui/toolbar.tsx:23,94-195`(`mainUIController`);`packages-user/client-base/src/components/misc.tsx:15-16` 与 `elements/index.ts:3-4`(对 `'../renderer'` 的 `@ts-expect-error` 反向耦合,D-59 / D-67 保留)—— 均为非 `this.state` 站点,按规则保持原样"
|
||||
- "`git status --porcelain -- packages packages-user src` 与 `git hash-object -- packages-user/data-system/src/combat/context.ts`(Task 1 (0a) 记录;规划日实测 ` M` + `b4c4bcd698fab7d30d602b7cf6e84eb211740ad2`,执行日以当日为准)↔ Task 1 / 2 / 3 / 4 的 D-56 用户改动基线门禁 —— 用户并发改动零触碰证明"
|
||||
assumptions:
|
||||
- "**FLAGGED ASSUMPTION(specless fallback)— REND-01 edge unresolved:** 未提供 REND-01(渲染端适配新数据层接口)的验收边界;本计划**不发明**阈值,只交付渲染端去单例化**第二步(挂载到主类)**的代码改动。用户须在推进 REND-01 余下部分前补齐边界判据。"
|
||||
- "**FLAGGED ASSUMPTION(specless fallback)— REND-02 edge unresolved:** 未提供 REND-02(移动端与桌面端双布局)的验收边界;本计划不涉及双布局,也不发明其验收阈值。"
|
||||
- "**本轮为 staged:模块级站点保留 → 并存实例(须用户知晓)**:因非 `this.state` 消费者仍需模块级导出,本轮**不删除** `render/renderer.ts:43` 的 `using`、`render/scene.ts:3` 的 `sceneController`、`render/ui/controller.tsx:11` 的 `mainUIController`。因此 `ClientCore` 权威实例与模块级 staged 实例会**并存**(`UIController` 的静态注册表以 id 去重:`new UIController('main-scene')` / `('main-ui')` 二次构造会触发 `logger.warn(57, id)` 且不覆盖注册项)。这是用户明确选择的 staged 迁移态,登记为 Known Stub,待消费者迁移后由后续步骤收敛。若用户不希望并存,请在 Task 0 选 `move-module-sites`(本轮即删除三个模块级站点并把非 `this.state` 消费者留待后续,可能使这些消费者暂不可用)。"
|
||||
- "**`this.state` 改写预期为 0**:规划日实测未发现任何消费 `using` / `sceneController` / `mainUIController` 的类是暴露 `this.state` 的类(唯一带 `state` 的是 `client-base/src/material/{autotile,builder,manager}.ts` 且 `state: ICoreState` 不含被挂字段)。执行日必须实测复核;**若发现可转换站点**,改写前**必须停下在 Task 0 关卡重报并更新 `files_modified`**(本计划只列 `types.ts` / `client.ts` / SUMMARY,不得悄悄扩大改动面)。"
|
||||
- "**F-03 本轮不删重复**:`mainRenderer` 的消费者非 `this.state` 可转换,且移除会连带删掉 `createApp` 的来源(又不许 alias / `import { client }`),故模块级 `rafExcitation` / `excitationDivider` / `mainRenderer` 本轮保留为 staged;F-03 的实现为「`ClientCore` 实例权威」这一声明 + 迁移去向登记,而非删除。若用户要求本轮即删,需在关卡重裁(会牵连 `render/index.tsx` 的 `createApp`)。"
|
||||
- "**忽略 `client-system` / `IClientSystem`**:用户明确要求本轮不修改 `packages-user/client-system`;`IClientCore` 仍 `extends IClientSystem` 不变,`IClientSystemExtended` 不被实现 / 使用。"
|
||||
- "**`IClientBase` 零改动**:client-base 侧在步单例仅 `texture`(F-01 保留)与 5 个 `GameUI`(F-04 / F-07 保留),均不得挂载,故 `IClientBase`(`packages-user/client-base/src/types.ts:8`)**不加任何字段**,本轮不触碰 `client-base` 任何生产文件。"
|
||||
- "**规划日实测行号(2026-10-01,须用户在 Task 0 确认,执行日以当日实测为准):** `types.ts:1,13-20`;`client.ts:1-17,41,43-56,58,90,91,108-114`;`render/renderer.ts:18,20,35,43,45`;`render/scene.ts:3`;`render/ui/controller.tsx:11`。用户会并发改其它文件(D-56 精神),执行时一律按符号名 / 文本检索定位,行号漂移按当日实测改写并在 SUMMARY 说明。"
|
||||
- "**无运行时验证可得(D-68 的延续):** 本步不要求「保证能运行」,不启动渲染、不运行任何运行时用例;承载体为静态门禁 + 人工复核。**不得**以 `pnpm check:type` / `pnpm build` / TS 诊断数冒充门禁。`IClientCore` 两处 `UIController` 字段的类型可满足性、`ClientCore` 构造器新增 `new` 的模块求值顺序、以及 staged 并存实例的运行时后果,**静态门禁不可完全证明**,仅由人工阅读确认形状并如实登记为「无运行时验证」。"
|
||||
prohibitions:
|
||||
- "不得在用户明确回复「可以执行」四个字之前执行本计划的任何任务、或派发任何执行子代理(AGENTS.md 的第一个 block 点约束;不因触发方式或 `yolo` / `auto_advance` 模式而放宽)。"
|
||||
- "不得新增 / 改名 / 删除任何公共、受保护、私有成员、方法、字段、接口或文件,**除 Task 0 汇报并获批的三个新增字段**(`IClientCore` / `ClientCore` 的 `using` / `sceneController` / `mainUIController`)外;**不得**给 `ClientCore` / `IClientCore` 新增 `state` 字段;不得自行命名任何其它标识符。"
|
||||
- "不得创建转发别名(禁 `export const x = client.x` / 任何 `export * from` 越界转发,dev.md 不转发导出、D-69);不得新增任何 `import { client }` 全局使用(D-23);不得为迁移而改写非 `this.state` 消费者。"
|
||||
- "不得修改 `packages-user/client-system`(`IClientSystem` 及其 `IClientSystemExtended` 一律不动);不得修改 `packages-user/client-base` 任何生产文件(含 `IClientBase` 与 `material/**`,D-42 / D-48)。"
|
||||
- "不得删除 / 改写 `render/renderer.ts` / `render/scene.ts` / `render/ui/controller.tsx` 的模块级站点与既有导出(staged 保留);不得删除 `rafExcitation` / `excitationDivider` / `mainRenderer`(F-03 本轮 staged)。"
|
||||
- "不得违反 F-01 / F-04 / F-07 / `DEFAULT_FONT` 保持项:`texture` / `DEFAULT_FONT` / 全部 `GameUI` 实例(含 `MainBackgroundUI`)与 `client` 本身零触碰、零挂载;`createMainController()` 不动。"
|
||||
- "不得修改任何既有 jsDoc / 注释文本(AGENTS.md);新增注释可加但须遵循 dev.md;不得使用 `import type`。"
|
||||
- "不得触碰、暂存、提交用户的并发未提交改动:`packages-user/data-system/src/combat/context.ts`(**必须原样保留**)与 `.planning/phases/04-render-adaptation/04-CONTEXT.md`(如仍为未提交)一律不修改、不暂存、不提交;提交前以 `git status` / `git diff --cached --name-only` 逐条核对。"
|
||||
- "不得修改 `packages-user/data-*`、其它 `packages-user/*`、`packages/**`、`src/**`;不得触碰任何测试文件、任何 legacy 面(D-49 / D-55 / D-65);不得改动任何 `package.json` / `pnpm-lock.yaml` / `tsconfig.json` / lint 配置、不得新增依赖、不得执行 `pnpm i`;不得修改任何 barrel(D-69)。"
|
||||
- "不得把任何门禁建立在「全仓 / 范围内 TS 诊断数」「`pnpm check:type`」「`pnpm build`」之上,也不得因这类检查失败而阻塞交付(D-68)。"
|
||||
- "不得以「无运行时」为由弱化验证:每条 `<automated>` 必须有 `<fails_when>`;确实无法运行的门禁必须显式声明为不可得并降级为静态 + 人工,不得伪造绿。"
|
||||
- "不得改写 / 覆盖 / 改号 `04-01`..`04-15` 的既有 `*-PLAN.md` / `*-SUMMARY.md`;不得改写既有 `04-CONTEXT.md` / `04-RENDER-SINGLETON-AUDIT.md`;不得产出 `04-17+` 或任何新计划文件。除本 `04-16-PLAN.md`、`04-16-SUMMARY.md`、`files_modified` 列出的两个生产文件外,不得写任何其它文件(含 `.planning/STATE.md` / `WINDOWS.md` / `REQUIREMENTS.md`;`.planning/ROADMAP.md` 的登记由编排器在计划阶段完成,本计划不写它)。"
|
||||
---
|
||||
<objective>
|
||||
Phase 4 第十六个增量(用户 2026-10-01 去单例化裁定)—— **渲染端去单例化·第二步(挂载到主类)·代码改动**。
|
||||
|
||||
**要解决的问题:**
|
||||
|
||||
`04-15` 只读台账(`.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md`)清点出渲染端两个包内 **30** 条单例(模块顶层实例化对象 `export const x = new X()`):`packages-user/client-modules` 24 条 + `packages-user/client-base` 6 条。用户 2026-10-01 裁定渲染端不得再有单例,须全部挂到主类 `ClientCore` / `client`,并镜像数据端的「单一具体类 + 分层接口」结构。本次会话用户**固定了设计(option B)**并给出了 F 类裁定,本计划据其落地。
|
||||
|
||||
**设计 = option B(镜像数据端):**
|
||||
|
||||
数据端结构(对照):`IDataCommon`(`data-common/src/types.ts:50`)→ `IStateBase extends IDataCommon`(`data-base/src/types.ts:17`)→ `IStateSystem extends IStateBase`(`data-system/src/types.ts:7`)→ `ICoreState extends IStateSystem,...`(`data-state/src/types.ts:26`);**每层接口只声明该层的 `readonly` 字段**,**单一具体类 `CoreState implements ICoreState`**(`data-state/src/core.ts:77`)实现整条链并在构造器内构造全部子系统;**不存在** `DataCommon` / `StateBase` / `StateSystem` 类。
|
||||
|
||||
渲染端镜像:**单一具体类 `ClientCore`**(`client-modules/src/client.ts:41`,`implements IClientCore`)实现整条链(`IClientBase` → `IClientSystem` → `IClientCore`)。
|
||||
- **client-modules 层的字段** → 加在 `IClientCore`(`client-modules/src/types.ts:13`),并在 `ClientCore` 构造器内构造 / 挂载。
|
||||
- **client-base 层的字段** → 加在 `IClientBase`(`client-base/src/types.ts:8`)——但**当前无在步单例可挂**:`texture`(F-01)保留、5 个 `GameUI`(F-04 / F-07)保留,故 **`IClientBase` 不加任何字段、`client-base` 包本轮零改动**(显式声明)。
|
||||
- **忽略 `IClientSystem` / `client-system` 包**(用户明确要求,不修改)。
|
||||
- **不给 `ClientCore` 加 `state` 字段**:`IClientBaseExtended` / `ICoreStateExtended` 是给**需要回指的子系统**实现的,不是顶层类。
|
||||
|
||||
**本次会话锁定裁定(逐条照做,不得削范围):**
|
||||
|
||||
| 裁定 | 内容 | 落点 |
|
||||
|---|---|---|
|
||||
| **F-01 `texture`** | 其整套接口稍后另行重构 → **不触碰 `texture`**(保持原样) | `client-base/src/elements/cache.ts:280`(不改) |
|
||||
| **F-02 外部包单例** | 仍挂主类,且**在 `ClientCore` 构造器内构造 / 挂载**:`using`、`sceneController`、`mainUIController` | `client.ts` 构造器 |
|
||||
| **F-03 重复实例(staged)** | 权威实例恒为 **`ClientCore` 上的那一份**;`render/renderer.ts` 模块级 `rafExcitation` / `excitationDivider` / `mainRenderer` 的消费者均非 `this.state` 可转换 → **本轮保留为 staged**,不删除 | 「ClientCore 权威」声明 + staged 登记 |
|
||||
| **F-04** | `GameUI` 实例**不得挂载**(保持原样);`UIController` 实例**必须挂载**(`sceneController`、`mainUIController`) | `ClientCore` 字段 |
|
||||
| **F-05 接口选择按包** | 位于 `packages-user/client-base` 的子系统 → 实现 `ICoreStateExtended`;位于 `packages-user/client-modules` 的 → 实现 `IClientBaseExtended`。**本轮无新增实现**(无子系统需要回指;顶层类不加 `state`) | —(显式声明) |
|
||||
| **F-06** | 已解决 / moot | — |
|
||||
| **F-07** | 全部 UI 保留、不触碰 | 各 `*UI`(不改) |
|
||||
| **`DEFAULT_FONT`** | `client-modules/src/shared.ts:105` → **不触碰** | 不改 |
|
||||
|
||||
**消费者改写规则(用户原话:能直接通过 `this.state` 调用的直接改了,不能通过这个办法调用的先不管):**
|
||||
- 仅当消费者本身是**暴露 `this.state` 且其类型链声明了被挂字段的类**时,才改写为 `this.state.<field>`;**否则保持原样**。
|
||||
- **不得**新增 `import { client }` 全局使用(D-23);**不得**创建转发别名(`export const x = client.x`,dev.md 不转发导出)。
|
||||
- 规划日实测:消费 `using` / `sceneController` / `mainUIController` 的站点**均非**此类(唯一带 `state` 的类是 `client-base/src/material/{autotile,builder,manager}.ts`,其 `state: ICoreState` 不含被挂字段)→ 预期 **0 处**改写;执行日实测复核。
|
||||
- 模块级站点与导出在非 `this.state` 消费者仍需时**保留(staged)**——本轮不删除任何模块级站点。
|
||||
|
||||
**要新增的字段命名(AGENTS.md:任何命名须先经用户批准;全部在 Task 0 汇报):**
|
||||
|
||||
| 名称 | 所在 | 类型 | 来源(被去单例化的旧符号) |
|
||||
|---|---|---|---|
|
||||
| `using` | `IClientCore` + `ClientCore` | `IRendererUsing`(`@motajs/render-vue`,`packages/render-vue/src/types.ts:67`) | `render/renderer.ts:43` |
|
||||
| `sceneController` | `IClientCore` + `ClientCore` | `UIController`(`@motajs/system`,`packages/system/src/ui/controller.ts:82`) | `render/scene.ts:3` |
|
||||
| `mainUIController` | `IClientCore` + `ClientCore` | `UIController`(`@motajs/system`) | `render/ui/controller.tsx:11` |
|
||||
|
||||
**明确不新增:** `ClientCore` / `IClientCore` 的 `state` 字段(顶层类不加);`IClientBase` 的任何字段;`IClientSystem` / `client-system` 的任何改动。
|
||||
|
||||
**源 → 目标映射:**
|
||||
|
||||
| 定义 file:line | 符号 | 处置 |
|
||||
|---|---|---|
|
||||
| `render/renderer.ts:43` | `using` | **挂载**到 `ClientCore.using`(构造器 `new RendererUsing(this.renderer)`);模块级站点 **staged 保留**(消费者 `render/ui/main.tsx` 非 `this.state`) |
|
||||
| `render/scene.ts:3` | `sceneController` | **挂载**到 `ClientCore.sceneController`(构造器 `new UIController('main-scene')`);模块级站点 **staged 保留** |
|
||||
| `render/ui/controller.tsx:11` | `mainUIController` | **挂载**到 `ClientCore.mainUIController`(构造器 `new UIController('main-ui')`);模块级站点 **staged 保留** |
|
||||
| `render/renderer.ts:18,20,35` | `rafExcitation` / `excitationDivider` / `mainRenderer` | F-03:`ClientCore` 实例权威;模块级重复 **staged 保留**(消费者非 `this.state`) |
|
||||
| 其余 24 条单例 | `client` / `texture` / `DEFAULT_FONT` / 全部 `GameUI` 实例 | F-01 / F-04 / F-07:**保持原样**(不挂载、不触碰) |
|
||||
|
||||
**staged 并存后果(须用户知晓):** 因模块级站点保留,`using` / `sceneController` / `mainUIController` 会同时存在 `ClientCore` 权威实例与模块级 staged 实例。`UIController` 的静态注册表以 id 去重——`new UIController('main-scene')` / `('main-ui')` 二次构造会触发 `logger.warn(57, id)` 且不覆盖注册项。这是本轮 staged 迁移的已知后果(登记为 Known Stub);若用户不接受并存,请选 `move-module-sites`。
|
||||
|
||||
**范围与硬边界:** 改动面 = `packages-user/client-modules/src/types.ts`、`packages-user/client-modules/src/client.ts` + `04-16-SUMMARY.md`。**零触碰** = `packages-user/client-modules/src/render/**`(staged 保留)、`packages-user/client-base` 全包(含 `IClientBase`、`material/**`)、`packages-user/client-system`、数据端一切生产文件(含用户未提交 `packages-user/data-system/src/combat/context.ts`)、其它 `packages-user/*`、`packages/**`、`src/**`、任何测试 / legacy 面、任何 `package.json` / lockfile、任何 barrel(D-69)。无依赖变更、不执行 `pnpm i`、不新增 / 改名公共成员(除三个获批字段)。
|
||||
|
||||
**任务切分(Task 0 汇报关卡 + Task 1 tracer + 1 个铺开任务 + 1 个 staged/消费者核对任务 + Task 4 收口):** 委托明确指定 `04-16` 为唯一新计划,故落在**单一计划文件**内。
|
||||
|
||||
- **Task 0** = `checkpoint:decision`(`gate="blocking-human"`):汇报问题 / 设计 B / F 裁定 / 全部新增命名(3 个,无 `state`)/ 源→目标映射(含 staged 保留项)/ 消费者改写规则 / 范围 / 验证方式,等待「可以执行」。
|
||||
- **Task 1(tracer)= `using` 一条线端到端**:`IClientCore.using` 声明 → `ClientCore.using` 字段 + 构造器 `new RendererUsing(this.renderer)` → `this.state` 扫描结论;模块级站点保持 staged。
|
||||
- **Task 2** = `sceneController` + `mainUIController` 挂载(同形态铺开)。
|
||||
- **Task 3** = `this.state` 消费者扫描与(若有)改写 + F-03 staged 校验 + staged 站点保留 / 无 alias / 无新 `import { client }` 门禁。
|
||||
- **Task 4** = 收口(静态门禁 + 范围 / 用户改动基线 + 既有产物保护 + ROADMAP 登记 + CRLF + 人工复核 + `04-16-SUMMARY.md` + 白名单提交)。
|
||||
|
||||
## Source Audit(多源覆盖审计)
|
||||
|
||||
| SOURCE | ID | Feature/Requirement | Plan | Status | Notes |
|
||||
|--------|----|---------------------|------|--------|-------|
|
||||
| GOAL | — | Phase 4:「渲染端通过新数据层接口驱动,并同时支持移动端与桌面端布局」 | 04-16 | COVERED(增量·去单例化第二步) | 本 run 落地「单例挂到主类」;消费者迁移、双布局属后续增量 |
|
||||
| REQ | REND-01 | 渲染端适配新数据层接口 | 04-16 | COVERED(子切片·代码改动;需求保持 Pending) | 「不得有单例、挂主类、镜像数据端结构」是 REND-01 的架构切片;验收边界未提供 → FLAGGED ASSUMPTION |
|
||||
| REQ | REND-02 | 渲染端同时支持移动端与桌面端布局 | 04-16 | COVERED(不涉及) | 本 run 范围不含双布局;验收边界未提供 → FLAGGED ASSUMPTION |
|
||||
| RESEARCH | — | (本阶段无 `04-RESEARCH.md`) | — | N/A | 等价输入 = `04-RENDER-SINGLETON-AUDIT.md` + 本次会话固定的 option B 设计与 F-01..F-07 |
|
||||
| CONTEXT | 设计 B(本次固定) | 单一具体类实现整链;`IClientBase` 不加字段;忽略 `client-system`;顶层类不加 `state` | 04-16 | COVERED | Task 1 / Task 2(字段加在 `IClientCore`)+ must_haves 显式声明 |
|
||||
| CONTEXT | F-01 | `texture` 不触碰 | 04-16 | COVERED(保持项) | prohibitions / 范围门禁 |
|
||||
| CONTEXT | F-02 | 外部包单例在 `ClientCore` 构造器内构造 / 挂载 | 04-16 | COVERED | Task 1(`using`)/ Task 2(两个 `UIController`) |
|
||||
| CONTEXT | F-03 | 重复实例以 `ClientCore` 为权威;模块级重复 staged | 04-16 | COVERED(staged 声明) | Task 3(staged 校验;本轮不删) |
|
||||
| CONTEXT | F-04 | `GameUI` 不挂载;`UIController` 必须挂载 | 04-16 | COVERED | Task 2 + 保持项门禁 |
|
||||
| CONTEXT | F-05 | 按包归属选接口(client-base → `ICoreStateExtended`;client-modules → `IClientBaseExtended`) | 04-16 | COVERED(本轮零新增实现) | 无子系统需要回指;顶层类不加 `state`;显式声明 |
|
||||
| CONTEXT | F-06 / F-07 | moot;全部 UI 保留 | 04-16 | COVERED | 无任务(保持项门禁) |
|
||||
| CONTEXT | `DEFAULT_FONT` | 不触碰 | 04-16 | COVERED(保持项) | 范围门禁 |
|
||||
| CONTEXT | D-23 | 不新增全局单例访问 | 04-16 | COVERED | 无新 `import { client }`;`this.state` 为合规替代 |
|
||||
| CONTEXT | D-33 / D-42 / D-48 | 范围外不改;`material/**` 归用户 | 04-16 | COVERED(边界) | 范围门禁 |
|
||||
| CONTEXT | D-49 / D-55 / D-65 | 不考虑任何 legacy | 04-16 | COVERED(零触碰) | 无 legacy 任务 / 门禁 / 待办 |
|
||||
| CONTEXT | D-56(精神) | 用户并发改动零触碰 | 04-16 | COVERED | Task 1 (0a) 基线 + `git hash-object` 标记;各任务断言 |
|
||||
| CONTEXT | D-68 | 不要求可运行、不以 TS 诊断数 / `check:type` / `build` 为门禁 | 04-16 | COVERED(延续) | 全部门禁为静态;prohibitions 明列 |
|
||||
| CONTEXT | D-69 | 桶导出边界原则(硬约束) | 04-16 | COVERED(不触发) | 本步不改任何 barrel |
|
||||
| LEDGER | `#04-15-A-06,A-07,A-08` | `using` / `sceneController` / `mainUIController` | 04-16 | COVERED | Task 1 / Task 2 |
|
||||
| LEDGER | `#04-15-A-03,A-04,A-05` | `rafExcitation` / `excitationDivider` / `mainRenderer` | 04-16 | COVERED(staged) | Task 3(F-03 声明;本轮不删) |
|
||||
| LEDGER | 其余 24 条单例 | `client` / `texture` / `DEFAULT_FONT` / 全部 `GameUI` 实例 | — | DEFERRED(保持原样) | F-01 / F-04 / F-07 |
|
||||
| Deferred Ideas | — | 阶段 4 后续(消费者迁移、`texture` 后续接口重构、双布局、外部包架构耦合收尾);`packages` 全部、`legacy-plugin-*`、`data-fallback` | — | DEFERRED | 未写入任何任务 |
|
||||
|
||||
## Estimate Basis(估算依据)
|
||||
|
||||
`estimate-calibration` 无历史样本(`factor: 1`、`applied: false`、`sample_count: 0`、`confidence: low`),故 `tokens == raw_tokens` 并如实标注 `confidence: low`。按「执行者实际触及量」估算(`estimate.tasks: 5`):Task 0 只读汇报并等待(约 6k);Task 1(tracer)只读基线 + `using` 端到端 + `this.state` 扫描(约 9k);Task 2 两个 `UIController` 挂载(约 8k);Task 3 staged / 消费者核对 + 门禁(约 7k);Task 4 收口 + 人工复核 + SUMMARY + 白名单提交(约 10k);合计约 40k。**本计划刻意不做多计划拆分**:委托明确要求只产出 `04-16` 一份计划。
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/phases/04-render-adaptation/04-CONTEXT.md
|
||||
@.planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md
|
||||
@.planning/phases/04-render-adaptation/04-15-PLAN.md
|
||||
@.planning/phases/04-render-adaptation/04-14-PLAN.md
|
||||
@packages-user/data-state/src/core.ts
|
||||
@packages-user/data-common/src/types.ts
|
||||
@packages-user/data-base/src/types.ts
|
||||
@packages-user/data-system/src/types.ts
|
||||
@packages-user/data-state/src/types.ts
|
||||
@packages-user/client-base/src/types.ts
|
||||
@packages-user/client-system/src/types.ts
|
||||
@packages-user/client-modules/src/types.ts
|
||||
@packages-user/client-modules/src/client.ts
|
||||
@AGENTS.md
|
||||
@dev.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报「渲染端去单例化·第二步(挂载到主类)」的问题、option B 设计、F-01..F-07 + `DEFAULT_FONT`、**全部新增命名(`using` / `sceneController` / `mainUIController`,无 `state`)**、源→目标映射(含 staged 保留项)、消费者改写规则、范围与硬边界、验证方式,等待用户回复「可以执行」</name>
|
||||
<precondition>用户 2026-10-01 去单例化裁定与本次会话固定的 option B 设计(镜像数据端:单一具体类实现整链;`IClientBase` 零改动;忽略 `client-system`;顶层类不加 `state`)已下达;`04-15` 只读台账 `04-RENDER-SINGLETON-AUDIT.md` 已在场(30 条单例);F-01..F-07 + `DEFAULT_FONT` 锁定裁定已下达;`client.ts:41,90,91,108`、`types.ts:13`、`render/renderer.ts:18,20,35,43`、`render/scene.ts:3`、`render/ui/controller.tsx:11`、`client-base/src/types.ts:8`、`client-system/src/types.ts:3`、`data-state/src/core.ts:77` 与规划日实测一致(执行日以当日实测为准)。</precondition>
|
||||
<files>(只读汇报,不修改任何文件)</files>
|
||||
<read_first>
|
||||
- AGENTS.md(「任何计划的执行,其第一个 block 点必须是向我汇报计划内容并等待我确认」;只有明确回复「可以执行」四个字之后才允许开始执行;「任何公共、受保护、私有方法、字段等内容的命名必须先向我反馈」;「不得擅自修改所有 jsDoc 注释,不得擅自修改任何已有注释」)
|
||||
- .planning/phases/04-render-adaptation/04-RENDER-SINGLETON-AUDIT.md(30 条单例 A-01..A-30、E 映射、F 类;本计划的权威输入)
|
||||
- .planning/phases/04-render-adaptation/04-15-PLAN.md 与 04-14-PLAN.md(上一增量 Task 0 汇报关卡的版式与口径)
|
||||
- packages-user/data-state/src/core.ts(`:77` 单一具体类 `CoreState`)与接口链 `data-common/src/types.ts:50` → `data-base/src/types.ts:17` → `data-system/src/types.ts:7` → `data-state/src/types.ts:26` —— **option B 镜像依据**
|
||||
- packages-user/client-base/src/types.ts:8,27、packages-user/client-system/src/types.ts:3,5、packages-user/client-modules/src/types.ts:13、packages-user/client-modules/src/client.ts:41,90,91,108
|
||||
- packages/render-vue/src/types.ts:67(`IRendererUsing`)与 packages/system/src/ui/controller.ts:82(`class UIController`)—— 新增字段的外部类型来源(D-33 只读)
|
||||
- dev.md(模块原则:无副作用 / 不转发导出 / 无循环引用 / 无类型导入 / 引入位置;命名规则;注释规范;CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
**停下并向用户汇报,等待明确回复后才继续;汇报本身只读,不写任何文件、不改任何生产代码、不派发子代理。**
|
||||
|
||||
① **要解决的问题**:渲染端 `packages-user/client-modules` 与 `packages-user/client-base` 内仍有**单例**(模块顶层实例化对象 `export const x = new X()`)。`04-15` 只读台账清点共 **30** 条(client-modules 24 / client-base 6)。用户 2026-10-01 裁定渲染端不得再有单例,须挂到主类 `ClientCore` / `client`,并镜像数据端结构。本计划 = **第二步(挂载到主类)**。
|
||||
|
||||
② **设计(option B,须逐条原样汇报)**:
|
||||
- **镜像数据端**:`IDataCommon` → `IStateBase extends IDataCommon` → `IStateSystem extends IStateBase` → `ICoreState extends IStateSystem,...` 每层只声明字段,**单一具体类 `CoreState implements ICoreState`**(`data-state/src/core.ts:77`)实现整链并在构造器构造子系统;**不存在** `DataCommon` / `StateBase` / `StateSystem` 类。
|
||||
- **渲染端镜像**:**单一具体类 `ClientCore implements IClientCore`** 实现整链(`IClientBase` → `IClientSystem` → `IClientCore`)。
|
||||
- **client-modules 层字段** → 加在 `IClientCore`(`client-modules/src/types.ts:13`)并在 `ClientCore` 构造器构造 / 挂载。
|
||||
- **client-base 层字段** → 本应加在 `IClientBase`(`client-base/src/types.ts:8`),但**当前无在步可挂单例**(`texture` F-01 保留、5 个 `GameUI` F-04 / F-07 保留)→ **`IClientBase` 不加任何字段,`client-base` 包本轮零改动**(显式声明)。
|
||||
- **忽略 `IClientSystem` / `client-system` 包**(用户明确要求,不修改)。
|
||||
- **不给 `ClientCore` 加 `state` 字段**(`IClientBaseExtended` / `ICoreStateExtended` 是给需要回指的子系统实现,不是顶层类)。
|
||||
|
||||
③ **F 裁定(须逐条汇报)**:**F-01** `texture` 不触碰;**F-02** 外部包单例在 `ClientCore` 构造器内构造 / 挂载;**F-03** 重复实例以 `ClientCore` 为权威、模块级重复 staged(本轮不删);**F-04** `GameUI` 不挂载 / `UIController` 必须挂载;**F-05** 按包归属选接口(client-base → `ICoreStateExtended`;client-modules → `IClientBaseExtended`)——本轮无新增实现;**F-06** moot;**F-07** 全部 UI 保留;**`DEFAULT_FONT`** 不触碰。沿用约束:D-23 / D-33 / D-42 / D-48 / D-49 / D-55 / D-56 / D-65 / D-68 / D-69。
|
||||
|
||||
④ **将新增的命名(须逐条原样呈报,供用户批准 —— AGENTS.md「任何命名必须先获批」的批准入口)**:
|
||||
1. `using` —— 类型 `IRendererUsing`(`@motajs/render-vue`,`packages/render-vue/src/types.ts:67`);加在 `IClientCore` 与 `ClientCore`;旧符号 `render/renderer.ts:43`。
|
||||
2. `sceneController` —— 类型 `UIController`(`@motajs/system`,`packages/system/src/ui/controller.ts:82`);加在 `IClientCore` 与 `ClientCore`;旧符号 `render/scene.ts:3`。
|
||||
3. `mainUIController` —— 类型 `UIController`(`@motajs/system`);加在 `IClientCore` 与 `ClientCore`;旧符号 `render/ui/controller.tsx:11`。
|
||||
- **明确不新增**:`state` 字段(顶层类不加);`IClientBase` 任何字段;`IClientSystem` / `client-system` 任何改动。**不新增 / 改名 / 删除任何其它公共、受保护、私有成员、方法、字段、接口或文件。**
|
||||
|
||||
⑤ **源 → 目标映射(须原样呈报,含 staged)**:`using` / `sceneController` / `mainUIController` 三个**挂载**到 `ClientCore`(构造器内 `new`),其模块级站点 **staged 保留**(消费者 `render/ui/main.tsx` / `render/index.tsx` / `render/action.ts` / `render/ui/statusBar.tsx` / `render/ui/toolbar.tsx` 均非 `this.state` 可转换);`rafExcitation` / `excitationDivider` / `mainRenderer`(F-03)以 `ClientCore` 为权威、模块级重复 **staged 保留**;其余 24 条(`client` / `texture` / `DEFAULT_FONT` / 全部 `GameUI`)**保持原样**。
|
||||
|
||||
⑥ **消费者改写规则(须原样呈报)**:仅当消费者本身是**暴露 `this.state` 且类型链声明了被挂字段的类**时才改写为 `this.state.<field>`;否则保持原样。**不得**新增 `import { client }` 全局使用(D-23);**不得**创建转发别名(dev.md 不转发导出)。规划日实测消费这三者的站点均非此类 → 预期 **0 处**改写(执行日实测复核;若发现可转换者,先停关卡重报并更新 `files_modified`)。
|
||||
|
||||
⑦ **staged 并存后果(须原样呈报)**:因模块级站点保留,`using` / `sceneController` / `mainUIController` 会同时存在 `ClientCore` 权威实例与模块级 staged 实例;`UIController` 静态注册表以 id 去重,二次 `new UIController('main-scene')` / `('main-ui')` 会触发 `logger.warn(57, id)` 且不覆盖注册项。这是本轮 staged 的已知后果(登记为 Known Stub);若用户不接受并存,请选 `move-module-sites`。
|
||||
|
||||
⑧ **范围与硬边界(须原样呈报)**:改动面 = `packages-user/client-modules/src/types.ts`、`packages-user/client-modules/src/client.ts` + `04-16-SUMMARY.md`。**零触碰** = `packages-user/client-modules/src/render/**`(staged 保留)、`packages-user/client-base` 全包(含 `IClientBase` / `material/**`)、`packages-user/client-system`、数据端一切生产文件(**尤其用户当前未提交的 `packages-user/data-system/src/combat/context.ts`,必须原样保留**)、其它 `packages-user/*`、`packages/**`、`src/**`、任何测试 / legacy 面、任何 `package.json` / lockfile、任何 barrel(D-69)。**无依赖变更、不执行 `pnpm i`、不使用 `import type`、不新增 / 改名公共成员(除 ④ 获批字段)。**
|
||||
|
||||
⑨ **验证方式(须原样呈报,声明无运行时要求)**:D-68 的延续——本步**没有「保证能运行」的要求**。证明方式全部为**静态**:① 文件内容断言(`IClientCore` / `ClientCore` 三字段 + 构造器 `new`);② **staged 站点保留断言**(`render/renderer.ts:18,20,35,43` / `render/scene.ts:3` / `render/ui/controller.tsx:11` 仍在);③ **逐符号站点反查**(用修正后的正则 `new (RafExcitation|ExcitationDivider|MotaRenderer|RendererUsing)[<(]` 与 `new UIController\(`,核对权威 + staged 计数);④ **无 alias / 无新 `import { client }`**;⑤ **范围门禁**(`git status --porcelain -- packages-user/client-base packages-user/client-system packages-user/client-modules/src/render packages packages/ src` 为空);⑥ **D-56 用户改动基线**(`combat/context.ts` 仍在场且哈希一致);⑦ **既有产物保护**(`04-01`..`04-15` porcelain 为空、无 `04-17+`);⑧ **CRLF**;⑨ **ROADMAP 登记**(`04-16-PLAN.md` 与 `**Wave 15**`);⑩ **人工复核**。**不得使用** `pnpm check:type` / `pnpm build` / TS 诊断数。**如实声明静态不可证项**:类型可满足性、运行时装配、staged 并存实例的最终收敛。
|
||||
|
||||
⑩ **编号与 wave(须原样告知)**:本计划为 **04-16**,`wave: 15`,`depends_on` 列 `04-01`..`04-15`;既有 `04-01`..`04-15` 产物不得改写 / 覆盖 / 改号,不得产出 `04-17+`。估算 **40k tokens**(`estimate.tasks: 5`)。
|
||||
|
||||
⑪ **关卡性质与失败语义(必须原样告知用户)**:无论触发方式(`/gsd-execute-phase`、`/gsd-manager`、`/gsd-autonomous`、子代理/执行器派发)或配置模式(`mode: yolo`、`workflow.auto_advance`)如何,**执行者 / 子代理都必须在此停下并等待**用户明确回复「可以执行」四个字;未收到前不得执行任何任务、不得派发执行子代理,也不得以「自动化模式」为由自动放行。回复其它任何内容(含修订意见、沉默、自动推进)均视为**未通过**:立即停止、不写任何文件。
|
||||
|
||||
⑫ **若用户给出修订意见**(含改动新增字段命名 / 要求本轮即删除模块级站点并迁移消费者 / 要求给顶层类加 `state` / 要求修改 `IClientBase` 或 `client-system`):退出本计划、修订 `04-16-PLAN.md` 后重新汇报,不得自行改道执行。
|
||||
</action>
|
||||
<decision>是否按本计划(04-16:渲染端去单例化·第二步·挂载到主类——option B 镜像数据端;在 `IClientCore` 与 `ClientCore` 上新增 `using` / `sceneController` / `mainUIController` 三个字段并在 `ClientCore` 构造器内构造;不给顶层类加 `state`;`IClientBase` / `client-system` / `render/**` 模块级站点 / 全部消费者保持不动(staged);`texture` / `DEFAULT_FONT` / 全部 `GameUI` 实例不触碰;只改 `client-modules/src/types.ts` 与 `client-modules/src/client.ts`)开始执行</decision>
|
||||
<context>
|
||||
AGENTS.md 要求任何计划的第一个 block 点是向用户汇报计划内容并等待用户确认。`.planning/config.json` 为 `mode: yolo`,故该关卡必须显式声明为 `blocking-human` 并置 `autonomous: false`。必须让用户明确知晓的三点:① **会新增三个公共字段命名**并写入 `IClientCore` / `ClientCore`(命名须先获批);② **本轮为 staged**——模块级站点与消费者不动,故 `using` / `sceneController` / `mainUIController` 会与模块级实例并存(`UIController` 同 id 二次构造会 `logger.warn`);③ **F-03 本轮不删重复**(`mainRenderer` 消费者非 `this.state`,删之会牵连 `createApp` 来源)。用户当前有未提交改动 `packages-user/data-system/src/combat/context.ts`(规划日实测 ` M`,`git hash-object` = `b4c4bcd698fab7d30d602b7cf6e84eb211740ad2`),必须完全不触碰。
|
||||
</context>
|
||||
<options>
|
||||
<option id="execute">
|
||||
<name>可以执行</name>
|
||||
<pros>按本计划落地:三个字段挂到 `IClientCore` / `ClientCore`(构造器内构造);`IClientBase` / `client-system` / 模块级站点 / 消费者保持不动;`texture` / `DEFAULT_FONT` / 全部 `GameUI` 不动;可整体 `git revert`</pros>
|
||||
<cons>staged 并存(权威实例 + 模块级实例),`UIController` 同 id 二次构造会 `logger.warn`(已登记 Known Stub)</cons>
|
||||
</option>
|
||||
<option id="move-module-sites">
|
||||
<name>可以执行,且本轮即删除三个模块级站点并把非 `this.state` 消费者留待后续</name>
|
||||
<pros>消除并存与同 id 重复构造;更接近「渲染端不得再有单例」的终态</pros>
|
||||
<cons>非 `this.state` 消费者(`render/index.tsx` / `render/ui/*` / `render/action.ts`)将暂不可用;扩大改动面(`files_modified` 需更新);需修订本计划并重新汇报</cons>
|
||||
</option>
|
||||
<option id="revise-interface">
|
||||
<name>可以执行,但改变字段命名 / 类型 / 落点</name>
|
||||
<pros>如要求给顶层类加 `state`、修改 `IClientBase` 或 `client-system`、或改用其它类型</pros>
|
||||
<cons>超出本计划获批命名与 option B 设计;需修订本计划并重新汇报</cons>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>修订范围 / staged 策略后再执行</name>
|
||||
<pros>避免带着错误的范围或 staged 策略落地后再返工</pros>
|
||||
<cons>退出本计划并修订 PLAN.md 后重新汇报,增加一轮往返</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>回复「可以执行」四个字(修订意见请按 action ⑫ 处理)</resume-signal>
|
||||
<verify>
|
||||
<human-check>用户在对话中明确回复「可以执行」四个字(人工核对用户回复原文;本关卡为人工关卡,无自动命令,失败语义写在 Task 0 的 action ⑪ 与 acceptance_criteria 中)</human-check>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 用户回复中明确包含「可以执行」四个字
|
||||
- 汇报内容涵盖:要解决的问题(30 条单例、第二步 = 挂载到主类)、**option B 设计(镜像数据端 / 单一具体类 / `IClientBase` 不加字段 / 忽略 `client-system` / 顶层类不加 `state`)**、**F-01..F-07 + `DEFAULT_FONT` 逐条**、**全部新增命名(`using` / `sceneController` / `mainUIController`,且明确无 `state`)与类型**、源→目标映射(含 staged 保留项)、消费者改写规则、staged 并存后果、范围与硬边界、验证方式
|
||||
- 汇报中已原样告知 **D-23**(不新增 `import { client }` 全局使用;`this.state` 为合规替代)与 **D-69 / dev.md 不转发导出**(禁 alias)
|
||||
- 汇报中已原样告知 **D-68 的延续**(无「保证能运行」要求;不设 `check:type` / `build` / TS 诊断数类门禁)与**静态不可证项**
|
||||
- 汇报中已原样告知**保持项**(`texture` / `DEFAULT_FONT` / 全部 `GameUI` / `client` 不动)与 **F-03 staged**(本轮不删重复)
|
||||
- 汇报中已原样告知 **编号与 wave**(04-16 / wave 15 / `depends_on` 04-01..04-15)及既有产物不得改写 / 改号、不得产出 `04-17+`
|
||||
- 汇报中已原样告知**关卡性质**:执行者 / 子代理必须停下等待,不因触发方式或 `yolo` / `auto_advance` 模式自动放行
|
||||
</acceptance_criteria>
|
||||
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
|
||||
</task>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1(tracer):一条线端到端 —— `using` 挂到主类:`IClientCore.using` 声明 → `ClientCore` 字段 + 构造器 `new RendererUsing(this.renderer)` → staged 站点保持 + `this.state` 扫描结论</name>
|
||||
<precondition>Task 0 已闭环:用户在对话中明确回复「可以执行」四个字(未收到则不得开始本任务);且用户已批准新增命名 `using`(类型 `IRendererUsing`)。</precondition>
|
||||
<files>packages-user/client-modules/src/types.ts, packages-user/client-modules/src/client.ts, .planning/phases/04-render-adaptation/04-16-SUMMARY.md</files>
|
||||
<read_first>
|
||||
- packages-user/client-modules/src/client.ts(整文件 138 行:`:1-39` import;`:41` `class ClientCore extends CoreState implements IClientCore`;`:43-56` 字段;`:58-121` 构造器;`:108-114` `this.renderer = new MotaRenderer({...})`)
|
||||
- packages-user/client-modules/src/types.ts(整文件 20 行:`:1-2` import;`:13-20` `export interface IClientCore extends IClientSystem`)
|
||||
- packages/render-vue/src/types.ts:67(`IRendererUsing`)与 packages/render-vue/src/use.ts:15(`RendererUsing implements IRendererUsing`)
|
||||
- packages-user/data-state/src/core.ts:77(单一具体类 `CoreState`,option B 参考)与 packages-user/client-system/src/types.ts:3(`IClientSystem extends IClientBase`)
|
||||
- packages-user/client-modules/src/render/renderer.ts:18,20,35,43,45(staged 模块级站点,**不得改动**)
|
||||
- 消费者(只读参照,**不得改动**):packages-user/client-modules/src/render/ui/main.tsx:31,155(`using`);packages-user/client-base/src/components/misc.tsx:15-16 与 textboxTyper.ts:7(`'../renderer'` 的 `@ts-expect-error` 反向耦合,D-59 / D-67 保留)
|
||||
- AGENTS.md(不得擅自修改既有注释;命名需先获批)与 dev.md(模块原则:无副作用 / 不转发导出 / 无循环引用 / 无类型导入 / 引入位置;CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(0a) **起始基线快照(只读前置,先做这一步)**:执行 `git status --porcelain -- packages packages-user src`,把输出**原样逐行**记为只读起始基线;执行 `git rev-parse HEAD` / `git rev-parse --abbrev-ref HEAD`;执行 `git hash-object -- packages-user/data-system/src/combat/context.ts`。用 Write 工具创建 `.planning/phases/04-render-adaptation/04-16-SUMMARY.md`(CRLF),先写入 frontmatter 与 `## 只读起始基线`:逐行抄录 porcelain(含用户未提交 ` M packages-user/data-system/src/combat/context.ts`,以当日实测为准)、起始 HEAD / 分支、机读标记行 `<!-- 04-16-baseline-hash-combat-context: <40hex> -->`(ASCII)。**不得回滚、暂存、提交或修改**任何既有改动。
|
||||
|
||||
(1) **`types.ts` —— 在 `IClientCore` 上新增 `using` 字段**:import 追加 `IRendererUsing`(`@motajs/render-vue`)。在 `IClientCore` 内新增只读字段声明 `readonly using: IRendererUsing;`(带 jsDoc,遵循 dev.md 成员注释风格)。**保持 `extends IClientSystem` 不变**;**不得**改为 `extends IClientBaseExtended`、**不得**新增 `state` 字段。**不得**改动既有 `mainMapRenderer` / `mainMapExtension` 字段与注释。
|
||||
|
||||
(2) **`client.ts` —— 新增 `ClientCore.using` 字段 + 构造器构造**:import 追加 `IRendererUsing, RendererUsing`(来自 `@motajs/render-vue`;遵守 dev.md「引入位置」,无注释 / 空行夹杂)。字段声明(显式类型)`readonly using: IRendererUsing;`(与既有字段风格一致)。构造器:在 `this.renderer = new MotaRenderer({...});`(`:108-114`)**之后**赋 `this.using = new RendererUsing(this.renderer);`。既有 `this.rafExcitation` / `this.excitationDivider` / `this.renderer` 保持权威(F-03),不动。
|
||||
|
||||
(3) **staged 站点保持**:**不得**修改 `render/renderer.ts` 的 `using`(`:43`)或任何模块级站点;**不得**创建 `export const using = client.using;` 之类别名。
|
||||
|
||||
(4) **`this.state` 扫描(本任务只扫描 `using` 的消费者并记录结论)**:用 `git grep -n "using" -- packages-user/client-modules/src packages-user/client-base/src` 与阅读法确认消费 `using` 的站点是否为本仓「暴露 `this.state` 且类型链声明了被挂字段的类」;**规划日预期为 0 处**。若发现**可转换**站点,**必须停下**在 Task 0 关卡重报并更新 `files_modified` 后再改;否则在 SUMMARY 记录「`using`:0 处可转换,全部保持原样」。
|
||||
|
||||
(5) **消费者零改动验证**:`render/ui/main.tsx` / `render/index.tsx` / `render/action.ts` / `render/ui/statusBar.tsx` / `render/ui/toolbar.tsx` / `render/ui/index.ts` / `client-base/**` 均**不得修一行**。
|
||||
|
||||
(6) **CRLF 与收口**:确认三个文件为 CRLF;只允许编辑这三个文件;**不得**改其它文件、不得新增计划文件、不得派发会写文件的子代理;**不得**执行 `pnpm check:type` / `pnpm build`。
|
||||
</action>
|
||||
<reversibility rating="reversible">本任务只新增一个字段并在构造器赋一个 `new`,无契约破坏、无依赖变更,可用 `git revert` 或重跑本任务回退。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const c=fs.readFileSync('packages-user/client-modules/src/client.ts','utf8');const t=fs.readFileSync('packages-user/client-modules/src/types.ts','utf8');const r=fs.readFileSync('packages-user/client-modules/src/render/renderer.ts','utf8');const bad=[];if(c.indexOf('readonly using: IRendererUsing;')<0)bad.push('client field missing');if(c.indexOf('this.using = new RendererUsing(this.renderer);')<0)bad.push('client ctor missing');if(c.indexOf('RendererUsing')<0||c.indexOf('@motajs/render-vue')<0)bad.push('client import missing');if(t.indexOf('readonly using: IRendererUsing;')<0)bad.push('IClientCore field missing');if(t.indexOf('extends IClientSystem')<0)bad.push('IClientSystem extends changed');if(t.indexOf('IClientBaseExtended')>=0||/readonly state\s*:/.test(t))bad.push('state/Extended wrongly added to types.ts');if(/readonly state\s*:/.test(c)||c.indexOf('this.state =')>=0)bad.push('state wrongly added to client.ts');if(r.indexOf('new RendererUsing(mainRenderer)')<0)bad.push('staged renderer.ts site removed');if(bad.length>0){console.error('TRACER MOUNT FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK using mounted, staged site kept, no state field');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "TRACER MOUNT FAILED"(`client.ts` / `types.ts` 缺 `using` 字段或构造赋值、误改 `extends`、误加 `state` / `IClientBaseExtended`、或 `render/renderer.ts` 的 staged 站点被删)</fails_when>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');function gg(p){try{return execFileSync('git',['grep','-n','-F','-e',p,'--','packages-user/client-modules','packages-user/client-base'],{encoding:'utf8'})}catch(e){return ''}}const bad=[];const a=gg('export const using = client.');if(a.trim().length>0)bad.push('FORBIDDEN ALIAS :: '+a.trim());const b=gg('export const sceneController = client.');if(b.trim().length>0)bad.push('FORBIDDEN ALIAS :: '+b.trim());const d=gg('export const mainUIController = client.');if(d.trim().length>0)bad.push('FORBIDDEN ALIAS :: '+d.trim());if(bad.length>0){console.error('ALIAS GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK no forwarding aliases introduced');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "ALIAS GATE FAILED" / "FORBIDDEN ALIAS"(引入了 `export const x = client.x` 转发别名 —— 违反 dev.md 不转发导出)</fails_when>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');const out=execFileSync('git',['status','--porcelain','--','packages-user/client-modules/src/render','packages-user/client-base/src/components/misc.tsx','packages-user/client-base/src/components/textboxTyper.ts','packages-user/client-base/src/elements/index.ts'],{encoding:'utf8'}).replace(/[ \\t\\r\\n]+$/,'');if(out.length>0){console.error('STAGED/CONSUMER TOUCHED :: '+out);process.exit(1)}console.log('OK staged sites + consumers untouched');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "STAGED/CONSUMER TOUCHED"(改动了 `client-modules/src/render/**` 的 staged 站点或 `client-base` 保留的 `@ts-expect-error` 耦合文件 —— 越出本轮范围)</fails_when>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');const fs=require('fs');const S='.planning/phases/04-render-adaptation/04-16-SUMMARY.md';const FILE='packages-user/data-system/src/combat/context.ts';const WANT=' M '+FILE;const bad=[];if(!fs.existsSync(S)){console.error('D-56 BASELINE FAILED :: SUMMARY MISSING');process.exit(1)}const t=fs.readFileSync(S,'utf8');const m=t.match(/<!-- 04-16-baseline-hash-combat-context: ([0-9a-f]{40}) -->/);if(!m)bad.push('BASELINE HASH MARKER MISSING');let o='';try{o=execFileSync('git',['status','--porcelain','--','packages-user/data-system'],{encoding:'utf8'})}catch(e){}const lines=o.replace(/\\r/g,'').split('\\n').map(l=>l.replace(/[ \\t]+$/,'')).filter(l=>l.length>0);if(lines.indexOf(WANT)<0)bad.push('USER EDIT MISSING');if(m){const cur=execFileSync('git',['hash-object','--',FILE],{encoding:'utf8'}).trim();if(cur!==m[1])bad.push('USER EDIT HASH DRIFT '+cur+' vs '+m[1]);}if(bad.length>0){console.error('D-56 BASELINE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK user edit present + hash matches baseline');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "D-56 BASELINE FAILED" / "SUMMARY MISSING" / "BASELINE HASH MARKER MISSING" / "USER EDIT MISSING" / "USER EDIT HASH DRIFT"(SUMMARY 缺基线哈希标记 / 用户未提交的 `combat/context.ts` 不在场(被回滚或提交)/ 被再编辑(哈希漂移)—— 违反 D-56 精神)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const files=['packages-user/client-modules/src/types.ts','packages-user/client-modules/src/client.ts','.planning/phases/04-render-adaptation/04-16-SUMMARY.md'];const bad=[];for(const f of files){const s=fs.readFileSync(f,'utf8');const lf=(s.match(/\\n/g)||[]).length;const crlf=(s.match(/\\r\\n/g)||[]).length;if(lf!==crlf)bad.push('NON-CRLF '+f)}if(bad.length>0){console.error('CRLF FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK CRLF tracer files');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "CRLF FAILED" / "NON-CRLF"(被编辑文件或 SUMMARY 出现非 CRLF 换行,违反 dev.md)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `types.ts` 的 `IClientCore` 新增 `readonly using: IRendererUsing;`;`extends IClientSystem` 不变;无 `state` / `IClientBaseExtended`
|
||||
- `client.ts` 新增 `readonly using: IRendererUsing;` 与构造器 `this.using = new RendererUsing(this.renderer);`(在 `this.renderer` 之后);无 `state` 字段
|
||||
- `render/renderer.ts` 的 staged 站点 `export const using = new RendererUsing(mainRenderer);` 仍在;无任何转发别名
|
||||
- `this.state` 扫描结论写入 SUMMARY(规划日预期 0 处可转换);消费者与 `client-base` 耦合文件零改动
|
||||
- `04-16-SUMMARY.md` 含只读基线 + 用户改动哈希标记且一致;三个文件 CRLF;未执行 `check:type` / `build`
|
||||
</acceptance_criteria>
|
||||
<done>端到端一条线走通:`using` 已声明于 `IClientCore` 并由 `ClientCore` 构造器构造;staged 站点保持;`this.state` 扫描已做且结论入 SUMMARY;无 alias、无 `state` 字段;门禁(挂载 / alias / staged / D-56 / CRLF)全绿</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2:铺开挂载 `sceneController` + `mainUIController`(F-02 / F-04)—— `IClientCore` 字段声明 + `ClientCore` 构造器 `new UIController('main-scene')` / `('main-ui')`;模块级 staged 站点保持</name>
|
||||
<precondition>Task 0 已闭环(用户已回复「可以执行」)且 Task 1 已交付(`using` 已挂载、staged 校验通过);用户已批准新增命名 `sceneController` / `mainUIController`(类型 `UIController`)。</precondition>
|
||||
<files>packages-user/client-modules/src/types.ts, packages-user/client-modules/src/client.ts</files>
|
||||
<read_first>
|
||||
- packages-user/client-modules/src/client.ts(Task 1 后状态:已含 `using`;本任务追加两个字段与构造赋值)
|
||||
- packages-user/client-modules/src/types.ts(Task 1 后状态:`IClientCore extends IClientSystem` 且含 `using`;本任务追加两个字段声明)
|
||||
- packages-user/client-modules/src/render/scene.ts:3 与 render/ui/controller.tsx:11(staged 模块级站点,**不得改动**)
|
||||
- packages/system/src/ui/controller.ts:82(`class UIController`)—— 字段类型来源(D-33 只读)
|
||||
- 消费者(只读参照,**不得改动**):render/index.tsx:8,17,25(`sceneController`);render/ui/main.tsx:29,281,284、render/action.ts:5,17-36、render/ui/statusBar.tsx:7,380、render/ui/toolbar.tsx:23,94-195、render/ui/controller.tsx:32(`mainUIController`)
|
||||
- AGENTS.md(不得擅自修改既有注释;命名需先获批)与 dev.md(模块原则:无副作用 / 不转发导出 / 无循环引用;引入位置;CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **`types.ts` —— 新增两个只读字段**:import 追加 `UIController`(`@motajs/system`,并入既有 import 组,遵守「引入位置」)。在 `IClientCore` 内新增 `readonly sceneController: UIController;` 与 `readonly mainUIController: UIController;`(带 jsDoc)。**不得**改动既有字段与注释、**不得**加 `state`。
|
||||
|
||||
(2) **`client.ts` —— 新增两个字段并在构造器构造**:import 追加 `UIController`(`@motajs/system`)。字段声明 `readonly sceneController: UIController;` 与 `readonly mainUIController: UIController;`。构造器加 `this.sceneController = new UIController('main-scene');` 与 `this.mainUIController = new UIController('main-ui');`(F-02:在 `ClientCore` 构造器内构造)。字符串 id `'main-scene'` / `'main-ui'` 与旧单例完全一致,不得改动。
|
||||
|
||||
(3) **staged 站点保持**:**不得**修改 `render/scene.ts:3` 的 `export const sceneController = new UIController('main-scene');` 或 `render/ui/controller.tsx:11` 的 `export const mainUIController = new UIController('main-ui');`;**不得**创建任何 `export const x = client.x;` 别名;`MainBackgroundUI`(`GameUI`,F-04 / F-07)与 `createMainController()` 保持原样。
|
||||
|
||||
(4) **`this.state` 扫描**:对 `sceneController` / `mainUIController` 的消费者做同 Task 1 的扫描并记录结论(规划日预期 0 处可转换);若发现可转换站点,**必须停下**在 Task 1 关卡重报并更新 `files_modified` 后再改。
|
||||
|
||||
(5) **消费者零改动验证**:`render/index.tsx` / `render/ui/main.tsx` / `render/action.ts` / `render/ui/statusBar.tsx` / `render/ui/toolbar.tsx` / `render/ui/index.ts` / `render/ui/controller.tsx` 均**不得修一行**。
|
||||
|
||||
(6) **CRLF 与收口**:确认两个文件为 CRLF;只允许编辑这两个文件;**不得**改其它文件、不得新增计划文件、不得派发会写文件的子代理;**不得**执行 `pnpm check:type` / `pnpm build`。
|
||||
</action>
|
||||
<reversibility rating="reversible">本任务只新增两个字段并在构造器赋两个 `new`,无契约破坏、无依赖变更,可用 `git revert` 或重跑本任务回退。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const c=fs.readFileSync('packages-user/client-modules/src/client.ts','utf8');const t=fs.readFileSync('packages-user/client-modules/src/types.ts','utf8');const s=fs.readFileSync('packages-user/client-modules/src/render/scene.ts','utf8');const u=fs.readFileSync('packages-user/client-modules/src/render/ui/controller.tsx','utf8');const bad=[];for(const k of ['readonly sceneController: UIController;','readonly mainUIController: UIController;',\\\"this.sceneController = new UIController('main-scene');\\\",\\\"this.mainUIController = new UIController('main-ui');\\\"]){if(c.indexOf(k)<0)bad.push('client.ts missing :: '+k)}for(const k of ['readonly sceneController: UIController;','readonly mainUIController: UIController;']){if(t.indexOf(k)<0)bad.push('types.ts missing :: '+k)}if(s.indexOf(\\\"new UIController('main-scene')\\\")<0)bad.push('staged scene.ts site removed');if(u.indexOf(\\\"new UIController('main-ui')\\\")<0)bad.push('staged controller.tsx site removed');if(u.indexOf(\\\"new GameUI('main-background'\\\")<0)bad.push('MainBackgroundUI (GameUI) must stay');if(/readonly state\s*:/.test(t)||/readonly state\\s*:/.test(c))bad.push('state wrongly added');if(bad.length>0){console.error('UI MOUNT FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK sceneController + mainUIController mounted, staged sites + GameUI kept');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "UI MOUNT FAILED"(`client.ts` / `types.ts` 缺两字段或构造赋值、`render/scene.ts` / `render/ui/controller.tsx` 的 staged 站点被删、`MainBackgroundUI` 被误删、或误加 `state`)</fails_when>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');function gg(p){try{return execFileSync('git',['grep','-n','-F','-e',p,'--','packages-user/client-modules','packages-user/client-base'],{encoding:'utf8'})}catch(e){return ''}}const bad=[];for(const a of ['export const sceneController = client.','export const mainUIController = client.','export const using = client.']){const g=gg(a);if(g.trim().length>0)bad.push('FORBIDDEN ALIAS :: '+a)}if(bad.length>0){console.error('ALIAS GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK no forwarding aliases');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "ALIAS GATE FAILED" / "FORBIDDEN ALIAS"(引入了转发别名 —— 违反 dev.md 不转发导出)</fails_when>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');const o=execFileSync('git',['status','--porcelain','--','packages-user/client-modules/src/render','packages-user/client-base'],{encoding:'utf8'}).replace(/[ \\t\\r\\n]+$/,'');if(o.length>0){console.error('STAGED/CONSUMER TOUCHED :: '+o);process.exit(1)}console.log('OK client-modules/render + client-base untouched');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "STAGED/CONSUMER TOUCHED"(改动了 `client-modules/src/render/**` staged 站点或 `client-base` 任何生产文件 —— 越出本轮范围)</fails_when>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');const out=execFileSync('git',['status','--porcelain','--','packages-user/client-system','packages','packages/','src'],{encoding:'utf8'}).replace(/[ \\t\\r\\n]+$/,'');if(out.length>0){console.error('FORBIDDEN PATH TOUCHED :: '+out);process.exit(1)}console.log('OK client-system/packages/src clean');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "FORBIDDEN PATH TOUCHED"(触碰 `client-system` / `packages/` / `src/` —— 违反用户「忽略 client-system」与 D-33)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const files=['packages-user/client-modules/src/types.ts','packages-user/client-modules/src/client.ts'];const bad=[];for(const f of files){const s=fs.readFileSync(f,'utf8');const lf=(s.match(/\\n/g)||[]).length;const crlf=(s.match(/\\r\\n/g)||[]).length;if(lf!==crlf)bad.push('NON-CRLF '+f)}if(bad.length>0){console.error('CRLF FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK CRLF UI-mount files');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "CRLF FAILED" / "NON-CRLF"(被编辑文件出现非 CRLF 换行,违反 dev.md)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `IClientCore` 含 `readonly sceneController: UIController;` 与 `readonly mainUIController: UIController;`
|
||||
- `ClientCore` 含两字段与构造器 `this.sceneController = new UIController('main-scene');`、`this.mainUIController = new UIController('main-ui');`
|
||||
- `render/scene.ts` / `render/ui/controller.tsx` 的 staged 站点仍在;`MainBackgroundUI`(`GameUI`)与 `createMainController()` 保持不动;无 alias;无 `state` 字段
|
||||
- `this.state` 扫描结论入 SUMMARY;`client-modules/src/render/**` 与 `client-base` 零改动;`client-system` / `packages/` / `src/` porcelain 为空;两文件 CRLF;未执行 `check:type` / `build`
|
||||
</acceptance_criteria>
|
||||
<done>两个 `UIController` 已声明于 `IClientCore` 并由 `ClientCore` 构造器构造;模块级 staged 站点与 `GameUI` 保持不动;无 alias、无 `state`;`this.state` 扫描结论入 SUMMARY;门禁(挂载 / alias / staged / 范围 / CRLF)全绿</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3:`this.state` 消费者扫描与(若有)改写 + F-03 staged 校验 + 逐符号站点反查(修正正则)+ staged 站点保留 / 无 alias / 无新 `import { client }` 门禁</name>
|
||||
<precondition>Task 0 已闭环(用户已回复「可以执行」)且 Task 1 / Task 2 已交付(三个字段挂到 `IClientCore` / `ClientCore`)。</precondition>
|
||||
<files>.planning/phases/04-render-adaptation/04-16-SUMMARY.md</files>
|
||||
<read_first>
|
||||
- packages-user/client-modules/src/{types.ts,client.ts}(Task 1 / 2 后终态)
|
||||
- packages-user/client-modules/src/render/renderer.ts:18,20,35,43(staged 站点,**不得改动**)与 render/scene.ts:3、render/ui/controller.tsx:11(staged 站点,**不得改动**)
|
||||
- 消费者全景(只读参照):render/index.tsx、render/ui/main.tsx、render/action.ts、render/ui/statusBar.tsx、render/ui/toolbar.tsx、render/ui/index.ts、client-base/src/components/{misc,textboxTyper}.tsx、client-base/src/elements/index.ts
|
||||
- packages-user/client-base/src/material/{autotile.ts:75,builder.ts:30,117,manager.ts:85}(仓内唯一带 `state` 的类;`state: ICoreState`)
|
||||
- packages-user/data-state/src/core.ts:77(单一具体类,option B 参考)
|
||||
- AGENTS.md(不得擅自修改既有注释;命名需先获批)与 dev.md(模块原则:无副作用 / 不转发导出 / 无循环引用;CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **`this.state` 消费者扫描(全量,产出结论)**:用 `git grep -n -E "this\.state" -- packages-user/client-modules/src packages-user/client-base/src` 与阅读法,逐一检查消费 `using` / `sceneController` / `mainUIController` 的站点,判定其是否为本仓「暴露 `this.state` 且其类型链(`IClientBase` → `IClientSystem` → `IClientCore`)声明了被挂字段的类」。规划日预期 **0 处**(消费站点均为函数 / Vue 组件;唯一带 `state` 的类在 `client-base/src/material/**` 且 `state: ICoreState` 不含被挂字段)。把逐站点结论(可转换 / 不可转换)写入 SUMMARY。
|
||||
- **若发现可转换站点**:**必须停下**在 Task 0 关卡重报(并更新 `files_modified`)后再改写为 `this.state.<field>`;**不得**擅自扩大改动面。
|
||||
- **若无**:不做任何消费者改动,SUMMARY 记录「0 处可转换,全部保持原样」。
|
||||
|
||||
(2) **F-03 staged 校验**:确认 `ClientCore` 构造器内 `rafExcitation` / `excitationDivider` / `renderer`(`client.ts:90,91,108`)是权威;确认 `render/renderer.ts:18,20,35` 的模块级重复仍在(staged);在 SUMMARY 登记「F-03:权威在 `ClientCore`;模块级重复 staged,删除推迟到消费者迁移步骤」。
|
||||
|
||||
(3) **逐符号站点反查(使用修正后的正则)**:执行
|
||||
`git grep -n -E "new (RafExcitation|ExcitationDivider|MotaRenderer|RendererUsing)[<(]" -- packages-user/client-modules packages-user/client-base`
|
||||
与 `git grep -n -E "new UIController\(" -- packages-user/client-modules packages-user/client-base`
|
||||
并逐符号核对:`RafExcitation` 2(`client.ts` 1 + `render/renderer.ts` 1)、`ExcitationDivider` 2(同上)、`MotaRenderer` 2、`RendererUsing` 2(`client.ts` 1 + `render/renderer.ts` 1)、`UIController` 4(`client.ts` 2 + `render/scene.ts` 1 + `render/ui/controller.tsx` 1);合计 12。**注意 `new ExcitationDivider<number>()` 的泛型 `<` 必须用 `[<(]` 捕获**(早期计划用 `new ExcitationDivider\(` 漏掉了它)。把实测值写入 SUMMARY;若实测与上述不符,按当日实测如实记录并说明差异。
|
||||
|
||||
(4) **staged / alias / 全局访问门禁自查**:无任何 `export const x = client.x` 别名;无任何新增 `import { client }`(本计划未改任何消费者,且未在 `types.ts` / `client.ts` 引入 `client`);`render/renderer.ts` / `render/scene.ts` / `render/ui/controller.tsx` 逐字未动。
|
||||
|
||||
(5) **CRLF 与收口**:本任务只允许编辑 `04-16-SUMMARY.md`;**不得**改任何生产文件、不得新增计划文件、不得派发会写文件的子代理;**不得**执行 `pnpm check:type` / `pnpm build`。
|
||||
</action>
|
||||
<reversibility rating="reversible">本任务只做扫描 / 校验并把结论写入 SUMMARY,无生产代码改动,可用 `git revert` 回退。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');function ggE(p){try{return execFileSync('git',['grep','-n','-E',p,'--','packages-user/client-modules','packages-user/client-base'],{encoding:'utf8'})}catch(e){return ''}}function count(p,sym){const g=ggE(p).replace(/\\r/g,'');return g.split('\\n').filter(l=>l.indexOf(sym)>=0&&l.length>0).length}const exp={RafExcitation:2,ExcitationDivider:2,MotaRenderer:2,RendererUsing:2};const bad=[];for(const k of Object.keys(exp)){const n=count('new ('+k+')[<(]',k);if(n!==exp[k])bad.push(k+' sites='+n+' want '+exp[k])}const uc=ggE('new UIController\\\\(').replace(/\\r/g,'').split('\\n').filter(l=>l.length>0).length;if(uc!==4)bad.push('UIController sites='+uc+' want 4');if(bad.length>0){console.error('SITE RECONCILE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK reconciled sites: RafExcitation=2 ExcitationDivider=2 MotaRenderer=2 RendererUsing=2 UIController=4 (total 12; client.ts authoritative 6 + modules staged 6)');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SITE RECONCILE FAILED"(用修正正则 `new (RafExcitation|ExcitationDivider|MotaRenderer|RendererUsing)[<(]` / `new UIController\(` 逐符号核对时数量与权威 6 + staged 6 的预期不符 —— 注意 `ExcitationDivider<number>` 的 `<`)</fails_when>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');function ggF(p){try{return execFileSync('git',['grep','-n','-F','-e',p,'--','packages-user/client-modules','packages-user/client-base'],{encoding:'utf8'})}catch(e){return ''}}const bad=[];for(const a of ['export const using = client.','export const sceneController = client.','export const mainUIController = client.']){if(ggF(a).trim().length>0)bad.push('FORBIDDEN ALIAS :: '+a)}for(const a of ['export const using = new RendererUsing(mainRenderer)','export const sceneController = new UIController(','export const mainUIController = new UIController(']){if(ggF(a).trim().length<1)bad.push('STAGED SITE MISSING :: '+a)}if(bad.length>0){console.error('STAGED/ALIAS GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK staged sites present, no aliases');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "STAGED/ALIAS GATE FAILED" / "FORBIDDEN ALIAS" / "STAGED SITE MISSING"(引入了转发别名,或误删了模块级 staged 站点 —— 违反 dev.md 不转发导出 / staged 保留)</fails_when>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');const o=execFileSync('git',['status','--porcelain','--','packages-user/client-modules/src/render','packages-user/client-base','packages-user/client-system'],{encoding:'utf8'}).replace(/[ \\t\\r\\n]+$/,'');if(o.length>0){console.error('STAGED/CONSUMER TOUCHED :: '+o);process.exit(1)}console.log('OK render/** + client-base + client-system untouched');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "STAGED/CONSUMER TOUCHED"(改动了 staged 站点 / `client-base` / `client-system` —— 越出本轮范围)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const f='.planning/phases/04-render-adaptation/04-16-SUMMARY.md';const s=fs.readFileSync(f,'utf8');const bad=[];for(const k of ['this.state','F-03','staged','0 处']){if(s.indexOf(k)<0)bad.push('SUMMARY missing :: '+k)}const lf=(s.match(/\\n/g)||[]).length;const crlf=(s.match(/\\r\\n/g)||[]).length;if(lf!==crlf)bad.push('NON-CRLF '+f);if(bad.length>0){console.error('TASK3 RECORD FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK SUMMARY records this.state scan + F-03 staged + CRLF');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "TASK3 RECORD FAILED" / "SUMMARY missing" / "NON-CRLF"(SUMMARY 未记录 `this.state` 扫描结论 / F-03 staged / staged 关键词,或非 CRLF)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `this.state` 扫描完成,逐站点结论写入 SUMMARY(规划日预期 0 处可转换);未擅自改写任何消费者
|
||||
- F-03 结论入 SUMMARY:`ClientCore` 权威、模块级重复 staged
|
||||
- 修正正则后逐符号站点数对账:`RafExcitation` 2 / `ExcitationDivider` 2 / `MotaRenderer` 2 / `RendererUsing` 2 / `UIController` 4(权威 6 + staged 6 = 12)
|
||||
- staged 站点逐字仍在;无 alias;`render/**` / `client-base` / `client-system` porcelain 为空;SUMMARY CRLF
|
||||
</acceptance_criteria>
|
||||
<done>`this.state` 扫描与结论、F-03 staged 结论、修正正则后的逐符号站点对账均完成并写入 SUMMARY;staged 站点保留、无 alias、无新全局访问;门禁(对账 / staged / 范围 / 记录)全绿</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4:收口 —— 全量静态门禁 + 范围 / 用户改动基线 + 既有产物保护 + ROADMAP 登记 + CRLF + 人工复核 + `04-16-SUMMARY.md` 定稿 + 白名单提交</name>
|
||||
<precondition>Task 0 已闭环(用户已回复「可以执行」)且 Task 1 / 2 / 3 均已交付(三个字段挂载 + `this.state` 扫描 + staged/站点对账)。</precondition>
|
||||
<files>.planning/phases/04-render-adaptation/04-16-SUMMARY.md</files>
|
||||
<read_first>
|
||||
- .planning/phases/04-render-adaptation/04-14-SUMMARY.md 与 04-15-SUMMARY.md(SUMMARY 版式:只读基线 / Accomplishments / Task Commits / 门禁实测 / 人工复核 / Decisions Made / Deviations / Known Stubs / Next Phase Readiness)
|
||||
- .planning/phases/04-render-adaptation/04-16-PLAN.md(本计划 must_haves / 源→目标映射 / 各任务门禁)
|
||||
- .planning/ROADMAP.md(Phase 4 的 `**Plans:**` 与 `**Wave 14**` 条目 —— 本步登记 `04-16-PLAN.md` / `**Wave 15**` 的样式基准;本计划不写该文件)
|
||||
- packages-user/client-modules/src/{types.ts,client.ts}(终态)与 `render/{renderer.ts,scene.ts,ui/controller.tsx}`(staged 终态)
|
||||
- AGENTS.md(命名须先获批;不得擅自修改既有注释)与 dev.md(CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **全量静态门禁(先跑,任一失败即停并回到相应任务修正)**:
|
||||
- 内容:`types.ts` 的 `IClientCore` 含 `readonly using: IRendererUsing;` / `readonly sceneController: UIController;` / `readonly mainUIController: UIController;`,且 `extends IClientSystem` 不变、无 `state` / `IClientBaseExtended`;`client.ts` 含三字段 + 构造器三处 `new`,无 `state`。
|
||||
- staged 保留:`render/renderer.ts` 的 `new RafExcitation()` / `new ExcitationDivider<number>()` / `new MotaRenderer(` / `new RendererUsing(mainRenderer)` 仍在;`render/scene.ts` / `render/ui/controller.tsx` 的 `new UIController(` 仍在;`MainBackgroundUI`(`GameUI`)仍在。
|
||||
- 保持项:`texture`(`client-base/src/elements/cache.ts:280`)、`DEFAULT_FONT`(`client-modules/src/shared.ts:105`)、`client`(`core.ts:6` 唯一 `new ClientCore`)仍在且未被挂载 / 改动。
|
||||
- 逐符号站点对账(修正正则):`RafExcitation` 2 / `ExcitationDivider` 2 / `MotaRenderer` 2 / `RendererUsing` 2 / `UIController` 4。
|
||||
- 无 alias;无新 `import { client }`;`import type` 零命中。
|
||||
- 范围:`git status --porcelain -- packages-user/client-base packages-user/client-system packages-user/client-modules/src/render packages packages/ src` 为空;`IClientBase` / `material/**` / `client-system` 零改动。
|
||||
- D-56:`packages-user/data-system/src/combat/context.ts` 仍在场(` M`,未提交)且 `git hash-object` 与 `04-16-SUMMARY.md` 的 `<!-- 04-16-baseline-hash-combat-context: ... -->` 标记一致。
|
||||
- barrel / 需求:`.planning/REQUIREMENTS.md` porcelain 为空;`client-modules/src/index.ts` / `render/index.tsx` / `render/ui/index.ts` / `client-base/src/index.ts` / `client-system/src/index.ts` porcelain 为空(D-69)。
|
||||
|
||||
(2) **既有产物保护门禁**:断言 `.planning/phases/04-render-adaptation/` 下 `04-01`..`04-15` 的 `*-PLAN.md` / `*-SUMMARY.md` porcelain 为空;断言目录内**不存在** `04-17+` 或任何新计划文件(`04-16-PLAN.md` 本身除外)。
|
||||
|
||||
(3) **CRLF 收口**:确认 `types.ts` / `client.ts` 与 `04-16-SUMMARY.md` 为 CRLF。
|
||||
|
||||
(4) **人工代码复核(不可自动化,如实记录结论)**:逐条打开当日源码确认 —— ① 三个字段仅在 `IClientCore` / `ClientCore` 上新增,`IClientBase` / `IClientSystem` 未动;② `this.using = new RendererUsing(this.renderer)` 位于 `this.renderer` 赋值之后;③ `sceneController` / `mainUIController` 的 id 字符串与旧站点一致;④ staged 站点逐字未动、无 alias、无新全局访问;⑤ `texture` / `DEFAULT_FONT` / 全部 `GameUI` 实例未被触碰;⑥ 无 `import type`;⑦ `client-base` 保留的 `@ts-expect-error` 反向耦合逐字未动。发现偏差即回到相应任务修正。把复核结论写入 SUMMARY。**如实声明静态不可证项**:`IClientCore` 类型可满足性、构造器 `new` 的模块求值顺序、staged 并存实例的收敛。
|
||||
|
||||
(5) **写 / 定稿 `04-16-SUMMARY.md`**:按 04-14 / 04-15 版式补全 —— frontmatter(`phase` / `plan: 16` / `tags` / `requires` / `provides` / `affects` / `actuals` / `key-files`(created / modified / deleted,`deleted: []`)/ `key-decisions`(option B、F-01..F-07、`DEFAULT_FONT`、D-23 / D-33 / D-42 / D-48 / D-49 / D-55 / D-56 / D-65 / D-68 / D-69)/ `requirements-completed: []` / `duration` / `completed` / `status`)+ 只读起始基线(原文 + `git hash-object` 标记)+ Accomplishments(三字段挂载 + staged 保留 + `this.state` 扫描 0 处 + 站点对账)+ Task Commits + 门禁实测结果 + 人工复核结论 + Decisions Made + Deviations(如有)+ Known Stubs(staged 并存实例与 `UIController` 同 id 二次构造告警、F-03 重复未删、消费者待迁移、静态不可证项)+ Next Phase Readiness(消费者迁移 / `texture` 后续接口重构 / 双布局 / REND-01 余下部分)。**如实声明本步为代码改动、无运行时验证(D-68 的延续),`REND-01` / `REND-02` 保持 Pending。**
|
||||
|
||||
(6) **白名单提交(单次)**:核对 `git status`,**只**暂存 `files_modified` 的两个生产文件与 `04-16-SUMMARY.md`——**绝不**暂存 `packages-user/data-system/src/combat/context.ts` 或任何其它文件;提交前执行 `git diff --cached --name-only` 逐条核对,确认不含 `combat/context.ts`、不含任何数据端 / `packages/` / `src/` / 测试文件。以 `refactor(04-16): 渲染端去单例化第二步(挂载到主类)` 为提交信息提交。**不得**借提交顺带格式化 / 重排无关文件;**不得** amend 任何既有提交。
|
||||
|
||||
(7) **不写其它 `.planning/` 文件**:本任务除 `04-16-SUMMARY.md` 外**不得**写 `.planning/STATE.md` / `WINDOWS.md` / `REQUIREMENTS.md` / `.planning/ROADMAP.md`(ROADMAP 的 `04-16-PLAN.md` / `Wave 15` 登记由编排器在计划阶段完成)。
|
||||
</action>
|
||||
<reversibility rating="reversible">本任务是收口(门禁 + 文档 + 一次白名单提交),改动内容为两个生产文件的挂载与 SUMMARY,可用 `git revert` 整体回退。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const c=fs.readFileSync('packages-user/client-modules/src/client.ts','utf8');const t=fs.readFileSync('packages-user/client-modules/src/types.ts','utf8');const r=fs.readFileSync('packages-user/client-modules/src/render/renderer.ts','utf8');const s=fs.readFileSync('packages-user/client-modules/src/render/scene.ts','utf8');const u=fs.readFileSync('packages-user/client-modules/src/render/ui/controller.tsx','utf8');const core=fs.readFileSync('packages-user/client-modules/src/core.ts','utf8');const cache=fs.readFileSync('packages-user/client-base/src/elements/cache.ts','utf8');const shared=fs.readFileSync('packages-user/client-modules/src/shared.ts','utf8');const bad=[];for(const k of ['readonly using: IRendererUsing;','readonly sceneController: UIController;','readonly mainUIController: UIController;','this.using = new RendererUsing(this.renderer);',\\\"this.sceneController = new UIController('main-scene');\\\",\\\"this.mainUIController = new UIController('main-ui');\\\"]){if(c.indexOf(k)<0)bad.push('client.ts missing :: '+k)}for(const k of ['readonly using: IRendererUsing;','readonly sceneController: UIController;','readonly mainUIController: UIController;','extends IClientSystem']){if(t.indexOf(k)<0)bad.push('types.ts missing :: '+k)}if(t.indexOf('IClientBaseExtended')>=0||/readonly state\s*:/.test(t))bad.push('state/Extended wrongly in types.ts');if(/readonly state\s*:/.test(c))bad.push('state wrongly in client.ts');for(const k of ['new RafExcitation(','new ExcitationDivider<x>','new MotaRenderer(','new RendererUsing(mainRenderer)']){if(k==='new ExcitationDivider<x>')continue;if(r.indexOf(k)<0)bad.push('staged renderer.ts missing :: '+k)}if(r.indexOf('new ExcitationDivider<number>(_')>=0){}if(r.indexOf('new ExcitationDivider')<0)bad.push('staged ExcitationDivider missing');if(s.indexOf(\\\"new UIController('main-scene')\\\")<0||u.indexOf(\\\"new UIController('main-ui')\\\")<0)bad.push('staged UIController missing');if(u.indexOf(\\\"new GameUI('main-background'\\\")<0)bad.push('MainBackgroundUI missing');if(cache.indexOf('export const texture = new TextureCache()')<0)bad.push('texture must stay');if(shared.indexOf('export const DEFAULT_FONT = new Font(')<0)bad.push('DEFAULT_FONT must stay');if((core.match(/new ClientCore\\(/g)||[]).length!==1)bad.push('ClientCore count != 1');if(bad.length>0){console.error('FINAL CONTENT FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK final content: 3 fields mounted + staged sites + kept items, no state');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "FINAL CONTENT FAILED"(三字段 / 构造器赋值缺失、误加 `state` / `IClientBaseExtended`、staged 站点或 `MainBackgroundUI` / `texture` / `DEFAULT_FONT` / 唯一 `ClientCore` 被破坏)</fails_when>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');function ggE(p){try{return execFileSync('git',['grep','-n','-E',p,'--','packages-user/client-modules','packages-user/client-base'],{encoding:'utf8'})}catch(e){return ''}}function count(p,sym){return ggE(p).replace(/\\r/g,'').split('\\n').filter(l=>l.indexOf(sym)>=0&&l.length>0).length}const exp={RafExcitation:2,ExcitationDivider:2,MotaRenderer:2,RendererUsing:2};const bad=[];for(const k of Object.keys(exp)){const n=count('new ('+k+')[<(]',k);if(n!==exp[k])bad.push(k+'='+n+' want '+exp[k])}const uc=ggE('new UIController\\\\(').replace(/\\r/g,'').split('\\n').filter(l=>l.length>0).length;if(uc!==4)bad.push('UIController='+uc+' want 4');if(bad.length>0){console.error('SITE RECONCILE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK reconciled: 4 cluster symbols x2 + UIController x4 (authoritative 6 + staged 6 = 12)');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SITE RECONCILE FAILED"(修正正则后逐符号数量与「权威 6 + staged 6」不符;`ExcitationDivider<number>` 须由 `[<(]` 捕获)</fails_when>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');const fs=require('fs');const S='.planning/phases/04-render-adaptation/04-16-SUMMARY.md';const FILE='packages-user/data-system/src/combat/context.ts';const WANT=' M '+FILE;const bad=[];const mat=execFileSync('git',['status','--porcelain','--','packages-user/client-base','packages-user/client-system'],{encoding:'utf8'}).replace(/[ \\t\\r\\n]+$/,'');if(mat.length>0)bad.push('CLIENT-BASE/SYSTEM TOUCHED :: '+mat);const pkg=execFileSync('git',['status','--porcelain','--','packages','packages/','src'],{encoding:'utf8'}).replace(/[ \\t\\r\\n]+$/,'');if(pkg.length>0)bad.push('FORBIDDEN PATH TOUCHED :: '+pkg);const req=execFileSync('git',['status','--porcelain','--','.planning/REQUIREMENTS.md'],{encoding:'utf8'}).replace(/[ \\t\\r\\n]+$/,'');if(req.length>0)bad.push('REQUIREMENTS.md TOUCHED');const bar=execFileSync('git',['status','--porcelain','--','packages-user/client-modules/src/index.ts','packages-user/client-modules/src/render/index.tsx','packages-user/client-modules/src/render/ui/index.ts','packages-user/client-base/src/index.ts','packages-user/client-system/src/index.ts'],{encoding:'utf8'}).replace(/[ \\t\\r\\n]+$/,'');if(bar.length>0)bad.push('BARREL TOUCHED :: '+bar);const o=execFileSync('git',['status','--porcelain','--','packages-user/data-system'],{encoding:'utf8'});const lines=o.replace(/\\r/g,'').split('\\n').map(l=>l.replace(/[ \\t]+$/,'')).filter(l=>l.length>0);const t=fs.readFileSync(S,'utf8');const m=t.match(/<!-- 04-16-baseline-hash-combat-context: ([0-9a-f]{40}) -->/);if(!m)bad.push('BASELINE HASH MARKER MISSING');if(lines.indexOf(WANT)<0)bad.push('USER EDIT MISSING');if(m){const cur=execFileSync('git',['hash-object','--',FILE],{encoding:'utf8'}).trim();if(cur!==m[1])bad.push('USER EDIT HASH DRIFT '+cur+' vs '+m[1]);}if(bad.length>0){console.error('SCOPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK scope: client-base/client-system/packages/src clean; REQUIREMENTS+barrels untouched; user edit present + hash matches');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SCOPE GATE FAILED" / "CLIENT-BASE/SYSTEM TOUCHED" / "FORBIDDEN PATH TOUCHED" / "REQUIREMENTS.md TOUCHED" / "BARREL TOUCHED" / "BASELINE HASH MARKER MISSING" / "USER EDIT MISSING" / "USER EDIT HASH DRIFT"(`client-base` / `client-system` / `packages/` / `src/` 被改、`REQUIREMENTS.md` 或 barrel 被改、用户 `combat/context.ts` 被回滚 / 再编辑、SUMMARY 缺标记 —— 违反用户「忽略 client-system」/ D-33 / D-42 / D-48 / D-56 / D-69)</fails_when>
|
||||
<automated>node -e "const{execFileSync}=require('child_process');const o=execFileSync('git',['status','--porcelain','--','.planning/phases/04-render-adaptation'],{encoding:'utf8'}).replace(/\\s+$/,'');const rows=o.length?o.split(/\\r?\\n/):[];const existing=rows.filter(r=>/04-(0[1-9]|1[0-5])-(PLAN|SUMMARY)\\.md$/.test(r));const newPlan=rows.filter(r=>/04-(1[7-9]|[2-9][0-9])-/.test(r));const bad=[];if(existing.length>0)bad.push('EXISTING ARTIFACT DIRTY :: '+existing.join(' ; '));if(newPlan.length>0)bad.push('NEW PLAN FILE :: '+newPlan.join(' ; '));if(bad.length>0){console.error('ARTIFACT PROTECTION FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK existing 04-01..04-15 artifacts untouched; no 04-17+ plan');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "ARTIFACT PROTECTION FAILED" / "EXISTING ARTIFACT DIRTY" / "NEW PLAN FILE"(改写 / 覆盖了既有 `04-01`..`04-15` 产物,或产出了 `04-17+` 计划文件)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const t=fs.readFileSync('.planning/ROADMAP.md','utf8');const bad=[];if(t.indexOf('04-16-PLAN.md')<0)bad.push('ROADMAP 04-16-PLAN.md MISSING');if(t.indexOf('Wave 15')<0)bad.push('ROADMAP Wave 15 MISSING');if(bad.length>0){console.error('ROADMAP REGISTRATION FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK ROADMAP has 04-16-PLAN.md / Wave 15 registration');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "ROADMAP REGISTRATION FAILED" / "ROADMAP 04-16-PLAN.md MISSING" / "ROADMAP Wave 15 MISSING"(编排器尚未在 `.planning/ROADMAP.md` 的 Phase 4 段登记真实条目 `04-16-PLAN.md` 与 `**Wave 15**`;仅裸 `04-16` 子串不算,因 04-15 正文亦含之)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const files=['packages-user/client-modules/src/types.ts','packages-user/client-modules/src/client.ts','.planning/phases/04-render-adaptation/04-16-SUMMARY.md'];const bad=[];const need=['04-16','去单例化','staged','Pending','无运行时','using','sceneController','mainUIController'];for(const f of files){if(!fs.existsSync(f)){bad.push('MISSING '+f);continue}const s=fs.readFileSync(f,'utf8');const lf=(s.match(/\\n/g)||[]).length;const crlf=(s.match(/\\r\\n/g)||[]).length;if(lf!==crlf)bad.push('NON-CRLF '+f)}const sum=fs.readFileSync('.planning/phases/04-render-adaptation/04-16-SUMMARY.md','utf8');for(const k of need){if(sum.indexOf(k)<0)bad.push('SUMMARY MISSING :: '+k)}const c=fs.readFileSync('packages-user/client-modules/src/client.ts','utf8');if(c.indexOf('import type')>=0){bad.push('import type in client.ts')}if(bad.length>0){console.error('CLOSURE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK closure: files + SUMMARY present, CRLF, keyed, no import type');"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "CLOSURE FAILED" / "MISSING" / "NON-CRLF" / "SUMMARY MISSING" / "import type"(生产文件或 SUMMARY 缺失 / 非 CRLF、SUMMARY 缺关键标识、或出现了 `import type` —— 违反 dev.md)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 全量内容门禁:三字段 + 构造器三处 `new` 齐备;`extends IClientSystem` 不变;无 `state` / `IClientBaseExtended`;staged 站点与 `texture` / `DEFAULT_FONT` / `MainBackgroundUI` / 唯一 `ClientCore` 保持
|
||||
- 逐符号站点对账(修正正则)通过:`RafExcitation` / `ExcitationDivider` / `MotaRenderer` / `RendererUsing` 各 2,`UIController` 4(权威 6 + staged 6 = 12)
|
||||
- 范围门禁:`client-base` / `client-system` / `packages/` / `src/` porcelain 为空;barrel 与 `.planning/REQUIREMENTS.md` 零改动
|
||||
- D-56 门禁:用户 `combat/context.ts` 仍在场(` M`)且 `git hash-object` 与 SUMMARY 标记一致
|
||||
- 既有产物保护:`04-01`..`04-15` porcelain 为空;无 `04-17+`
|
||||
- ROADMAP 登记:Phase 4 段含 `04-16-PLAN.md` 与 `**Wave 15**`
|
||||
- CRLF:两个生产文件与 `04-16-SUMMARY.md` 均为 CRLF;无 `import type`
|
||||
- `04-16-SUMMARY.md` 已定稿(含只读基线 / 新增命名 / 源→目标映射含 staged / `this.state` 扫描结论 / 门禁实测 / 人工复核 / 无运行时验证声明 / Known Stubs / Next Phase Readiness)
|
||||
- 已按白名单单次提交(只含两个生产文件 + SUMMARY);`git diff --cached --name-only` 不含 `combat/context.ts` 或任何范围外文件
|
||||
</acceptance_criteria>
|
||||
<done>收口完成:全量静态门禁 / 逐符号站点对账 / staged 保留 / 范围(含忽略 `client-system`)/ D-56 用户改动基线 / 既有产物保护 / ROADMAP `04-16-PLAN.md` + `Wave 15` / CRLF 全绿;人工复核完成并记录(含静态不可证项);`04-16-SUMMARY.md` 定稿;白名单单次提交完成(未含用户 `combat/context.ts`);`REND-01` / `REND-02` 保持 Pending</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| (无新增外部边界) | 本计划是纯代码改动(只改两个渲染端生产文件),不引入外部输入 / 网络 / 进程边界;不新增依赖、不安装包、不执行 `pnpm i` |
|
||||
| 仓库工作树 → git 提交 | 用户的并发未提交改动(`packages-user/data-system/src/combat/context.ts`)与本次改动共处同一工作树;若暂存面把控不当,会把用户的改动混入本计划提交 |
|
||||
| staged 并存 → 运行时装配 | 模块级 staged 实例与 `ClientCore` 权威实例并存(`UIController` 同 id 二次构造告警);静态门禁无法证明运行时收敛 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-04-16-01 | Tampering | 范围外生产树(`client-base` / `client-system` / 数据端 / `packages/` / `src/`) | high | mitigate | Task 2 / 3 / 4 的 `<verify>` 断言对应 porcelain 为空;prohibitions 明列「忽略 `client-system`」与 D-33 / D-42 / D-48 |
|
||||
| T-04-16-02 | Tampering | 提交暂存面(`git add`) | high | mitigate | Task 4 步骤 (6) 明示只暂存两个生产文件 + SUMMARY;Task 1 / 4 断言用户 `combat/context.ts` 仍在且哈希一致、`git diff --cached --name-only` 逐条核对;prohibitions 明列 |
|
||||
| T-04-16-03 | Tampering | 用户并发改动(D-56) | high | mitigate | Task 1 (0a) 基线 + `git hash-object` 标记;Task 1 / 4 的 D-56 门禁断言用户改动仍在且哈希一致、未暂存 |
|
||||
| T-04-16-04 | Tampering | staged 站点被误删 / 消费者被误改 | high | mitigate | Task 1 / 2 / 3 的「staged 站点保留 / 消费者零改动」门禁;prohibitions 明列不得删模块级站点、不得改写非 `this.state` 消费者 |
|
||||
| T-04-16-05 | Elevation of Privilege | 未获批命名 / 越过 option B 设计(加 `state`、改 `IClientBase` / `client-system`) | high | mitigate | Task 0 列出全部新增命名与设计并等待「可以执行」;Task 1 / 2 / 4 断言无 `state` / `IClientBaseExtended`、`extends IClientSystem` 不变、`client-base` / `client-system` porcelain 为空 |
|
||||
| T-04-16-06 | Spoofing | 转发别名 / 新全局 `client` 访问(dev.md 不转发导出 / D-23) | high | mitigate | Task 1 / 2 / 3 的 alias 门禁(`export const x = client.` 零命中);本计划不引入 `import { client }`,消费者零改动 |
|
||||
| T-04-16-07 | Repudiation | 站点计数错误 / 泛型漏计(`ExcitationDivider<number>`) | medium | mitigate | Task 3 / 4 使用修正正则 `new (RafExcitation|ExcitationDivider|MotaRenderer|RendererUsing)[<(]` 逐符号对账(权威 6 + staged 6 = 12) |
|
||||
| T-04-16-08 | Repudiation | 「无运行时」被用来弱化验证 | low | mitigate | 每条 `<automated>` 必带 `<fails_when>`;D-68 明示禁止 `check:type` / `build` / TS 诊断数类门禁;Task 4 步骤 (4) 如实声明静态不可证项;prohibitions 明列不得伪造绿 |
|
||||
| T-04-16-SC | Tampering | npm/pip/cargo 安装 | low | accept | 本计划无任何依赖新增 / 变更、不执行 `pnpm i`、不触及任何 `package.json`,故无供应链安装面;`T-04-16-SC` 保留为「本 run 不适用」的显式登记 |
|
||||
</threat_model>
|
||||
|
||||
## Verification Runnability(PowerShell 5.1 可运行性)
|
||||
|
||||
**执行者必读。** 仓库开发环境为 Windows / PowerShell 5.1。把本计划 `<automated>` 里的内联 `node -e "..."` 命令原样粘贴到 PowerShell 5.1 时,其中的内嵌引号、`$`(如正则尾锚 `$`)、反斜杠、CJK 字符与 `→` 可能被 shell 转义 / 编码破坏,使命令被 mangle 而非按其语义失败。**执行者必须把每条门禁的原样语义写入仓库外的临时 `.cjs` 文件(例如 `%TEMP%\opencode\04-16-<gate>.cjs`)并运行 `node <该文件>`**,从而绕开 PowerShell 的引号 / `$` / 编码处理;**判定语义与对应的 `<fails_when>` 一字不变**——不得为 shell 兼容而删减断言、放宽条件或改变失败判据。本计划的站点反查门禁统一改用 `execFileSync('git', [...])`(避免 shell 引号),临时 `.cjs` 方案与之等价。该做法与 04-12 / 04-14 / 04-15 SUMMARY 记录的先例一致。
|
||||
|
||||
<verification>
|
||||
**Static-only(D-68 的延续:不设 `check:type` / `build` / TS 诊断数类门禁)**
|
||||
|
||||
逐任务门禁汇总:
|
||||
|
||||
1. **内容门禁(Task 1 / 2 / 4)** —— `IClientCore` / `ClientCore` 含三个新字段与构造器三处 `new`;`extends IClientSystem` 不变;无 `state` / `IClientBaseExtended`。
|
||||
2. **staged 保留门禁(Task 1 / 2 / 3 / 4)** —— `render/renderer.ts:18,20,35,43`、`render/scene.ts:3`、`render/ui/controller.tsx:11` 的模块级站点逐字仍在;`MainBackgroundUI`(`GameUI`)仍在。
|
||||
3. **逐符号站点对账(Task 3 / 4)** —— 修正正则 `new (RafExcitation|ExcitationDivider|MotaRenderer|RendererUsing)[<(]` 与 `new UIController\(`:`RafExcitation` 2 / `ExcitationDivider` 2 / `MotaRenderer` 2 / `RendererUsing` 2 / `UIController` 4(权威 6 + staged 6 = 12)。**注意 `new ExcitationDivider<number>()` 的泛型 `<` 必须由 `[<(]` 捕获**(早期计划用 `new ExcitationDivider\(` 漏计,且笼统写作 5 处——此处按符号逐项修正并明确为 12)。
|
||||
4. **alias / 全局访问门禁(Task 1 / 2 / 3)** —— `export const x = client.` 零命中;无新 `import { client }`(本计划不改任何消费者)。
|
||||
5. **范围门禁(Task 2 / 3 / 4)** —— `git status --porcelain -- packages-user/client-base packages-user/client-system packages-user/client-modules/src/render packages packages/ src` 为空;barrel 与 `.planning/REQUIREMENTS.md` 零改动(D-69)。
|
||||
6. **D-56 用户改动基线门禁(Task 1 / 4)** —— `packages-user/data-system/src/combat/context.ts` 仍在场(` M`)且 `git hash-object` 与 SUMMARY 标记一致。
|
||||
7. **既有产物保护(Task 4)** —— `04-01`..`04-15` 的 `*-PLAN.md` / `*-SUMMARY.md` porcelain 为空;无 `04-17+`。
|
||||
8. **ROADMAP 登记(Task 4)** —— Phase 4 段含真实条目 `04-16-PLAN.md` 与 `**Wave 15**`(仅裸 `04-16` 子串不算,因 04-15 正文亦含之)。
|
||||
9. **CRLF(Task 1 / 2 / 4)** —— 两个生产文件与 `04-16-SUMMARY.md` 均为 CRLF;无 `import type`。
|
||||
10. **人工复核(Task 0 / 4)** —— Task 0 的「可以执行」回复原文;Task 4 逐条代码复核与结论记录。
|
||||
|
||||
**明确禁止的门禁(D-68):** 不得以「`pnpm check:type` 通过」「`pnpm build` 成功」「全仓 / 范围内 TS 诊断数归零 / 下降」作为门禁或验收项;不得因这类检查失败而阻塞交付。
|
||||
|
||||
**静态不可证项(如实声明):** ① `IClientCore` 新增两个 `UIController` 字段与一个 `IRendererUsing` 字段在类型层面的可满足性(含 `client.ts` 的外部类 import 可解析性)无法由静态文本门禁证明;② `ClientCore` 构造器新增 `new` 的 ES 模块求值顺序无法证明;③ staged 并存实例(模块级 + `ClientCore` 权威)的运行时收敛、以及 `UIController` 同 id 二次构造的告警不影响功能,无法证明;④ 无循环引用仅由人工阅读确认。以上均以人工复核 + 「无运行时验证」声明覆盖,不得伪造绿。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Task 0 汇报关卡已闭环:用户明确回复「可以执行」四个字;汇报涵盖 option B 设计、F-01..F-07 + `DEFAULT_FONT`、**全部新增命名(`using` / `sceneController` / `mainUIController`,明确无 `state`)与类型**、源→目标映射(含 staged 保留项)、消费者改写规则、staged 并存后果、范围与边界、验证方式、关卡性质与失败语义
|
||||
- [ ] `IClientCore`(`extends IClientSystem` 不变)新增 `readonly using: IRendererUsing;` / `readonly sceneController: UIController;` / `readonly mainUIController: UIController;`;**无** `state` 字段、**无** `IClientBaseExtended`
|
||||
- [ ] `ClientCore` 新增三字段,并在构造器内构造(`using` 在 `this.renderer` 之后;两个 `UIController` 用既有 id `'main-scene'` / `'main-ui'`)
|
||||
- [ ] `IClientBase` / `client-system` / `material/**` / `render/**` 模块级站点 / 全部消费者零改动(staged)
|
||||
- [ ] F-03:`ClientCore` 实例权威;模块级重复 staged 未删(结论入 SUMMARY)
|
||||
- [ ] `this.state` 扫描完成,逐站点结论入 SUMMARY(规划日 0 处可转换;若有则先停关卡重报)
|
||||
- [ ] F-01 / F-04 / F-07 / `DEFAULT_FONT` 保持项:`texture` / `DEFAULT_FONT` / 全部 `GameUI` 实例(含 `MainBackgroundUI`)与 `client` 未被触碰、未被挂载
|
||||
- [ ] 逐符号站点对账(修正正则,含 `ExcitationDivider<`)通过:4 类渲染簇单例各 2、`UIController` 4(权威 6 + staged 6 = 12)
|
||||
- [ ] 范围门禁通过:`client-base` / `client-system` / `packages/` / `src/` porcelain 为空;barrel 与 `.planning/REQUIREMENTS.md` 零改动;无 alias、无新 `import { client }`
|
||||
- [ ] D-56 用户改动基线门禁通过:`combat/context.ts` 仍在场(` M`)且哈希与 SUMMARY 标记一致、未被暂存 / 提交
|
||||
- [ ] 既有 `04-01`..`04-15` 产物零触碰;无 `04-17+` 计划文件
|
||||
- [ ] ROADMAP 的 Phase 4 段含 `04-16-PLAN.md` 与 `**Wave 15**`
|
||||
- [ ] 两个生产文件与 `04-16-SUMMARY.md` 均为 CRLF;SUMMARY 含只读基线 / 新增命名 / 源→目标映射含 staged / `this.state` 扫描结论 / 门禁实测 / 人工复核 / 无运行时验证声明 / Known Stubs / Next Phase Readiness
|
||||
- [ ] 白名单单次提交完成;`git diff --cached --name-only` 不含 `combat/context.ts` 或任何范围外文件
|
||||
- [ ] 全部门禁为静态;未使用 `pnpm check:type` / `pnpm build` / TS 诊断数作为门禁;`REND-01` / `REND-02` 保持 Pending
|
||||
</success_criteria>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
| Artifact | Kind | Purpose |
|
||||
|---|---|---|
|
||||
| `packages-user/client-modules/src/types.ts` | 修改(生产) | `IClientCore` 新增 `using` / `sceneController` / `mainUIController` 三个只读字段;`extends IClientSystem` 不变;无 `state` |
|
||||
| `packages-user/client-modules/src/client.ts` | 修改(生产) | `ClientCore` 新增三个只读字段并在构造器内构造(F-02);既有 `rafExcitation` / `excitationDivider` / `renderer` 保持权威(F-03) |
|
||||
| `.planning/phases/04-render-adaptation/04-16-SUMMARY.md` | 新增(文档) | 执行记录:只读 / 用户改动基线 + 新增命名 + 源→目标映射(含 staged)+ `this.state` 扫描结论 + 门禁实测 + 人工复核 + 无运行时验证声明 + Known Stubs |
|
||||
| `.planning/ROADMAP.md`(`04-16-PLAN.md` + `Wave 15` register note) | 修改(编排器所有) | 计划阶段登记;本计划写入**不修改**该文件(登记由编排器完成,Task 4 以可验证后置条件断言其已就位) |
|
||||
| 未改动(显式保留 / staged) | — | `packages-user/client-modules/src/render/**`(模块级站点 staged 保留)、`packages-user/client-base/**`(含 `IClientBase` / `material/**`)、`packages-user/client-system/**`、`packages-user/data-*/src/**`(含用户未提交 `data-system/src/combat/context.ts`)、`packages/**`、`src/**`、`.planning/REQUIREMENTS.md` |
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/04-render-adaptation/04-16-SUMMARY.md` when done
|
||||
</output>
|
||||
@ -75,6 +75,18 @@ export class TileStore implements ITileStore {
|
||||
}
|
||||
}
|
||||
|
||||
identity(token: number | string): [number, string] | [] {
|
||||
if (typeof token === 'number') {
|
||||
const id = this.numMap.get(token);
|
||||
if (isNil(id)) return [];
|
||||
else return [token, id];
|
||||
} else {
|
||||
const num = this.idMap.get(token);
|
||||
if (isNil(num)) return [];
|
||||
else return [num, token];
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除一组旧的图块定义及其双向索引 */
|
||||
private deleteBy(num: number, id: string): void {
|
||||
this.dataMap.delete(num);
|
||||
|
||||
@ -2,6 +2,7 @@ import { IRange, ITileLocator, logger } from '@motajs/common';
|
||||
import {
|
||||
IAuraConverter,
|
||||
IAuraView,
|
||||
IComputingEnemyView,
|
||||
IDamageSystem,
|
||||
IEnemyAuraView,
|
||||
IEnemyCommonQueryEffect,
|
||||
@ -23,13 +24,15 @@ import {
|
||||
} from '@user/data-base';
|
||||
import { EnemyView } from './enemy';
|
||||
import { ILocationIndexer, MapLocIndexer } from '@user/data-common';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
TEnemy,
|
||||
THero
|
||||
> {
|
||||
/** 坐标索引 -> 怪物视图 */
|
||||
private readonly enemyViewMap: Map<number, EnemyView<TEnemy>> = new Map();
|
||||
private readonly enemyViewMap: Map<number, IComputingEnemyView<TEnemy>> =
|
||||
new Map();
|
||||
/** 坐标索引 -> 计算前怪物对象 */
|
||||
private readonly enemyMap: Map<number, IEnemy<TEnemy>> = new Map();
|
||||
/** 怪物视图 -> 坐标索引 */
|
||||
@ -40,7 +43,7 @@ export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
/** 计算后怪物对象 -> 怪物视图 */
|
||||
private readonly computedToView: Map<
|
||||
IReadonlyEnemy<TEnemy>,
|
||||
EnemyView<TEnemy>
|
||||
IComputingEnemyView<TEnemy>
|
||||
> = new Map();
|
||||
|
||||
/** 当前已注册的光环转换器 */
|
||||
@ -203,29 +206,45 @@ export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
return this.bindedHero;
|
||||
}
|
||||
|
||||
attachDamageSystem(system: IDamageSystem<TEnemy, THero> | null): void {
|
||||
this.damageSystem = system;
|
||||
if (system) {
|
||||
system.bindHeroStatus(this.bindedHero);
|
||||
}
|
||||
}
|
||||
|
||||
getDamageSystem(): IDamageSystem<TEnemy, THero> | null {
|
||||
return this.damageSystem;
|
||||
}
|
||||
|
||||
attachMapDamage(damage: IMapDamage<TEnemy, THero> | null): void {
|
||||
this.mapDamage = damage;
|
||||
if (damage) {
|
||||
damage.refreshAll();
|
||||
}
|
||||
}
|
||||
|
||||
getMapDamage(): IMapDamage<TEnemy, THero> | null {
|
||||
return this.mapDamage;
|
||||
}
|
||||
|
||||
addAura(aura: IAuraView<TEnemy>): void {
|
||||
this.globalAuraList.add(aura);
|
||||
this.needUpdate = true;
|
||||
}
|
||||
|
||||
deleteAura(aura: IAuraView<TEnemy>): void {
|
||||
this.globalAuraList.delete(aura);
|
||||
this.needUpdate = true;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* 创建可修改信息对象
|
||||
* @param enemy 怪物对象
|
||||
* @param locator 怪物位置
|
||||
*/
|
||||
private createHandler(
|
||||
enemy: IEnemy<TEnemy>,
|
||||
locator: ITileLocator
|
||||
): IEnemyHandler<TEnemy, THero> {
|
||||
return {
|
||||
enemy,
|
||||
context: this,
|
||||
locator,
|
||||
hero: this.bindedHero!,
|
||||
state: this.state
|
||||
};
|
||||
}
|
||||
//#region 怪物操作
|
||||
|
||||
getEnemyLocator(enemy: IEnemy<TEnemy>): Readonly<ITileLocator> | null {
|
||||
const index = this.locatorEnemyMap.get(enemy);
|
||||
if (index === undefined) return null;
|
||||
if (isNil(index)) return null;
|
||||
return this.indexer.locator(index);
|
||||
}
|
||||
|
||||
@ -233,7 +252,7 @@ export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
view: IEnemyView<TEnemy>
|
||||
): Readonly<ITileLocator> | null {
|
||||
const index = this.locatorViewMap.get(view);
|
||||
if (index === undefined) return null;
|
||||
if (isNil(index)) return null;
|
||||
return this.indexer.locator(index);
|
||||
}
|
||||
|
||||
@ -315,17 +334,15 @@ export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
private *internalScanRange<T>(
|
||||
range: IRange<T>,
|
||||
param: T
|
||||
): Iterable<[ITileLocator, EnemyView<TEnemy>]> {
|
||||
): Iterable<[ITileLocator, IComputingEnemyView<TEnemy>]> {
|
||||
range.bindHost(this);
|
||||
const keys = new Set(this.enemyViewMap.keys());
|
||||
const matched = range.autoDetect(keys, param);
|
||||
const viewMap = this.enemyViewMap;
|
||||
for (const index of matched) {
|
||||
const view = viewMap.get(index);
|
||||
if (view) {
|
||||
const locator = this.indexer.locator(index);
|
||||
yield [locator, view];
|
||||
}
|
||||
const view = viewMap.get(index)!;
|
||||
const locator = this.indexer.locator(index);
|
||||
yield [locator, view];
|
||||
}
|
||||
}
|
||||
|
||||
@ -343,36 +360,26 @@ export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
}
|
||||
}
|
||||
|
||||
addAura(aura: IAuraView<TEnemy>): void {
|
||||
this.globalAuraList.add(aura);
|
||||
this.needUpdate = true;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
deleteAura(aura: IAuraView<TEnemy>): void {
|
||||
this.globalAuraList.delete(aura);
|
||||
this.needUpdate = true;
|
||||
}
|
||||
//#region 上下文构建
|
||||
|
||||
attachMapDamage(damage: IMapDamage<TEnemy, THero> | null): void {
|
||||
this.mapDamage = damage;
|
||||
if (damage) {
|
||||
damage.refreshAll();
|
||||
}
|
||||
}
|
||||
|
||||
getMapDamage(): IMapDamage<TEnemy, THero> | null {
|
||||
return this.mapDamage;
|
||||
}
|
||||
|
||||
attachDamageSystem(system: IDamageSystem<TEnemy, THero> | null): void {
|
||||
this.damageSystem = system;
|
||||
if (system) {
|
||||
system.bindHeroStatus(this.bindedHero);
|
||||
}
|
||||
}
|
||||
|
||||
getDamageSystem(): IDamageSystem<TEnemy, THero> | null {
|
||||
return this.damageSystem;
|
||||
/**
|
||||
* 创建可修改信息对象
|
||||
* @param enemy 怪物对象
|
||||
* @param locator 怪物位置
|
||||
*/
|
||||
private createHandler(
|
||||
enemy: IEnemy<TEnemy>,
|
||||
locator: ITileLocator
|
||||
): IEnemyHandler<TEnemy, THero> {
|
||||
return {
|
||||
enemy,
|
||||
context: this,
|
||||
locator,
|
||||
hero: this.bindedHero!,
|
||||
state: this.state
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -389,6 +396,7 @@ export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
for (const converter of this.auraConverter) {
|
||||
if (!this.converterStatus.get(converter)) continue;
|
||||
if (converter.shouldConvert(special, handler)) {
|
||||
// 一个特殊属性只能对应一个转换器
|
||||
if (matched) {
|
||||
logger.warn(97, special.code.toString());
|
||||
return null;
|
||||
@ -566,10 +574,12 @@ export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
* 构建所有由特殊属性衍生出的光环与特殊查询结果
|
||||
*/
|
||||
private buildupSpecials(): void {
|
||||
// 首先把全局光环(无来源光环)加到排序列表中
|
||||
for (const aura of this.globalAuraList) {
|
||||
this.insertIntoSortedAura(aura);
|
||||
}
|
||||
|
||||
// 进行第一轮的特殊属性转换,生成可能的光环视图
|
||||
for (const [index, view] of this.enemyViewMap) {
|
||||
const enemy = view.getComputingEnemy();
|
||||
const locator = this.indexer.locator(index);
|
||||
@ -583,27 +593,31 @@ export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
}
|
||||
}
|
||||
|
||||
// 其实大多数情况下并不存在光环,直接返回
|
||||
if (this.sortedAura.size === 0) return;
|
||||
|
||||
const processedPriorities = new Set<number>();
|
||||
|
||||
// 由于期间可能会产生新优先级的光环,所以要用 while (true) 而不是直接遍历
|
||||
// 直接使用 forEach 的话,会无法遍历到临时新增的那些优先级
|
||||
while (true) {
|
||||
let maxPriority: number | null = null;
|
||||
let maxPriority: number | undefined = undefined;
|
||||
for (const priority of this.sortedAura.keys()) {
|
||||
if (!processedPriorities.has(priority)) {
|
||||
if (maxPriority === null || priority > maxPriority) {
|
||||
if (isNil(maxPriority) || priority > maxPriority) {
|
||||
maxPriority = priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const priority of this.specialQueryEffects.keys()) {
|
||||
if (!processedPriorities.has(priority)) {
|
||||
if (maxPriority === null || priority > maxPriority) {
|
||||
if (isNil(maxPriority) || priority > maxPriority) {
|
||||
maxPriority = priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maxPriority === null) break;
|
||||
if (isNil(maxPriority)) break;
|
||||
processedPriorities.add(maxPriority);
|
||||
|
||||
const auras = this.sortedAura.get(maxPriority);
|
||||
@ -723,6 +737,10 @@ export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 局部刷新
|
||||
|
||||
markDirty(view: IEnemyView<TEnemy>): void {
|
||||
if (!this.locatorViewMap.has(view)) return;
|
||||
this.dirtyEnemy.add(view);
|
||||
@ -898,6 +916,8 @@ export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
clear(): void {
|
||||
this.enemyViewMap.clear();
|
||||
this.enemyMap.clear();
|
||||
|
||||
@ -19,6 +19,8 @@ import {
|
||||
IStateBase
|
||||
} from '@user/data-base';
|
||||
|
||||
// TODO: 与不在地图上的怪物战斗
|
||||
|
||||
interface ICriticalSearchResult {
|
||||
/** 此临界点的属性值 */
|
||||
readonly value: number;
|
||||
@ -131,6 +133,54 @@ export class DamageContext<TEnemy, THero> implements IDamageContext<
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算下一个临界点
|
||||
* @param handler 信息对象,其中的 `hero` 成员与 `hero` 参数同引用
|
||||
* @param hero 可修改勇士属性对象,与 `handler` 中的 `hero` 成员同引用
|
||||
* @param attribute 勇士属性名
|
||||
* @param currentValue 当前勇士属性值
|
||||
* @param upperLimit 二分上界
|
||||
* @param referenceDamage 参考伤害值
|
||||
* @param maxIterations 最大迭代数量
|
||||
*/
|
||||
private findNextCritical(
|
||||
handler: IReadonlyEnemyHandler<TEnemy, THero>,
|
||||
hero: IHeroAttribute<THero>,
|
||||
attribute: CriticalableHeroStatus<THero>,
|
||||
currentValue: number,
|
||||
upperLimit: number,
|
||||
referenceDamage: number,
|
||||
maxIterations: number
|
||||
): ICriticalSearchResult | null {
|
||||
let left = currentValue;
|
||||
let right = upperLimit;
|
||||
|
||||
hero.set(attribute, right as THero[typeof attribute]);
|
||||
|
||||
let targetInfo = this.calculator!.calculate(handler);
|
||||
if (targetInfo.damage >= referenceDamage) return null;
|
||||
|
||||
let iter = 0;
|
||||
while (iter++ < maxIterations) {
|
||||
const middle = Math.floor((left + right) / 2);
|
||||
hero.set(attribute, middle as THero[typeof attribute]);
|
||||
const middleInfo = this.calculator!.calculate(handler);
|
||||
|
||||
if (middleInfo.damage < referenceDamage) {
|
||||
right = middle;
|
||||
targetInfo = middleInfo;
|
||||
} else {
|
||||
left = middle;
|
||||
}
|
||||
if (right - left <= 1) break;
|
||||
}
|
||||
|
||||
return {
|
||||
value: right,
|
||||
info: targetInfo
|
||||
};
|
||||
}
|
||||
|
||||
*calculateCritical(
|
||||
view: IEnemyView<TEnemy>,
|
||||
attribute: CriticalableHeroStatus<THero>,
|
||||
@ -192,54 +242,6 @@ export class DamageContext<TEnemy, THero> implements IDamageContext<
|
||||
baseInfo = next.info;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算下一个临界点
|
||||
* @param handler 信息对象,其中的 `hero` 成员与 `hero` 参数同引用
|
||||
* @param hero 可修改勇士属性对象,与 `handler` 中的 `hero` 成员同引用
|
||||
* @param attribute 勇士属性名
|
||||
* @param currentValue 当前勇士属性值
|
||||
* @param upperLimit 二分上界
|
||||
* @param referenceDamage 参考伤害值
|
||||
* @param maxIterations 最大迭代数量
|
||||
*/
|
||||
private findNextCritical(
|
||||
handler: IReadonlyEnemyHandler<TEnemy, THero>,
|
||||
hero: IHeroAttribute<THero>,
|
||||
attribute: CriticalableHeroStatus<THero>,
|
||||
currentValue: number,
|
||||
upperLimit: number,
|
||||
referenceDamage: number,
|
||||
maxIterations: number
|
||||
): ICriticalSearchResult | null {
|
||||
let left = currentValue;
|
||||
let right = upperLimit;
|
||||
|
||||
hero.set(attribute, right as THero[typeof attribute]);
|
||||
|
||||
let targetInfo = this.calculator!.calculate(handler);
|
||||
if (targetInfo.damage >= referenceDamage) return null;
|
||||
|
||||
let iter = 0;
|
||||
while (iter++ < maxIterations) {
|
||||
const middle = Math.floor((left + right) / 2);
|
||||
hero.set(attribute, middle as THero[typeof attribute]);
|
||||
const middleInfo = this.calculator!.calculate(handler);
|
||||
|
||||
if (middleInfo.damage < referenceDamage) {
|
||||
right = middle;
|
||||
targetInfo = middleInfo;
|
||||
} else {
|
||||
left = middle;
|
||||
}
|
||||
if (right - left <= 1) break;
|
||||
}
|
||||
|
||||
return {
|
||||
value: right,
|
||||
info: targetInfo
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class DamageSystem<TEnemy, THero>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { IEnemy, IReadonlyEnemy } from '@user/data-base';
|
||||
import { IEnemyView, IEnemyContext } from './types';
|
||||
import { IEnemyContext, IComputingEnemyView } from './types';
|
||||
|
||||
export class EnemyView<TAttr> implements IEnemyView<TAttr> {
|
||||
export class EnemyView<TAttr> implements IComputingEnemyView<TAttr> {
|
||||
/** 计算后怪物 */
|
||||
private readonly computedEnemy: IEnemy<TAttr>;
|
||||
|
||||
@ -25,9 +25,6 @@ export class EnemyView<TAttr> implements IEnemyView<TAttr> {
|
||||
return this.computedEnemy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取计算中怪物对象,这个接口不对外暴露,仅在系统内部的 EnemyContext 中使用。
|
||||
*/
|
||||
getComputingEnemy(): IEnemy<TAttr> {
|
||||
return this.computedEnemy;
|
||||
}
|
||||
|
||||
@ -45,17 +45,18 @@ export class MapDamage<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
private readonly sourcelessDamage: Map<number, IPointInfo> = new Map();
|
||||
/** 有来源地图伤害,坐标 -> 点伤害信息 */
|
||||
private readonly sourcedDamage: Map<number, IPointInfo> = new Map();
|
||||
/** 地图伤害视图 -> 其信息对象 */
|
||||
/** 地图伤害视图 -> 地图伤害视图对应的信息对象 */
|
||||
private readonly viewStore: Map<IMapDamageView<any>, IViewStore<TEnemy>> =
|
||||
new Map();
|
||||
/** 地图伤害信息 -> 其信息对象 */
|
||||
/** 地图伤害数据 -> 地图伤害数据对应的信息对象 */
|
||||
private readonly damageStore: Map<IMapDamageInfo, IDamageStore<TEnemy>> =
|
||||
new Map();
|
||||
/** 怪物视图 -> 其影响对象 */
|
||||
/** 怪物视图 -> 其产生的地图伤害视图 */
|
||||
private readonly enemyStore: Map<
|
||||
IEnemyView<TEnemy>,
|
||||
Set<IMapDamageView<any>>
|
||||
> = new Map();
|
||||
|
||||
/** 需要延迟刷新的坐标索引 */
|
||||
private readonly dirtyIndexes: Set<number> = new Set();
|
||||
/** 合并后伤害缓存,索引 -> 合并结果 */
|
||||
@ -96,6 +97,16 @@ export class MapDamage<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空地图伤害对象
|
||||
*/
|
||||
private createEmptyDamage(): IPointInfo {
|
||||
return {
|
||||
affectedBy: new Set(),
|
||||
damages: new Set()
|
||||
};
|
||||
}
|
||||
|
||||
useReducer(reducer: IMapDamageReducer): void {
|
||||
this.reducer = reducer;
|
||||
this.reducedCache.clear();
|
||||
@ -103,10 +114,9 @@ export class MapDamage<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
|
||||
addMapDamage(locator: ITileLocator, info: IMapDamageInfo): void {
|
||||
const index = this.indexer.locatorToIndex(locator);
|
||||
const store = this.sourcelessDamage.getOrInsertComputed(index, () => ({
|
||||
affectedBy: new Set(),
|
||||
damages: new Set()
|
||||
}));
|
||||
const store = this.sourcelessDamage.getOrInsertComputed(index, () =>
|
||||
this.createEmptyDamage()
|
||||
);
|
||||
store.damages.add(info);
|
||||
this.markDirtyIndex(index);
|
||||
}
|
||||
@ -150,27 +160,36 @@ export class MapDamage<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
this.refreshEnemyAndClearCache(view, locator);
|
||||
}
|
||||
|
||||
deleteEnemy(view: IEnemyView<TEnemy>): void {
|
||||
const store = this.enemyStore.get(view);
|
||||
if (!store) return;
|
||||
const collection = new Set<number>();
|
||||
for (const viewItem of store) {
|
||||
const affecting = this.viewStore.get(viewItem);
|
||||
if (!affecting) continue;
|
||||
affecting.damages.forEach((dam, index) => {
|
||||
this.damageStore.delete(dam);
|
||||
collection.add(index);
|
||||
/**
|
||||
* 移除指定怪物所产生的地图伤害
|
||||
* @param view 怪物视图
|
||||
*/
|
||||
private removeEnemyAffecting(view: IEnemyView<TEnemy>) {
|
||||
const views = this.enemyStore.get(view);
|
||||
if (!views) return;
|
||||
// 收集被移除贡献的坐标索引,遍历结束后统一标脏以清掉其旧合并缓存
|
||||
const removed = new Set<number>();
|
||||
views.forEach(viewItem => {
|
||||
const store = this.viewStore.get(viewItem);
|
||||
if (!store) return;
|
||||
store.damages.forEach((dam, index) => {
|
||||
removed.add(index);
|
||||
const point = this.sourcedDamage.get(index);
|
||||
if (!point) return;
|
||||
point.affectedBy.delete(viewItem);
|
||||
point.damages.delete(dam);
|
||||
this.damageStore.delete(dam);
|
||||
});
|
||||
this.viewStore.delete(viewItem);
|
||||
}
|
||||
this.enemyStore.delete(view);
|
||||
collection.forEach(v => {
|
||||
this.markDirtyIndex(v);
|
||||
});
|
||||
removed.forEach(index => {
|
||||
this.markDirtyIndex(index);
|
||||
});
|
||||
}
|
||||
|
||||
deleteEnemy(view: IEnemyView<TEnemy>): void {
|
||||
this.removeEnemyAffecting(view);
|
||||
this.enemyStore.delete(view);
|
||||
}
|
||||
|
||||
getReducedDamage(locator: ITileLocator): Readonly<IMapDamageInfo> | null {
|
||||
@ -240,34 +259,6 @@ export class MapDamage<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
this.reducedCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定怪物所产生的地图伤害
|
||||
* @param view 怪物视图
|
||||
*/
|
||||
private removeEnemyAffecting(view: IEnemyView<TEnemy>) {
|
||||
const views = this.enemyStore.get(view);
|
||||
if (!views) return;
|
||||
// 收集被移除贡献的坐标索引,遍历结束后统一标脏以清掉其旧合并缓存
|
||||
const removed = new Set<number>();
|
||||
views.forEach(viewItem => {
|
||||
const store = this.viewStore.get(viewItem);
|
||||
if (!store) return;
|
||||
store.damages.forEach((dam, index) => {
|
||||
removed.add(index);
|
||||
const point = this.sourcedDamage.get(index);
|
||||
if (!point) return;
|
||||
point.affectedBy.delete(viewItem);
|
||||
point.damages.delete(dam);
|
||||
this.damageStore.delete(dam);
|
||||
});
|
||||
this.viewStore.delete(viewItem);
|
||||
});
|
||||
this.enemyStore.delete(view);
|
||||
removed.forEach(index => {
|
||||
this.markDirtyIndex(index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 登记有来源伤害的反向索引,供删除与局部刷新时按来源清理
|
||||
* @param point 坐标点伤害信息
|
||||
@ -322,10 +313,7 @@ export class MapDamage<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||
const loc = this.indexer.locator(index);
|
||||
const point = this.sourcedDamage.getOrInsertComputed(
|
||||
index,
|
||||
() => ({
|
||||
affectedBy: new Set(),
|
||||
damages: new Set()
|
||||
})
|
||||
() => this.createEmptyDamage()
|
||||
);
|
||||
const damage = viewItem.getDamageWithoutCheck(loc);
|
||||
if (damage) {
|
||||
|
||||
@ -71,6 +71,13 @@ export interface IEnemyView<TEnemy> {
|
||||
markDirty(): void;
|
||||
}
|
||||
|
||||
export interface IComputingEnemyView<TEnemy> extends IEnemyView<TEnemy> {
|
||||
/**
|
||||
* 获取正在计算过程中的怪物对象,此方法理应只用于内部上下文的属性计算,外部不应调用
|
||||
*/
|
||||
getComputingEnemy(): IEnemy<TEnemy>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 光环与查询
|
||||
|
||||
Loading…
Reference in New Issue
Block a user