mirror of
https://github.com/motajs/template.git
synced 2026-10-19 05:10:16 +08:00
docs(09): revise phase plan 09-20 (plugin system) for revision 3 (D-92 error codes 76/77/78, no-overwrite, exclude from load flow)
This commit is contained in:
parent
8739141aff
commit
618dbb7654
@ -11,43 +11,47 @@ files_modified:
|
||||
- packages-user/data-state/src/__test__/plugin.test.ts
|
||||
- packages-user/client-modules/src/types.ts
|
||||
- packages-user/client-modules/src/client.ts
|
||||
- packages/common/src/logger.json
|
||||
- .planning/phases/09-minimum-browser-runnable/09-20-SUMMARY.md
|
||||
autonomous: false
|
||||
requirements: []
|
||||
estimate:
|
||||
tokens: 70000
|
||||
raw_tokens: 35000
|
||||
tokens: 72000
|
||||
raw_tokens: 36000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03 / D-86)**:Task 0 必须先汇报「本计划 = 任务 10 · 数据端插件驱动 + 渲染端插件系统镜像(仅引擎侧)」的**要做什么 / 为什么 / 大致怎么做 / 影响范围**,**必须列明:四段式 `load()` 编排(`awake` 同步 → `initialize` 异步阻塞 → loader 加载 → `afterLoad` 异步阻塞)、依赖拓扑序与依赖环处置、数据端注册表 `plugins` 与 `getDataPlugin`、渲染端镜像命名 `IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / `clientPlugins` 与 `ClientCore` 落点、渲染端 `state` 参数类型(待定)、数据端先渲染端后的统一 `load()` 关系、运行时守卫(`config.plugins ?? []` / `config.clientPlugins ?? []`)及其理由、**全部拟引入命名(`plugins` / `plugin.ts` / `orderPlugins` / `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult` 及其成员 / `clientPlugins` / `awake`·`initialize`·`afterLoad` 的 `private`→`protected` 可见性变更)**、写操作边界、验证门禁方式与 `open_items`(OI-1..OI-10)**,然后必须**停下等待**用户回复;**回复为「可以执行」即视为通过(本计划不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行)**;用户若对任一 `open_items` 给出指定,按指定执行。"
|
||||
- "**本计划的第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求,D-03 / D-86)**:Task 0 必须先汇报「本计划 = 任务 10 · 数据端插件驱动 + 渲染端插件系统镜像(仅引擎侧)」的**要做什么 / 为什么 / 大致怎么做 / 影响范围**,**必须列明:四段式 `load()` 编排(`awake` 同步 → `initialize` 异步阻塞 → loader 加载 → `afterLoad` 异步阻塞)、依赖拓扑序与插件错误处置(循环依赖 → 新码 76、依赖不存在 → 新码 77、重复 id → 新码 78 且不覆盖原插件;报错插件及其下游全部不进入加载流程)、数据端注册表 `plugins` 与 `getDataPlugin`、渲染端镜像命名 `IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / `clientPlugins` 与 `ClientCore` 落点、渲染端 `state` 参数类型(待定)、数据端先渲染端后的统一 `load()` 关系、运行时守卫(`config.plugins ?? []` / `config.clientPlugins ?? []`)及其理由、**`packages/common/src/logger.json` 将新增 error 76/77/78(保留既有 75 及其它条目)**、**全部拟引入命名(`plugins` / `plugin.ts` / `orderPlugins` / `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult` 及其成员 / `clientPlugins` / `awake`·`initialize`·`afterLoad` 的 `private`→`protected` 可见性变更)**、写操作边界、验证门禁方式与 `open_items`(OI-1..OI-11)**,然后必须**停下等待**用户回复;**回复为「可以执行」即视为通过(本计划不因触发方式或 `mode: yolo` / `workflow.auto_advance` 自动放行)**;用户若对任一 `open_items` 给出指定,按指定执行。"
|
||||
- "**运行时安全(D-91 / D-56):** `CoreState` 构造器以 `for (const define of config.plugins ?? [])` **运行时守卫**建立注册表;`ClientCore` 镜像 `for (const define of config.clientPlugins ?? [])`。理由:现存全部 `new CoreState({...})` 调用点(`data-state/src/ins.ts:11` 及 ~9 处 test/fixture)均未提供 `plugins`,`ClientCore` 调用点(`client-modules/src/core.ts:6`)亦未提供 `clientPlugins`——若无守卫,`for … of undefined` 会抛错并破坏全部 `@user/data-state` 导入。既有调用点**不纳入 `files_modified`、不改动**;其 `ICoreStateConfig` 必填字段(`plugins` / `heroFace` / `events`)缺失属**用户 WIP**(D-56 / D-91),由用户后续接线时统一核对。"
|
||||
- "**数据端插件驱动(D-86 / D-87):** `CoreState` 构造器由 `config.plugins`(经守卫)建立注册表(`Map<string, IDataPlugin<IDataPluginDefine>>`,每项 `{ status: PluginStatus.NotAwakened, plugin: define }`);`getDataPlugin(id)` 从注册表读取;`load()` 编排四段式——① `awake`(同步,主对象创建后、初始化前)→ ② `initialize`(异步、阻塞,加载开始前)→ ③ 执行 loader 加载(`this.loader.start()` 驱动至完成)→ ④ `afterLoad`(异步、阻塞,加载完毕后)。"
|
||||
- "**依赖拓扑序与依赖环(D-87):** 三个阶段各自按依赖拓扑序调用插件方法(`plugin.awake/initialize/afterLoad(this, deps)`),并在调用后推进 `status`(Awakened / Initialized / Loaded);依赖出现环时记录 `logger.error(75, id, info)`,**环上及其下游插件均不初始化**(`status` 保持 `NotAwakened`),其余无环插件正常初始化。"
|
||||
- "**渲染端镜像(D-88):** 类型放 `packages-user/client-modules/src/types.ts` 顶层——`IClientPlugin` / `IClientPluginDefine`(配置字段 `clientPlugins`);`IClientCore` 增 `getClientPlugin(id)`;**复用数据端 `PluginStatus`(自 `@user/data-state` 导入,不自建枚举)**;实现在 `ClientCore`(`packages-user/client-modules/src/client.ts`)。渲染端 `orderPlugins` **自 `@user/data-state` 导入**(`client-modules` 内不存在含该导出的本地模块)。"
|
||||
- "**插件错误处置与日志码(D-92,修订 D-87):** 三个阶段各自按依赖拓扑序调用插件方法(`plugin.awake/initialize/afterLoad(this, deps)`),并在调用后推进 `status`(Awakened / Initialized / Loaded)。插件错误一律使用 **`packages/common/src/logger.json` error 段新增码**:**循环依赖 → `logger.error(76, …)`**;**依赖不存在的插件 → `logger.error(77, …)`**;**重复 id → `logger.error(78, …)`**。**重复 id 不覆盖原插件**(保留先注册者,重复者不注册)。**报错的插件全部不进入加载流程**(`awake`/`initialize`/`afterLoad` 均不执行,`status` 保持 `NotAwakened`);**其下游依赖者亦不加载**(同样缺席于拓扑序、`status` 保持 `NotAwakened`)。"
|
||||
- "**渲染端镜像(D-88 / D-92):** 类型放 `packages-user/client-modules/src/types.ts` 顶层——`IClientPlugin` / `IClientPluginDefine`(配置字段 `clientPlugins`);`IClientCore` 增 `getClientPlugin(id)`;**复用数据端 `PluginStatus`(自 `@user/data-state` 导入,不自建枚举)**;实现在 `ClientCore`(`packages-user/client-modules/src/client.ts`)。渲染端 `orderPlugins` **自 `@user/data-state` 导入**(`client-modules` 内不存在含该导出的本地模块)。渲染端**镜像同一错误处置**(重复 id → `logger.error(78, …)` 且不覆盖;循环依赖 → 76;依赖不存在 → 77;报错插件及其下游不进入加载流程)。"
|
||||
- "**统一 `load()` 与渲染端四段式(D-89 / D-90):** 渲染端 `awake` / `initialize` / `afterLoad` 语义与数据端一致(同一四段式);**数据插件先、渲染插件后**——在统一 `load()` 中,每一阶段先驱动数据端插件(L0-L3),再驱动渲染端插件(L4-L5)。"
|
||||
- "**仅引擎侧与范围边界(D-86 / D-91 / D-56):** 仅实现插件系统机制本身;**不改 `src/` 入口与配置接线**、**不改 `initializeL0..L3` 等 config 字段口径**、**不新增依赖**、**不改用户其它并发 WIP 文件**(`index.html`、`data-base/hero/*`、`common/logger.json`、`src/bootstrap.ts` / `src/client/**` / `src/data/**` / `src/constant.ts`、`packages-user/data-state/src/types.ts`、`packages-user/client-modules/src/index.ts`)、**不改 `ROADMAP.md` / `STATE.md` / 既有 PLAN 与 SUMMARY / `09-CONTEXT.md`**;遵循 `dev.md`(无循环引用、命名规则、jsDoc、模块无副作用)。"
|
||||
- "**命名须先获批(AGENTS.md / D-04):** 除 D-88 已批准的 `IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / `clientPlugins`(配置字段)外,任何新命名(含 `plugins` 字段、`plugin.ts` / `orderPlugins` / 三个接口**及其成员**(`IPluginDepends` 的 `id` / `dependencies`、`IPluginHolder` 的 `plugin`、`IPluginOrderResult` 的 `order` / `cyclic`)、`private`→`protected` 可见性变更、`clientPlugins` 注册表字段)一律先入 `open_items`,由 Task 0 报备待批准后方可使用;未获批前不得擅自使用(D-04 覆盖接口成员命名)。"
|
||||
- "**仅引擎侧与范围边界(D-86 / D-91 / D-56):** 仅实现插件系统机制本身;**不改 `src/` 入口与配置接线**、**不改 `initializeL0..L3` 等 config 字段口径**、**不新增依赖**、**不改用户其它并发 WIP 文件**(`index.html`、`data-base/hero/*`、`src/bootstrap.ts` / `src/client/**` / `src/data/**` / `src/constant.ts`、`packages-user/data-state/src/types.ts`、`packages-user/client-modules/src/index.ts`)、**不改 `ROADMAP.md` / `STATE.md` / 既有 PLAN 与 SUMMARY / `09-CONTEXT.md`**;遵循 `dev.md`(无循环引用、命名规则、jsDoc、模块无副作用)。**唯一例外:本计划允许编辑 `packages/common/src/logger.json`(error 段新增 76/77/78,保留既有条目尤其用户 WIP 已加的 75 不动)**。"
|
||||
- "**命名须先获批(AGENTS.md / D-04):** 除 D-88 已批准的 `IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / `clientPlugins`(配置字段)外,任何新命名(含 `plugins` 字段、`plugin.ts` / `orderPlugins` / 三个接口**及其成员**(`IPluginDepends` 的 `id` / `dependencies`、`IPluginHolder` 的 `plugin`、`IPluginOrderResult` 的 `order` / `cyclic` / `missing`)、`private`→`protected` 可见性变更、`clientPlugins` 注册表字段)一律先入 `open_items`,由 Task 0 报备待批准后方可使用;未获批前不得擅自使用(D-04 覆盖接口成员命名)。新增 logger 码 76/77/78 的**文本**亦入 `open_items`(OI-11)待批准。"
|
||||
artifacts:
|
||||
- path: packages-user/data-state/src/plugin.ts
|
||||
provides: "新增共享纯函数 `orderPlugins`(依赖拓扑排序 + 依赖环检测)与其结果/约束接口(`IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`);零 import、零副作用,可被数据端与渲染端复用(命名待批准,OI-1)"
|
||||
provides: "新增共享纯函数 `orderPlugins`(依赖拓扑排序 + 依赖环检测 + 依赖不存在检测)与其结果/约束接口(`IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`);零 import、零副作用,可被数据端与渲染端复用(命名与 `missing` 成员待批准,OI-1)"
|
||||
- path: packages-user/data-state/src/core.ts
|
||||
provides: "`CoreState` 增 `plugins` 注册表(由 `config.plugins ?? []` 建立);实现 `awake` / `initialize` / `afterLoad` 四段式插件驱动(拓扑序 + `PluginStatus` 推进 + 依赖环处置 `logger.error(75,...)`);`getDataPlugin` 从注册表读取;`load()` 编排四段(含非空循环体驱动 `this.loader.start()`)"
|
||||
provides: "`CoreState` 增 `plugins` 注册表(由 `config.plugins ?? []` 建立,重复 id 不覆盖且 `logger.error(78,...)`);实现 `awake` / `initialize` / `afterLoad` 四段式插件驱动(拓扑序 + `PluginStatus` 推进 + 错误处置 `logger.error(76,...)` / `logger.error(77,...)`);`getDataPlugin` 从注册表读取;`load()` 编排四段(含非空循环体驱动 `this.loader.start()`)"
|
||||
- path: packages-user/data-state/src/index.ts
|
||||
provides: "新增桶导出 `export * from './plugin'`(同目录项,符合 D-69 桶边界),使渲染端可自 `@user/data-state` 导入 `orderPlugins` 与相关接口"
|
||||
- path: packages-user/data-state/src/__test__/plugin.test.ts
|
||||
provides: "可运行单测:`orderPlugins` 的拓扑序与环检测;`CoreState` 四段式驱动的调用顺序与 `PluginStatus` 推进(构造时提供 `plugins` / `heroFace` / `events`,不依赖 DOM);**以及省略 `plugins` 时 `new CoreState({ loadStarter, coreURL } as any)` + `await state.load()` 不抛错(证明运行时守卫)**"
|
||||
provides: "可运行单测:`orderPlugins` 的拓扑序、环检测、依赖不存在检测;`CoreState` 四段式驱动的调用顺序与 `PluginStatus` 推进(构造时提供 `plugins` / `heroFace` / `events`,不依赖 DOM);环上插件与依赖不存在插件(含下游)均不初始化;重复 id 保留原插件(不覆盖);**以及省略 `plugins` 时 `new CoreState({ loadStarter, coreURL } as any)` + `await state.load()` 不抛错(证明运行时守卫)**"
|
||||
- path: packages-user/client-modules/src/types.ts
|
||||
provides: "顶层新增 `IClientPlugin<T>` / `IClientPluginDefine`;`IClientCoreConfig` 增 `clientPlugins: IClientPluginDefine[]`;`IClientCore` 增 `getClientPlugin`;自 `@user/data-state` 导入并复用 `PluginStatus`"
|
||||
- path: packages-user/client-modules/src/client.ts
|
||||
provides: "`ClientCore` 增 `clientPlugins` 注册表(由 `config.clientPlugins ?? []` 建立)与 `getClientPlugin`;自 `@user/data-state` 复用 `orderPlugins`;覆写 `awake` / `initialize` / `afterLoad`(`super.` + 渲染端插件的同阶段驱动),使统一 `load()` 中数据端阶段先、渲染端阶段后"
|
||||
provides: "`ClientCore` 增 `clientPlugins` 注册表(由 `config.clientPlugins ?? []` 建立,重复 id 不覆盖且 `logger.error(78,...)`)与 `getClientPlugin`;自 `@user/data-state` 复用 `orderPlugins`;覆写 `awake` / `initialize` / `afterLoad`(`super.` + 渲染端插件的同阶段驱动 + 错误处置 `logger.error(76,...)` / `logger.error(77,...)`),使统一 `load()` 中数据端阶段先、渲染端阶段后"
|
||||
- path: packages/common/src/logger.json
|
||||
provides: "error 段新增 `76`(循环依赖)/ `77`(依赖不存在)/ `78`(重复 id)三条错误消息;**保留既有全部条目(尤其用户 WIP 已加的 `75`)不作任何改动**(消息文本待批准,OI-11)"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-20-SUMMARY.md
|
||||
provides: "本计划执行摘要:范围、四段式编排与拓扑/环处置口径、运行时守卫及其理由、命名获批情形、渲染端镜像落点与 `state` 参数类型最终裁定、**用户接线前「数据先渲染后」为空转的已知后果(D-91)**、门禁结果、`open_items` 最终处置"
|
||||
provides: "本计划执行摘要:范围、四段式编排与拓扑/错误处置口径(错误码 76/77/78 与不覆盖语义)、运行时守卫及其理由、命名获批情形、logger.json 新增条目、渲染端镜像落点与 `state` 参数类型最终裁定、**用户接线前「数据先渲染后」为空转的已知后果(D-91)**、门禁结果、`open_items` 最终处置"
|
||||
key_links:
|
||||
- "`orderPlugins`(`data-state/src/plugin.ts`,经 `data-state/src/index.ts` 桶导出)↔ `CoreState.plugins` / `ClientCore.clientPlugins` 两个注册表 —— 拓扑序与环检测的唯一实现,数据/渲染两端复用(渲染端自 `@user/data-state` 导入)"
|
||||
- "`orderPlugins`(`data-state/src/plugin.ts`,经 `data-state/src/index.ts` 桶导出)↔ `CoreState.plugins` / `ClientCore.clientPlugins` 两个注册表 —— 拓扑序、环检测与依赖不存在检测的唯一实现,数据/渲染两端复用(渲染端自 `@user/data-state` 导入)"
|
||||
- "`CoreState.awake` / `initialize` / `afterLoad`(`protected`)↔ `ClientCore` 的同名覆写 —— 统一 `load()` 中「数据端阶段先、渲染端阶段后」的接线点(D-90)"
|
||||
- "`CoreState.load()` ↔ `this.loader.start()`(`IMotaDataLoader`,`packages-user/data-state/src/loader/loader.ts:84`)—— 第三段「执行 loader 加载」的驱动方式(非空循环体,对齐 `packages-user/data-state/src/loader/__test__/loader.test.ts:163` 的 `for await ... of loader.start()` 模式,且满足 eslint `no-empty`)"
|
||||
- "`logger.error(76/77/78, …)`(`packages/common/src/logger.ts:117` 的 `error(code, ...params: string[])`)↔ `packages/common/src/logger.json` error 段新增 `76` / `77` / `78` —— 插件错误码的唯一来源;`75` 为用户 WIP 已加、保留不动(D-92 / D-56)"
|
||||
- "`PluginStatus`(`packages-user/data-state/src/types.ts:10`)↔ `client-modules/src/types.ts` —— 渲染端复用数据端枚举(D-88),不自建"
|
||||
- "`IClientCoreConfig.clientPlugins` ↔ `config.plugins`(`ICoreStateConfig`,`packages-user/data-state/src/types.ts:80`)—— 渲染端配置扩展数据端配置(`IClientCoreConfig extends ICoreStateConfig`)"
|
||||
- "`config.plugins ?? []`(`CoreState` 构造器)↔ `config.clientPlugins ?? []`(`ClientCore` 构造器)—— 运行时守卫,保证未接线插件配置的既有调用点不抛错(D-91 / D-56)"
|
||||
@ -58,53 +62,58 @@ must_haves:
|
||||
- "**测试可运行且不依赖 DOM:** `packages-user/data-state` 已有 vitest 测试设施(`packages-user/data-state/src/**/__test__/*.test.ts`、`packages-user/data-state/__test__/*.test.ts`)且既有测试直接 `new CoreState({...})`(如 `packages-user/data-state/__test__/coreNode.test.ts:8`),说明 `CoreState` 可在 Node 下构造;`vitest` 由 esbuild 转译、不做类型检查,故用户并发 WIP(`ICoreStateConfig` 必填字段变更)造成的历史类型错误不阻断本计划的运行期门禁。"
|
||||
- "**`load()` 第三段在无配置时为安全空转:** `MotaDataLoader.start()` 在 `coreConfigs` / `extraConfigs` / `customTask` 均为空时会走完全部空迭代并 `loadResolve()`(`packages-user/data-state/src/loader/loader.ts:84-142`),故本计划不接线加载器配置(D-91)时 `load()` 仍可完成。"
|
||||
- "**渲染端无法在 Node 下做运行期门禁:** `ClientCore` 构造器实例化 `MotaRenderer`(需要 canvas / DOM),故渲染端以**结构断言**(源码逐项断言)而非运行期测试作为门禁;数据端以**运行期单测 + 结构断言**双门禁。"
|
||||
- "**`logger.json` 的编辑面最小:** 用户 WIP 已在 error 段加入 `75`(`packages/common/src/logger.json:77`);本计划仅在其后**追加** `76` / `77` / `78` 三条(保持 4 空格缩进与尾逗号风格),**不得**改动、删除或重排任何既有条目(尤其 `75` 与其它码)。"
|
||||
- "**行号可能漂移:** 本计划引用行号为规划日实测;门禁以「文件存在 + 结构/行为断言」为准,不硬编码行号。"
|
||||
prohibitions:
|
||||
- "不得在用户回复通过 Task 0 关卡之前执行本计划的任何任务、或派发任何执行子代理。"
|
||||
- "**未经批准的命名一律不得使用:** 本计划拟引入的 `plugins` / `plugin.ts` / `orderPlugins` / `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`(**及其成员** `id` / `dependencies` / `plugin` / `order` / `cyclic`)/ `clientPlugins`(注册表字段)以及 `awake`·`initialize`·`afterLoad` 的 `private`→`protected` 可见性变更,**必须先经 Task 0 报备并获用户批准**;未获批前不得落码。"
|
||||
- "**未经批准的命名一律不得使用:** 本计划拟引入的 `plugins` / `plugin.ts` / `orderPlugins` / `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`(**及其成员** `id` / `dependencies` / `plugin` / `order` / `cyclic` / `missing`)/ `clientPlugins`(注册表字段)以及 `awake`·`initialize`·`afterLoad` 的 `private`→`protected` 可见性变更,**必须先经 Task 0 报备并获用户批准**;未获批前不得落码。**logger 码 76/77/78 的消息文本**亦须经 OI-11 报备。"
|
||||
- "**注册表构建必须运行时守卫(D-91 / D-56):** `CoreState` 构造器必须用 `config.plugins ?? []`、`ClientCore` 构造器必须用 `config.clientPlugins ?? []`;**不得**对未提供 `plugins` / `clientPlugins` 的调用点做无守卫遍历。**不得**把现存 `CoreState` 调用点(`ins.ts` 与各 test/fixture)或 `ClientCore` 调用点纳入 `files_modified` 或修改之;**不得**改 `data-state/src/types.ts`——既有调用点的类型不匹配属用户 WIP(D-56 / D-91)。"
|
||||
- "**不得使用空循环体:** `load()` 第三段驱动加载器的 `for await` 循环体不得为空块(eslint `no-empty` 会报错),必须消费/忽略产出值或含有效语句。"
|
||||
- "**渲染端 `orderPlugins` 来源固定:** 必须自 `@user/data-state` 导入;**不得**指向 `client-modules` 内不存在的本地模块。"
|
||||
- "**写入边界(D-86 / D-91):** 只允许写本 `files_modified` 所列 7 个路径;**不得**改 `src/**`(入口与接线)、`packages-user/data-base/**`、`packages-user/client-base/**`、`packages/common/**`、`package.json` / lockfile、以及用户其它并发 WIP 文件(`index.html`、`data-base/hero/*`、`common/logger.json`、`packages-user/data-state/src/types.ts`、`packages-user/client-modules/src/index.ts`)。"
|
||||
- "**写入边界(D-86 / D-91):** 只允许写本 `files_modified` 所列 8 个路径;**不得**改 `src/**`(入口与接线)、`packages-user/data-base/**`、`packages-user/client-base/**`、`package.json` / lockfile、以及用户其它并发 WIP 文件(`index.html`、`data-base/hero/*`、`packages-user/data-state/src/types.ts`、`packages-user/client-modules/src/index.ts`)。**允许且仅允许**对 `packages/common/src/logger.json` 的 error 段**追加** 76/77/78;不得改动或删除该文件任何既有条目。"
|
||||
- "**`.planning/**` 边界:** 不得改 `ROADMAP.md` / `STATE.md` / `09-CONTEXT.md` / 任何既有 PLAN 或 SUMMARY;只允许新增 `09-20-SUMMARY.md`。"
|
||||
- "**用户并发改动保留:** 编辑 `data-state/src/core.ts` / `client-modules/src/types.ts` / `client-modules/src/client.ts` 时,只能在其**现有内容之上**追加/补全,**不得回退或删除**用户已有改动(如 `data-state/src/core.ts` 的 `awake` / `initialize` / `afterLoad` / `getDataPlugin` 空壳与 import、`client-modules/src/types.ts` 的 `IClientCoreConfig extends ICoreStateConfig` 注释改动);提交前 `git status` 逐条核对暂存面。"
|
||||
- "**用户并发改动保留:** 编辑 `data-state/src/core.ts` / `client-modules/src/types.ts` / `client-modules/src/client.ts` / `common/src/logger.json` 时,只能在其**现有内容之上**追加/补全,**不得回退或删除**用户已有改动(如 `data-state/src/core.ts` 的 `awake` / `initialize` / `afterLoad` / `getDataPlugin` 空壳与 import、`client-modules/src/types.ts` 的 `IClientCoreConfig extends ICoreStateConfig` 注释改动、`logger.json` 的 error `75` 条目);提交前 `git status` 逐条核对暂存面。"
|
||||
- "**不自建 `PluginStatus`:** 渲染端必须复用 `@user/data-state` 的 `PluginStatus`;不得在 `packages-user/client-modules` 内新建同义枚举或常量。"
|
||||
- "**不新增依赖 / 不改 logger:** 不得新增任何依赖、不得改 `package.json` / lockfile;**不得改 `packages/common/src/logger.json`**(D-56 保护)——依赖环日志复用**既有** `error 75`;重复插件 id 的告警若需新码,先入 `open_items`,未获批前不新增告警码。"
|
||||
- "**插件错误码与语义(D-92):** 循环依赖必须走 `logger.error(76, …)`、依赖不存在必须走 `logger.error(77, …)`、重复 id 必须走 `logger.error(78, …)`;**不得**再复用 `logger.error(75, …)` 作为插件错误码(`75` 仅作为既有条目保留于 `logger.json`,不被本计划代码引用)。**重复 id 不得覆盖原插件**;**报错插件及其下游必须排除于整个加载流程**(不得调用 `awake` / `initialize` / `afterLoad`,`status` 保持 `NotAwakened`)。"
|
||||
- "**不新增依赖:** 不得新增任何依赖、不得改 `package.json` / lockfile;`logger` 自既有 `@motajs/common` 导入(`data-state` 已用、`client-modules` 已有先例 `client-modules/src/loader/hook.ts:1`)。"
|
||||
- "**不触碰无关内容:** 不得改动既有代码的注释 / jsDoc(AGENTS.md);不得将 `initializeEnemy` / `initializeSave` / `initializeReplay` 的接线纳入本计划(属用户 config 口径,D-91)。"
|
||||
- "不得把门禁建立在「全仓 `pnpm check:type`」「全仓 `pnpm test:ci`」「`pnpm build`」或全仓 TS 诊断数之上(用户 WIP 存在历史类型错误)。"
|
||||
- "不得触碰、暂存、提交用户的并发改动(`AGENTS.md` / `09-CONTEXT.md` / `index.html` / `data-base/hero/*` / `common/logger.json` / `src/**`)。"
|
||||
- "不得触碰、暂存、提交用户的其它并发改动(`AGENTS.md` / `09-CONTEXT.md` / `index.html` / `data-base/hero/*` / `src/**` / `packages-user/client-modules/src/index.ts`)。"
|
||||
open_items:
|
||||
- "**OI-1(共享排序模块与命名,含成员):** 提议新增文件 `packages-user/data-state/src/plugin.ts`,导出纯函数 `orderPlugins`(依赖拓扑排序 + 环检测)及接口 `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`(dev.md 要求对象类型须单独声明 interface,故拆为三接口)。**待用户报备批准**(D-04 亦覆盖接口成员命名):`IPluginDepends` 成员 `id`(只读字符串)/ `dependencies`(只读字符串数组);`IPluginHolder<T>` 成员 `plugin`(只读,类型 `T`);`IPluginOrderResult<T>` 成员 `order`(`T[]`)/ `cyclic`(`Set<string>`);`orderPlugins(plugins: ReadonlyMap<string, H>)` 的参数名 `plugins`。备选 = 各端内联私有方法(但会引入两个不同名的私有方法且逻辑重复)。"
|
||||
- "**OI-1(共享排序模块与命名,含成员):** 提议新增文件 `packages-user/data-state/src/plugin.ts`,导出纯函数 `orderPlugins`(依赖拓扑排序 + 环检测 + 依赖不存在检测)及接口 `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`(dev.md 要求对象类型须单独声明 interface,故拆为三接口)。**待用户报备批准**(D-04 亦覆盖接口成员命名):`IPluginDepends` 成员 `id`(只读字符串)/ `dependencies`(只读字符串数组);`IPluginHolder<T>` 成员 `plugin`(只读,类型 `T`);`IPluginOrderResult<T>` 成员 `order`(`T[]`,可初始化拓扑序)/ `cyclic`(`Set<string>`,**处于依赖环中的插件 id**)/ `missing`(`Map<string, string[]>`,**直接引用了不存在依赖的插件 id → 其不存在的依赖 id 数组**);`orderPlugins(plugins: ReadonlyMap<string, H>)` 的参数名 `plugins`。**语义(D-92):** `order` 不含环上插件 / 依赖不存在插件**及其下游**;`cyclic` / `missing` 仅记录直接成因,供驱动端分别报 76 / 77;下游仅缺席于 `order`、不单独报码(由消息「related plugins will not be initialized」涵盖)。备选 = 各端内联私有方法(但会引入两个不同名的私有方法且逻辑重复)。"
|
||||
- "**OI-2(数据端注册表字段名):** 提议 `CoreState` 增私有只读字段 `plugins`(`Map<string, IDataPlugin<IDataPluginDefine>>`)。用户 WIP 的 `getDataPlugin` 已引用 `this.plugins`,但该名不在 D-88 批准名单内。**待用户报备批准**。"
|
||||
- "**OI-3(渲染端注册表字段名):** 提议 `ClientCore` 增私有只读字段 `clientPlugins`(`Map<string, IClientPlugin<IClientPluginDefine>>`)。D-88 仅明确**配置字段** `clientPlugins`;注册表字段同名是否可接受,**待确认**。"
|
||||
- "**OI-4(统一 `load()` 的接线方式):** 提议把 `CoreState` 的 `awake` / `initialize` / `afterLoad` 由 `private` 改为 `protected`,由 `ClientCore` 覆写同名方法(`super.` + 渲染端同阶段驱动),使统一 `load()` 中「每阶段数据端先、渲染端后」。**可见性变更为受保护成员,须报备批准**。备选 = `ClientCore` 覆写 `load()` 并引入 3 个新私有方法名(须另行报备)。"
|
||||
- "**OI-5(渲染插件 `state` 参数类型):** 提议渲染插件三方法的 `state` 参数类型为 **`IClientCore`**(与数据端 `ICoreState` 同为「主对象」顶层类型对齐);备选 **`IClientBase`**(层级 L4)。**待用户确认**;未确认前采用 `IClientCore`。"
|
||||
- "**OI-6(依赖环处置语义):** 类型注释称「会抛出错误,且所有在环上的插件都不会初始化」(`data-state/src/types.ts:31`)。本计划提议:**记录 `logger.error(75, id, info)` 且环上及其下游插件均不初始化,不抛出、不中断 `load()`**(与既有 `error 75` 文案「related plugins will not be initialized」一致)。**待用户确认**是否需改为 `throw`。"
|
||||
- "**OI-7(依赖 id 不存在时的处置):** 插件 `dependencies` 引用了注册表中不存在的 id 时,本计划提议**忽略该依赖(排序时跳过、解析 deps 时省略)**,不报错。**待用户确认**。"
|
||||
- "**OI-8(重复插件 id 的处置):** 若 `config.plugins` 出现重复 id,本计划提议**后者覆盖前者**(对齐 `addSaveableContent` 风格 `packages-user/data-state/src/core.ts:237-244`),但**不新增告警**(`logger.json` 受 D-56 保护、无对应 warn 码)。**待用户确认**是否需要告警及其 logger 码来源。"
|
||||
- "**OI-6(依赖环处置语义 · 已裁定,D-92):** **DECIDED** —— 循环依赖不抛出、不中断 `load()`;由驱动端记录 **`logger.error(76, …)`**(error 段新码),**环上插件及其下游均不初始化**(`status` 保持 `NotAwakened`)。取代原「复用 `error 75`」口径。类型注释(`data-state/src/types.ts:31`)字面为「会抛出错误」,实现取「记录错误码 + 排除」,**不改注释**(AGENTS.md)。"
|
||||
- "**OI-7(依赖 id 不存在时的处置 · 已裁定,D-92):** **DECIDED** —— 插件 `dependencies` 引用了注册表中不存在的 id 时,记录 **`logger.error(77, pluginId, missingDepId)`**(error 段新码);**该插件及其下游均不初始化**(`status` 保持 `NotAwakened`)。取代原「忽略该依赖、不报错」口径。"
|
||||
- "**OI-8(重复插件 id 的处置 · 已裁定,D-92):** **DECIDED** —— `config.plugins` / `config.clientPlugins` 出现重复 id 时,记录 **`logger.error(78, id)`**(error 段新码)**并保留先注册者、重复者不注册(不覆盖)**。取代原「后者覆盖前者」口径。"
|
||||
- "**OI-9(是否为 `ClientCore` 传入数据端配置):** `ClientCore` 构造器当前 `super({ loadStarter: new WebLoadStarter(), coreURL: 'placeholder' })` 为占位(未传 `plugins` / `heroFace` / `events`)。本计划默认**不改该 `super(...)` 调用**(属「接线」,D-91 归用户),故 `ClientCore` 的数据端插件注册表沿用用户最终接线结果;**若用户要求本计划一并把 `config` 传入 `super`,请在 Task 0 回复中指明**。"
|
||||
- "**OI-10(新增测试文件):** 提议新增 `packages-user/data-state/src/__test__/plugin.test.ts`(测试文件按 AGENTS.md 免命名审批,但列入 `files_modified`)。若不允许新增测试,改为纯结构门禁(`orderPlugins` 与 `CoreState` 驱动的结构断言 + 既有调用点的运行时守卫断言)。**待用户确认**。"
|
||||
- "**OI-11(`logger.json` error 新增 76/77/78 的消息文本,须批准):** 提议在 `packages/common/src/logger.json` 的 `error` 段现有 `\"75\"` 之后**追加**三条(**不改动既有 75 及其它条目**),文本如下(**须用户批准后方可使用,不得擅自改动或扩写**):`\"76\": \"Plugin dependency cycle detected: $1. Cyclic plugins will not be initialized.\"`;`\"77\": \"Plugin $1 depends on a non-existent plugin: $2. Related plugins will not be initialized.\"`;`\"78\": \"Duplicate plugin id: $1. The duplicate will not be registered.\"`。调用约定:76 一次性以环上插件 id 逗号拼接传入 `$1`(`logger.error(76, [...cyclic].join(', '))`);77 对每个「直接引用不存在依赖」的插件逐条调用(`logger.error(77, pluginId, missingDepId)`);78 在注册表建表时对每个重复 id 调用(`logger.error(78, id)`)。**若用户对文本或调用粒度有指定,按其指定执行。**"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Phase 9 · 任务 10 · 第 20 个计划 —— **实现数据端插件驱动 + 在渲染端镜像一套一样的插件系统并实现(仅引擎侧)**。
|
||||
|
||||
**要解决的问题:** 用户已在 `packages-user/data-state/src/types.ts` 新增数据端插件类型(`PluginStatus` / `IDataPlugin<T>` / `IDataPluginDefine` / `ICoreState.getDataPlugin` / `ICoreState.load` / `ICoreStateConfig.plugins`),并在 `core.ts` 留下空壳(`awake` / `initialize` / `afterLoad` 空实现、`getDataPlugin` 引用未声明的 `this.plugins`、`load()` 返回空 Promise)。插件系统**机制本身尚未实现**:注册表未建立、四段式驱动未实现、依赖拓扑序与依赖环未处理;渲染端**尚无**对应的镜像(`IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / `clientPlugins`)。本计划把这套机制在**引擎侧**实现,并让渲染端镜像同一套系统(数据端先、渲染端后),**不接入口配置**(用户最后自行接线)。
|
||||
**要解决的问题:** 用户已在 `packages-user/data-state/src/types.ts` 新增数据端插件类型(`PluginStatus` / `IDataPlugin<T>` / `IDataPluginDefine` / `ICoreState.getDataPlugin` / `ICoreState.load` / `ICoreStateConfig.plugins`),并在 `core.ts` 留下空壳(`awake` / `initialize` / `afterLoad` 空实现、`getDataPlugin` 引用未声明的 `this.plugins`、`load()` 返回空 Promise)。插件系统**机制本身尚未实现**:注册表未建立、四段式驱动未实现、依赖拓扑序与插件错误处置(循环依赖 / 依赖不存在 / 重复 id)未处理;渲染端**尚无**对应的镜像(`IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / `clientPlugins`)。本计划把这套机制在**引擎侧**实现,并让渲染端镜像同一套系统(数据端先、渲染端后),**不接入口配置**(用户最后自行接线)。
|
||||
|
||||
**关键约束(运行时安全):** `config.plugins` / `config.clientPlugins` 由用户最后接线,而**现存全部 `CoreState` 调用点(`ins.ts:11` 及 ~9 处 test/fixture)与 `ClientCore` 调用点均未提供该字段**——故构造器建表必须用 `config.plugins ?? []` / `config.clientPlugins ?? []` **运行时守卫**,否则 `for … of undefined` 会抛错并破坏全部 `@user/data-state` 导入。既有调用点**不改动、不入 `files_modified`**;其既存类型不匹配属用户 WIP(D-56 / D-91),由用户接线时统一核对。
|
||||
|
||||
**插件错误处置(D-92,修订 D-87 的「复用 error 75」口径):** 一律使用 **`packages/common/src/logger.json` error 段新增码**——**循环依赖 → 76**、**依赖不存在的插件 → 77**、**重复 id → 78**;**重复 id 保留先注册者、不覆盖**;**报错的插件全部不进入加载流程**(`awake`/`initialize`/`afterLoad` 均不执行,`status` 保持 `NotAwakened`),**其下游依赖者亦不加载**。`logger.json` 现有 `75`(用户 WIP 已加)**保留不动**,本计划仅在其后追加 76/77/78(D-56)。
|
||||
|
||||
**大致解决方案(本计划 = 09-20):**
|
||||
- **Task 0(`checkpoint:decision@blocking-human`)= 预执行汇报关卡**:汇报要做什么 / 为什么 / 大致怎么做 / 影响范围(含四段式编排、拓扑序与环处置、运行时守卫及其理由、全部拟引入命名(含接口成员)、写操作边界、门禁方式、`open_items` OI-1..OI-10),停下等待用户回复**「可以执行」**。
|
||||
- **Task 1(`tracer`)= 数据端插件驱动端到端最小切片**:新增共享纯函数 `orderPlugins`(拓扑排序 + 环检测);在 `CoreState` 用 `config.plugins ?? []` 建立 `plugins` 注册表、实现 `awake` / `initialize` / `afterLoad` 四段式驱动与 `load()` 编排(第三段非空循环体驱动 `loader.start()`);`getDataPlugin` 从注册表读取;数据端以**运行期单测**(拓扑序、四段调用顺序、`PluginStatus` 推进、环排除、**省略 `plugins` 时 `load()` 不抛错**)验证端到端。
|
||||
- **Task 2 = 渲染端镜像 + 收口**:在 `client-modules/src/types.ts` 顶层新增 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin`(复用 `PluginStatus`);在 `client.ts` 的 `ClientCore` 用 `config.clientPlugins ?? []` 建立 `clientPlugins` 注册表、自 `@user/data-state` 复用 `orderPlugins`、实现 `getClientPlugin`、覆写 `awake` / `initialize` / `afterLoad` 使统一 `load()` 中数据端阶段先、渲染端阶段后;跑最终结构门禁与范围门禁并写 `09-20-SUMMARY.md`。
|
||||
- **Task 0(`checkpoint:decision@blocking-human`)= 预执行汇报关卡**:汇报要做什么 / 为什么 / 大致怎么做 / 影响范围(含四段式编排、拓扑序与错误处置、运行时守卫及其理由、全部拟引入命名(含接口成员)、`logger.json` 新增 76/77/78、写操作边界、门禁方式、`open_items` OI-1..OI-11),停下等待用户回复**「可以执行」**。
|
||||
- **Task 1(`tracer`)= 数据端插件驱动端到端最小切片**:在 `logger.json` error 段追加 76/77/78(保留 75);新增共享纯函数 `orderPlugins`(拓扑排序 + 环检测 + 依赖不存在检测);在 `CoreState` 用 `config.plugins ?? []` 建立 `plugins` 注册表(重复 id 报 78、不覆盖)、实现 `awake` / `initialize` / `afterLoad` 四段式驱动与 `load()` 编排(第三段非空循环体驱动 `loader.start()`;`awake` 阶段对 `orderPlugins` 结果报 76 / 77);`getDataPlugin` 从注册表读取;数据端以**运行期单测**(拓扑序、四段调用顺序、`PluginStatus` 推进、环排除、依赖不存在排除(含下游)、重复 id 保留原插件、**省略 `plugins` 时 `load()` 不抛错**)验证端到端。
|
||||
- **Task 2 = 渲染端镜像 + 收口**:在 `client-modules/src/types.ts` 顶层新增 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin`(复用 `PluginStatus`);在 `client.ts` 的 `ClientCore` 用 `config.clientPlugins ?? []` 建立 `clientPlugins` 注册表(重复 id 报 78、不覆盖)、自 `@user/data-state` 复用 `orderPlugins`、实现 `getClientPlugin`、覆写 `awake` / `initialize` / `afterLoad`(`super.` 先数据端后渲染端,渲染端错误处置报 76 / 77)使统一 `load()` 中数据端阶段先、渲染端阶段后;跑最终结构门禁与范围门禁并写 `09-20-SUMMARY.md`。
|
||||
|
||||
**为何分两个执行任务且 Task 1 为 tracer:** 本增量是**分层功能实现**——数据端驱动是所有后续逻辑的地基(渲染端只在其上镜像同阶段);Task 1 先把「一条路径」端到端打通(构造守卫 → 注册 → 拓扑排序 → 四阶段调用 → 状态推进 → 环排除),用**运行期单测**证明机制成立,Task 2 再在其上横向铺开渲染端镜像。渲染端因 `ClientCore` 依赖 canvas 无法在 Node 下运行期验证,故其门禁为结构断言。
|
||||
**为何分两个执行任务且 Task 1 为 tracer:** 本增量是**分层功能实现**——数据端驱动是所有后续逻辑的地基(渲染端只在其上镜像同阶段);Task 1 先把「一条路径」端到端打通(构造守卫 → 注册(含重复 id 处置)→ 拓扑排序 → 四阶段调用 → 状态推进 → 环 / 依赖不存在排除),用**运行期单测**证明机制成立,Task 2 再在其上横向铺开渲染端镜像。渲染端因 `ClientCore` 依赖 canvas 无法在 Node 下运行期验证,故其门禁为结构断言。
|
||||
|
||||
**修改方式声明(AGENTS.md):** 本计划执行含**文件写入**(新增 `plugin.ts` 与测试、改写 `core.ts` / `client-modules/src/types.ts` / `client-modules/src/client.ts`、新增桶导出行),将由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 所列路径外无任何写入。
|
||||
**修改方式声明(AGENTS.md):** 本计划执行含**文件写入**(新增 `plugin.ts` 与测试、改写 `core.ts` / `client-modules/src/types.ts` / `client-modules/src/client.ts`、新增桶导出行、在 `common/src/logger.json` 追加 76/77/78),将由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 所列路径外无任何写入。
|
||||
|
||||
**四段式 `load()` 编排(D-87,本规划已核实):** ① `awake`(**同步**,主对象创建后、初始化前)→ ② `initialize`(**异步、阻塞**,加载开始前)→ ③ 执行 loader 加载(驱动 `this.loader.start()` 至完成,`data-state/src/loader/loader.ts:84`;对齐 `loader.test.ts:163` 的 `for await ... of loader.start()`)→ ④ `afterLoad`(**异步、阻塞**,加载完毕后)。三阶段均按**依赖拓扑序**调用 `plugin.awake/initialize/afterLoad(this, deps)` 并在调用后推进 `status`。
|
||||
|
||||
**依赖拓扑序与依赖环(D-87):** 由 `orderPlugins` 一次性计算「可初始化的拓扑序」与「处于依赖环上(或依赖其)的插件 id」;环上及其下游插件在三个阶段均被跳过(`status` 保持 `NotAwakened`),并记录 `logger.error(75, id, info)`(复用既有 error 75,`packages/common/src/logger.json` **不改**)。
|
||||
**插件错误处置与日志码(D-92,修订 D-87):** 由 `orderPlugins` 一次性计算「可初始化的拓扑序」与「错误成因」(环上插件 id、直接引用不存在依赖的插件 id 及其缺失依赖);`awake` 阶段据此记录 `logger.error(76, …)`(环)与 `logger.error(77, …)`(依赖不存在);构造器建表时对重复 id 记录 `logger.error(78, …)` 且**保留先注册者、不覆盖**。**报错插件及其下游在三个阶段均被跳过**(`status` 保持 `NotAwakened`),不抛出、不中断 `load()`。
|
||||
|
||||
**数据端先、渲染端后(D-90):** 统一 `load()` 只保留一个公开入口(`CoreState.load()`,`ClientCore` 经继承/同阶段覆写复用);每个阶段先跑数据端插件(L0-L3),再由 `ClientCore` 的覆写跑渲染端插件(L4-L5),从而「数据插件先、渲染插件后,按同一四段式驱动」。
|
||||
|
||||
@ -129,166 +138,178 @@ Phase 9 · 任务 10 · 第 20 个计划 —— **实现数据端插件驱动 +
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报「任务 10 · 数据端插件驱动 + 渲染端插件系统镜像(仅引擎侧)」的要做什么 / 为什么 / 大致怎么做 / 影响范围,等待用户回复「可以执行」</name>
|
||||
<precondition>`packages-user/data-state/src/types.ts` 已含 `PluginStatus` / `IDataPlugin` / `IDataPluginDefine` / `ICoreState.getDataPlugin` / `ICoreState.load` / `ICoreStateConfig.plugins`(用户新增);`packages-user/data-state/src/core.ts` 已含 `awake` / `initialize` / `afterLoad` 空壳与引用 `this.plugins` 的 `getDataPlugin`;`packages-user/client-modules/src/types.ts` 已含 `IClientCoreConfig extends ICoreStateConfig` / `IClientCore extends IClientSystem`;`packages-user/client-modules/src/client.ts` 已含 `ClientCore extends CoreState implements IClientCore`;`packages/common/src/logger.json` 已含 `error 75`(Plugin $1 error...);`packages-user/data-state/src/index.ts` 与 `packages-user/client-modules/src/index.ts` 存在;现存 `new CoreState({...})` 调用点(`data-state/src/ins.ts:11` 等)与 `ClientCore` 调用点(`client-modules/src/core.ts:6`)均**未**提供 `plugins` / `clientPlugins`;`packages-user/data-state/src/__test__/plugin.test.ts` 与 `packages-user/data-state/src/plugin.ts` **尚不存在**。</precondition>
|
||||
<precondition>`packages-user/data-state/src/types.ts` 已含 `PluginStatus` / `IDataPlugin` / `IDataPluginDefine` / `ICoreState.getDataPlugin` / `ICoreState.load` / `ICoreStateConfig.plugins`(用户新增);`packages-user/data-state/src/core.ts` 已含 `awake` / `initialize` / `afterLoad` 空壳与引用 `this.plugins` 的 `getDataPlugin`;`packages-user/client-modules/src/types.ts` 已含 `IClientCoreConfig extends ICoreStateConfig` / `IClientCore extends IClientSystem`;`packages-user/client-modules/src/client.ts` 已含 `ClientCore extends CoreState implements IClientCore`;`packages/common/src/logger.json` 已含 error `75`(Plugin $1 error...,用户 WIP 添加);`packages-user/data-state/src/index.ts` 与 `packages-user/client-modules/src/index.ts` 存在;现存 `new CoreState({...})` 调用点(`data-state/src/ins.ts:11` 等)与 `ClientCore` 调用点(`client-modules/src/core.ts:6`)均**未**提供 `plugins` / `clientPlugins`;`packages-user/data-state/src/__test__/plugin.test.ts` 与 `packages-user/data-state/src/plugin.ts` **尚不存在**。</precondition>
|
||||
<files>(只读汇报,不修改任何文件)</files>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-86 / D-87 / D-88 / D-89 / D-90 / D-91;以及 D-01 / D-03 / D-04 / D-56)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-86 / D-87 / D-88 / D-89 / D-90 / D-91 / **D-92**;以及 D-01 / D-03 / D-04 / D-56)
|
||||
- packages-user/data-state/src/types.ts(`PluginStatus:10` / `IDataPlugin:21` / `IDataPluginDefine:28`(含「依赖环会抛出错误」注释:31)/ `ICoreStateConfig.plugins:80` / `ICoreState.getDataPlugin:105` / `ICoreState.load:112`)
|
||||
- packages-user/data-state/src/core.ts(现状:`awake:148` / `initialize:153` / `afterLoad:158` 空壳、`getDataPlugin:160` 引用未声明 `this.plugins`、`load:170` 空实现;`addSaveableContent:237` 的重复 id 处置风格;constructor:117-141)
|
||||
- packages-user/data-state/src/loader/loader.ts:84-150(`start()` / `loaded()`);packages-user/data-state/src/loader/__test__/loader.test.ts:163(`for await ... of loader.start()`)
|
||||
- packages-user/data-state/src/ins.ts:11(模块级单例 `new CoreState({ loadStarter, coreURL })`,**未提供 `plugins`**);packages-user/data-state/__test__/coreNode.test.ts:8(同类未提供 `plugins` 的 test 调用点)
|
||||
- packages-user/client-modules/src/types.ts(`IClientCoreConfig:4` / `IClientCore:13`);packages-user/client-modules/src/client.ts(`ClientCore:47`、`super(...):67-70`,**未提供 `clientPlugins` / `plugins`**)
|
||||
- packages-user/client-modules/src/types.ts(`IClientCoreConfig:4` / `IClientCore:13`);packages-user/client-modules/src/client.ts(`ClientCore:47`、`super(...):67-70`,**未提供 `clientPlugins` / `plugins`)
|
||||
- packages-user/client-system/src/types.ts:3(`IClientSystem extends IClientBase`);packages-user/client-base/src/types.ts:21(`IClientBase extends ICoreState`)
|
||||
- packages/common/src/logger.json(error `75`);eslint.config.js(`eslint.configs.recommended` → 启用 `no-empty`);AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明);dev.md(命名规则、对象类型须声明 interface、模块无副作用、无循环引用)
|
||||
- packages/common/src/logger.ts:117(`error(code: number, ...params: string[])`);packages/common/src/logger.json(error 段 **现状:`75` 为用户 WIP 已加;尚无 76/77/78**);packages-user/client-modules/src/loader/hook.ts:1(`client-modules` 已自 `@motajs/common` 导入 `logger` 的先例)
|
||||
- eslint.config.js(`eslint.configs.recommended` → 启用 `no-empty`);AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明);dev.md(命名规则、对象类型须声明 interface、模块无副作用、无循环引用)
|
||||
</read_first>
|
||||
<action>
|
||||
**停下并向用户汇报,等待用户回复「可以执行」后才继续;汇报本身只读。**
|
||||
|
||||
① **要解决的问题:** 数据端插件类型已由用户新增,但插件系统**机制**尚未实现(注册表缺失、四段式驱动缺失、拓扑序/环未处理、`getDataPlugin` 引用了未声明的 `this.plugins`、`load()` 为空);渲染端尚无镜像。本计划在**引擎侧**实现数据端驱动,并在渲染端镜像一套一样的系统(数据端先、渲染端后),**不接入口配置**。
|
||||
① **要解决的问题:** 数据端插件类型已由用户新增,但插件系统**机制**尚未实现(注册表缺失、四段式驱动缺失、拓扑序与插件错误处置未处理、`getDataPlugin` 引用了未声明的 `this.plugins`、`load()` 为空);渲染端尚无镜像。本计划在**引擎侧**实现数据端驱动,并在渲染端镜像一套一样的系统(数据端先、渲染端后),**不接入口配置**。
|
||||
|
||||
② **范围(D-86):** 仅插件系统机制——数据端 `CoreState` 注册表 + 四段式驱动 + `load()` 编排 + 依赖拓扑/环;渲染端 `IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / `clientPlugins` 镜像 + `ClientCore` 实现。**不含**:入口/接线(`src/**`)、`initializeL0..L3` 等 config 口径、其它并发 WIP。
|
||||
② **范围(D-86):** 仅插件系统机制——数据端 `CoreState` 注册表 + 四段式驱动 + `load()` 编排 + 依赖拓扑/错误处置;渲染端 `IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / `clientPlugins` 镜像 + `ClientCore` 实现。**不含**:入口/接线(`src/**`)、`initializeL0..L3` 等 config 口径、其它并发 WIP。
|
||||
|
||||
③ **数据端设计(D-87):** 构造器由 `config.plugins` 建注册表(每项 `{ status: PluginStatus.NotAwakened, plugin: define }`);`awake`(同步)→ `initialize`(异步阻塞)→ 执行 loader 加载(驱动 `this.loader.start()`)→ `afterLoad`(异步阻塞);各阶段按**依赖拓扑序**调用 `plugin.awake/initialize/afterLoad(this, deps)` 并推进 `status`;`getDataPlugin` 从注册表读取。
|
||||
|
||||
④ **运行时守卫(关键理由,D-91 / D-56):** 由于现存 `CoreState` 调用点(`data-state/src/ins.ts:11` 及 ~9 处 test/fixture)与 `ClientCore` 调用点(`client-modules/src/core.ts:6`)**均未提供** `plugins` / `clientPlugins`,构造器建表必须以 `config.plugins ?? []` / `config.clientPlugins ?? []` **运行时守卫**——否则 `for … of undefined` 抛错,破坏全部 `@user/data-state` 导入。既有调用点**不纳入 `files_modified`、不改动**;其 `ICoreStateConfig` 必填字段(`plugins` / `heroFace` / `events`)缺失属**用户 WIP**,本计划**不改 `data-state/src/types.ts`**,由用户后续接线时统一核对。
|
||||
|
||||
⑤ **依赖拓扑序与环:** 由共享纯函数 `orderPlugins` 计算「可初始化拓扑序」与「环上(含下游)插件 id」;环上插件三阶段均跳过、`status` 保持 `NotAwakened`,记录 `logger.error(75, id, info)`(**复用既有 error 75,不改 `logger.json`**)。
|
||||
⑤ **插件错误处置与日志码(D-92,修订 D-87):** 由共享纯函数 `orderPlugins` 计算「可初始化拓扑序」与「错误成因」;**循环依赖 → `logger.error(76, …)`**、**依赖不存在的插件 → `logger.error(77, …)`**、**重复 id → `logger.error(78, …)` 且不覆盖原插件**。**报错插件全部不进入加载流程**(`awake`/`initialize`/`afterLoad` 均不执行,`status` 保持 `NotAwakened`),**其下游依赖者亦不加载**;不抛出、不中断 `load()`。码 76/77/78 将**新增**到 `packages/common/src/logger.json` 的 error 段(**保留既有条目尤其用户 WIP 的 `75` 不动**);**不再复用 `error 75`**。
|
||||
|
||||
⑥ **渲染端镜像(D-88):** 类型放 `packages-user/client-modules/src/types.ts` **顶层**(与数据端对齐):`IClientPlugin` / `IClientPluginDefine`、`IClientCoreConfig.clientPlugins`、`IClientCore.getClientPlugin`;**复用数据端 `PluginStatus`**(自 `@user/data-state` 导入,不自建枚举);实现在 `ClientCore`。渲染端 `orderPlugins` **自 `@user/data-state` 导入**(`client-modules` 内不存在含该导出的本地模块)。
|
||||
⑥ **渲染端镜像(D-88):** 类型放 `packages-user/client-modules/src/types.ts` **顶层**(与数据端对齐):`IClientPlugin` / `IClientPluginDefine`、`IClientCoreConfig.clientPlugins`、`IClientCore.getClientPlugin`;**复用数据端 `PluginStatus`**(自 `@user/data-state` 导入,不自建枚举);实现在 `ClientCore`。渲染端 `orderPlugins` **自 `@user/data-state` 导入**(`client-modules` 内不存在含该导出的本地模块)。渲染端**镜像同一错误处置**(重复 id 报 78、环报 76、依赖不存在报 77;报错插件及其下游不进入加载流程)。
|
||||
|
||||
⑦ **统一 `load()`(D-89 / D-90):** 渲染端 `awake` / `initialize` / `afterLoad` 语义与数据端一致;**数据端阶段先、渲染端阶段后**——每阶段先驱动数据插件(L0-L3),再驱动渲染插件(L4-L5)。`load()` 第三段的 `for await` 循环体**非空**(消费进度值),以符合 eslint `no-empty`。
|
||||
|
||||
⑧ **拟引入命名(须报备批准,OI-1..OI-4):** 数据端注册表字段 `plugins`(OI-2);共享模块 `packages-user/data-state/src/plugin.ts` 及其导出 `orderPlugins` / `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`(OI-1),**含成员命名**(D-04 覆盖成员)——`IPluginDepends` 成员 `id` / `dependencies`;`IPluginHolder` 成员 `plugin`;`IPluginOrderResult` 成员 `order` / `cyclic`;`orderPlugins` 参数名 `plugins`;渲染端注册表字段 `clientPlugins`(OI-3);`CoreState.awake` / `initialize` / `afterLoad` 由 `private` 改为 `protected`(供 `ClientCore` 覆写,OI-4)。**D-88 已批准可直接使用:** `IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / 配置字段 `clientPlugins`。
|
||||
⑧ **拟引入命名(须报备批准,OI-1..OI-4):** 数据端注册表字段 `plugins`(OI-2);共享模块 `packages-user/data-state/src/plugin.ts` 及其导出 `orderPlugins` / `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`(OI-1),**含成员命名**(D-04 覆盖成员)——`IPluginDepends` 成员 `id` / `dependencies`;`IPluginHolder` 成员 `plugin`;`IPluginOrderResult` 成员 `order` / `cyclic` / **`missing`**;`orderPlugins` 参数名 `plugins`;渲染端注册表字段 `clientPlugins`(OI-3);`CoreState.awake` / `initialize` / `afterLoad` 由 `private` 改为 `protected`(供 `ClientCore` 覆写,OI-4)。**D-88 已批准可直接使用:** `IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / 配置字段 `clientPlugins`。
|
||||
|
||||
⑨ **待裁定项(OI-5..OI-10):** 渲染插件 `state` 参数类型 `IClientCore`(提议)vs `IClientBase`(OI-5);依赖环处置 = `logger.error(75,...)` + 环上排除、不抛出(提议;类型注释字面为「抛出错误」,OI-6);依赖 id 不存在时忽略(OI-7);重复插件 id 后者覆盖、不告警(OI-8);是否把 `config` 传入 `ClientCore` 的 `super(...)`(默认不改,OI-9);是否允许新增测试文件 `data-state/src/__test__/plugin.test.ts`(OI-10)。
|
||||
⑨ **待裁定项(OI-5 / OI-9 / OI-10 仍待;OI-6 / OI-7 / OI-8 已由 D-92 裁定,OI-11 为新增消息审批):** 渲染插件 `state` 参数类型 `IClientCore`(提议)vs `IClientBase`(OI-5);是否把 `config` 传入 `ClientCore` 的 `super(...)`(默认不改,OI-9);是否允许新增测试文件 `data-state/src/__test__/plugin.test.ts`(OI-10);**`logger.json` 新增 76/77/78 的确切消息文本与调用粒度**(OI-11,须批准)。**已裁定的语义(D-92)**:环 → 76 不抛出;依赖不存在 → 77;重复 id → 78 且不覆盖;报错插件及其下游全部不加载。
|
||||
|
||||
⑩ **产出文件:** 新增 `packages-user/data-state/src/plugin.ts`、`packages-user/data-state/src/__test__/plugin.test.ts`;改写 `packages-user/data-state/src/core.ts`、`packages-user/data-state/src/index.ts`(桶导出)、`packages-user/client-modules/src/types.ts`、`packages-user/client-modules/src/client.ts`;新增 `09-20-SUMMARY.md`。
|
||||
⑩ **产出文件:** 新增 `packages-user/data-state/src/plugin.ts`、`packages-user/data-state/src/__test__/plugin.test.ts`;改写 `packages-user/data-state/src/core.ts`、`packages-user/data-state/src/index.ts`(桶导出)、`packages-user/client-modules/src/types.ts`、`packages-user/client-modules/src/client.ts`;在 `packages/common/src/logger.json` 的 error 段追加 76/77/78;新增 `09-20-SUMMARY.md`。
|
||||
|
||||
⑪ **不在本增量(D-86 / D-91):** 不接入口/配置(`src/**`);不改 `initializeL0..L3` 等 config 口径;不处理 `initializeEnemy` / `initializeSave` / `initializeReplay` 的接线;不新增依赖、不改 `package.json` / lockfile / `logger.json`。
|
||||
⑪ **不在本增量(D-86 / D-91):** 不接入口/配置(`src/**`);不改 `initializeL0..L3` 等 config 口径;不处理 `initializeEnemy` / `initializeSave` / `initializeReplay` 的接线;不新增依赖、不改 `package.json` / lockfile、**不改 `logger.json` 既有条目**(仅追加 76/77/78)。
|
||||
|
||||
⑫ **已知后果(D-91,须一并告知并记入 `09-20-SUMMARY.md`):** 渲染端调用点(`client-modules/src/core.ts`、`src/client/entry.ts`)尚不传 `clientPlugins`,数据端 `ins.ts` 尚不传 `plugins`,故「数据先渲染后」的顺序在**用户接线前是空转的**(注册表为空、无插件可驱动);本计划只保证机制成立且既有调用点不因缺配置而崩溃。
|
||||
|
||||
⑬ **验证方式:** 数据端 = **运行期单测**(`orderPlugins` 拓扑序与环检测;`CoreState.load()` 的四段调用顺序与 `PluginStatus` 推进;环上插件不初始化;**省略 `plugins` 时 `new CoreState({ loadStarter, coreURL } as any)` + `await state.load()` 不抛错,证明运行时守卫**)——`pnpm exec vitest run packages-user/data-state/src/__test__/plugin.test.ts`;两端 = **结构断言**(`plugin.ts` 导出 `orderPlugins`;`core.ts` 含 `plugins` 注册表 / `config.plugins ?? []` 守卫 / `protected` 三阶段 / `load()` 四段(非空 `for await` 循环体)/ `PluginStatus` 推进 / `logger.error(75`;`client-modules/src/types.ts` 含 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin` 且 `PluginStatus` 自 `@user/data-state` 导入、未自建;`client.ts` 含 `clientPlugins` 注册表 / `config.clientPlugins ?? []` 守卫 / `getClientPlugin` / 三阶段覆写 / `orderPlugins` 自 `@user/data-state` 导入);**方向/环**结构断言(`data-state/src/**` 不导入 `@user/client-modules` / `@user/client-base` / `@user/client-system`);**范围门禁**(`git status` 仅 `files_modified` 变更,用户 WIP 零触碰)。**不设全仓 `check:type` / `build` / 全量 `test:ci` 门禁。**
|
||||
⑬ **验证方式:** 数据端 = **运行期单测**(`orderPlugins` 拓扑序、环检测、依赖不存在检测;`CoreState.load()` 的四段调用顺序与 `PluginStatus` 推进;环上插件与依赖不存在插件(含下游)均不初始化;重复 id 保留原插件;**省略 `plugins` 时 `new CoreState({ loadStarter, coreURL } as any)` + `await state.load()` 不抛错,证明运行时守卫**)——`pnpm exec vitest run packages-user/data-state/src/__test__/plugin.test.ts`;两端 = **结构断言**(`plugin.ts` 导出 `orderPlugins` / 三接口(含 `missing` 成员);`core.ts` 含 `plugins` 注册表 / `config.plugins ?? []` 守卫 / 重复 id 判重(`this.plugins.has`)且 `logger.error(78` / `protected` 三阶段 / `load()` 四段(非空 `for await` 循环体)/ `PluginStatus` 推进 / `logger.error(76` / `logger.error(77` 且无 `logger.error(75`;`logger.json` error 段含 76/77/78 且保留 75;`client-modules/src/types.ts` 含 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin` 且 `PluginStatus` 自 `@user/data-state` 导入、未自建;`client.ts` 含 `clientPlugins` 注册表 / `config.clientPlugins ?? []` 守卫 / `getClientPlugin` / 三阶段覆写 / `orderPlugins` 自 `@user/data-state` 导入 / `logger.error(76` / `logger.error(77` / `logger.error(78`);**方向/环**结构断言(`data-state/src/**` 不导入 `@user/client-modules` / `@user/client-base` / `@user/client-system`);**范围门禁**(`git status` 仅 `files_modified` 变更,用户 WIP 零触碰)。**不设全仓 `check:type` / `build` / 全量 `test:ci` 门禁。**
|
||||
|
||||
⑭ **修改方式声明(AGENTS.md):** 本计划执行含文件写入,由**派发子代理执行**(`gsd-executor`),非内联执行。
|
||||
|
||||
⑮ **关卡性质:** **无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户回复;回复「可以执行」即视为通过。** 若用户就 OI-1..OI-10 给出指定,按指定执行。
|
||||
⑮ **关卡性质:** **无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户回复;回复「可以执行」即视为通过。** 若用户就 OI-1..OI-11 给出指定,按指定执行。
|
||||
</action>
|
||||
<decision>是否按本计划(09-20)执行「任务 10 · 数据端插件驱动 + 渲染端插件系统镜像(仅引擎侧)」——在 `CoreState` 用 `config.plugins ?? []` 实现注册表 + 四段式驱动(`awake` 同步 → `initialize` 异步阻塞 → loader 加载 → `afterLoad` 异步阻塞)+ 依赖拓扑序与环处置 + `load()` 编排;在渲染端 `client-modules/src/types.ts` 顶层新增 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin`(复用 `PluginStatus`)并在 `ClientCore` 用 `config.clientPlugins ?? []` 实现、自 `@user/data-state` 复用 `orderPlugins`,使统一 `load()` 中数据端阶段先、渲染端阶段后;不接入口/配置、不改 config 口径、不新增依赖、不动用户并发 WIP。</decision>
|
||||
<decision>是否按本计划(09-20)执行「任务 10 · 数据端插件驱动 + 渲染端插件系统镜像(仅引擎侧)」——在 `CoreState` 用 `config.plugins ?? []` 实现注册表(重复 id 报 `logger.error(78,...)` 且不覆盖)+ 四段式驱动(`awake` 同步 → `initialize` 异步阻塞 → loader 加载 → `afterLoad` 异步阻塞)+ 依赖拓扑序与错误处置(环报 `logger.error(76,...)`、依赖不存在报 `logger.error(77,...)`,报错插件及其下游全部不加载)+ `load()` 编排,并在 `packages/common/src/logger.json` error 段新增 76/77/78(保留既有 75);在渲染端 `client-modules/src/types.ts` 顶层新增 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin`(复用 `PluginStatus`)并在 `ClientCore` 用 `config.clientPlugins ?? []` 实现、自 `@user/data-state` 复用 `orderPlugins`,使统一 `load()` 中数据端阶段先、渲染端阶段后;不接入口/配置、不改 config 口径、不新增依赖、不动用户其它并发 WIP。</decision>
|
||||
<context>
|
||||
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期为**写操作**(新增 `plugin.ts` 与测试、改写 `core.ts` / `types.ts` / `client.ts` / `index.ts`)。本计划拟引入若干新命名(`plugins` / `plugin.ts` / `orderPlugins` / 三个接口**及其成员** / `clientPlugins` 注册表字段)与一处可见性变更(`private`→`protected`),**均须在本关卡报备待批准**(OI-1..OI-4);另有 OI-5..OI-10 待裁定。执行期工作树存在用户并发改动(`09-CONTEXT.md`、`index.html`、`client-modules/src/{index,types}.ts`、`data-base/hero/*`、`data-state/src/{core,types}.ts`、`common/logger.json`、新增 `src/bootstrap.ts` / `src/client/` / `src/data/` / `src/constant.ts`,D-56),提交时须逐条核对暂存面、只暂存本计划 `files_modified` 条目。既有 `CoreState` / `ClientCore` 调用点未提供插件配置,其既存类型不匹配属用户 WIP(D-56),本计划以运行时守卫保证其不崩溃、不改动它们、不改 `data-state/src/types.ts`。渲染端因 `ClientCore` 依赖 canvas 无法在 Node 下运行期验证,故以结构断言为门禁。行号为规划日实测、可能漂移,门禁以结构/行为断言为准。
|
||||
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期为**写操作**(新增 `plugin.ts` 与测试、改写 `core.ts` / `types.ts` / `client.ts` / `index.ts`、追加 `logger.json` 76/77/78)。本计划拟引入若干新命名(`plugins` / `plugin.ts` / `orderPlugins` / 三个接口**及其成员**(含 `missing`)/ `clientPlugins` 注册表字段)与一处可见性变更(`private`→`protected`),**均须在本关卡报备待批准**(OI-1..OI-4);另有 OI-5/OI-9/OI-10 待裁定、OI-11 消息文本待批准。执行期工作树存在用户并发改动(`09-CONTEXT.md`、`index.html`、`client-modules/src/{index,types}.ts`、`data-base/hero/*`、`data-state/src/{core,types}.ts`、`common/logger.json`(error 75)、新增 `src/bootstrap.ts` / `src/client/` / `src/data/` / `src/constant.ts`,D-56),提交时须逐条核对暂存面、只暂存本计划 `files_modified` 条目。既有 `CoreState` / `ClientCore` 调用点未提供插件配置,其既存类型不匹配属用户 WIP(D-56),本计划以运行时守卫保证其不崩溃、不改动它们、不改 `data-state/src/types.ts`。渲染端因 `ClientCore` 依赖 canvas 无法在 Node 下运行期验证,故以结构断言为门禁。行号为规划日实测、可能漂移,门禁以结构/行为断言为准。
|
||||
</context>
|
||||
<options>
|
||||
<option id="execute-default">
|
||||
<name>可以执行(按本计划默认口径:`IClientCore` 为渲染 `state` 参数类型、环处置为记录 `logger.error(75,...)` + 环上排除不抛出、依赖 id 不存在时忽略、重复 id 后者覆盖不告警、不改 `super(...)`、允许新增测试文件、构造器以 `config.plugins ?? []` / `config.clientPlugins ?? []` 运行时守卫)</name>
|
||||
<pros>四段式编排与数据端先/渲染端后与 D-87/D-89/D-90 逐字一致;拓扑/环机制有运行期单测兜底;运行时守卫保证既有调用点不崩、符合 D-91「用户最后接线」的时序;复用既有 error 75、不触碰受保护的 `logger.json`;仅引擎侧、不动用户 WIP</pros>
|
||||
<cons>引入若干待批准命名(`plugins` / `plugin.ts` / `orderPlugins` / 三接口及其成员 / `clientPlugins` 注册表字段)与一处 `private`→`protected` 可见性变更</cons>
|
||||
<name>可以执行(按本计划默认口径:`IClientCore` 为渲染 `state` 参数类型、错误处置为环报 `logger.error(76,...)` / 依赖不存在报 `logger.error(77,...)` / 重复 id 报 `logger.error(78,...)` 且不覆盖、报错插件及其下游全部不加载、不改 `super(...)`、允许新增测试文件、构造器以 `config.plugins ?? []` / `config.clientPlugins ?? []` 运行时守卫、`logger.json` error 段追加 76/77/78(保留 75)、消息文本按 OI-11 提议)</name>
|
||||
<pros>四段式编排与数据端先/渲染端后与 D-87/D-89/D-90 逐字一致;插件错误处置与 D-92 逐字一致(76/77/78 + 不覆盖 + 排除加载流程);拓扑/环/依赖不存在/重复 id 机制有运行期单测兜底;运行时守卫保证既有调用点不崩、符合 D-91「用户最后接线」的时序;错误码集中、不新增依赖;仅引擎侧、不动用户其它 WIP</pros>
|
||||
<cons>引入若干待批准命名(`plugins` / `plugin.ts` / `orderPlugins` / 三接口及其成员(含 `missing`)/ `clientPlugins` 注册表字段)与一处 `private`→`protected` 可见性变更;新增 logger 码 76/77/78(消息文本待批准)</cons>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>修订 OI-1..OI-10 的任一取向后重新汇报</name>
|
||||
<pros>避免带着未确认的命名 / 类型 / 语义落码</pros>
|
||||
<name>修订 OI-1..OI-11 的任一取向后重新汇报</name>
|
||||
<pros>避免带着未确认的命名 / 类型 / 语义 / 日志文本落码</pros>
|
||||
<cons>须修订 PLAN.md 后重新汇报</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>回复「可以执行」即可(默认采用 OI-5=`IClientCore`、OI-6=`logger.error(75,...)`+不抛出、OI-7=忽略、OI-8=后者覆盖不告警、OI-9=不改 `super(...)`、OI-10=允许新增测试)。如需调整任一 `open_items`,请在回复中一并指明。</resume-signal>
|
||||
<resume-signal>回复「可以执行」即可(默认采用 OI-5=`IClientCore`、OI-6/7/8=按 D-92(环 76 / 依赖不存在 77 / 重复 id 78 且不覆盖)、OI-9=不改 `super(...)`、OI-10=允许新增测试、OI-11=按提议消息文本)。如需调整任一 `open_items`,请在回复中一并指明。</resume-signal>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const must=['packages-user/data-state/src/types.ts','packages-user/data-state/src/core.ts','packages-user/data-state/src/index.ts','packages-user/data-state/src/loader/loader.ts','packages-user/data-state/src/ins.ts','packages-user/client-modules/src/types.ts','packages-user/client-modules/src/client.ts','packages-user/client-system/src/types.ts','packages-user/client-base/src/types.ts','packages/common/src/logger.json'];for(const p of must){if(!fs.existsSync(p))bad.push('missing ref: '+p)}const ts=fs.readFileSync('packages-user/data-state/src/types.ts','utf8');for(const tok of ['PluginStatus','IDataPlugin','IDataPluginDefine','getDataPlugin'])if(ts.indexOf(tok)<0)bad.push('types.ts missing '+tok);const core=fs.readFileSync('packages-user/data-state/src/core.ts','utf8');if(core.indexOf('this.plugins')<0)bad.push('core.ts does not reference this.plugins');if(core.indexOf('getDataPlugin')<0)bad.push('core.ts missing getDataPlugin');const lg=fs.readFileSync('packages/common/src/logger.json','utf8');if(lg.indexOf('Plugin ') <0)bad.push('logger.json missing plugin error code');const ins=fs.readFileSync('packages-user/data-state/src/ins.ts','utf8');if(ins.indexOf('new CoreState(')<0)bad.push('ins.ts: expected existing module-level CoreState singleton (no plugins)');for(const p of ['packages-user/data-state/src/plugin.ts','packages-user/data-state/src/__test__/plugin.test.ts']){if(fs.existsSync(p))bad.push('post-state already exists: '+p)}if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state: plugin types + core shells + logger error 75 + unconfigured call sites present; plugin.ts and plugin.test.ts absent')"</automated>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const must=['packages-user/data-state/src/types.ts','packages-user/data-state/src/core.ts','packages-user/data-state/src/index.ts','packages-user/data-state/src/loader/loader.ts','packages-user/data-state/src/ins.ts','packages-user/client-modules/src/types.ts','packages-user/client-modules/src/client.ts','packages-user/client-system/src/types.ts','packages-user/client-base/src/types.ts','packages/common/src/logger.json'];for(const p of must){if(!fs.existsSync(p))bad.push('missing ref: '+p)}const ts=fs.readFileSync('packages-user/data-state/src/types.ts','utf8');for(const tok of ['PluginStatus','IDataPlugin','IDataPluginDefine','getDataPlugin'])if(ts.indexOf(tok)<0)bad.push('types.ts missing '+tok);const core=fs.readFileSync('packages-user/data-state/src/core.ts','utf8');if(core.indexOf('this.plugins')<0)bad.push('core.ts does not reference this.plugins');if(core.indexOf('getDataPlugin')<0)bad.push('core.ts missing getDataPlugin');const lg=fs.readFileSync('packages/common/src/logger.json','utf8');if(lg.indexOf('Plugin ') <0)bad.push('logger.json missing plugin error code (expect user WIP error 75)');if(!/\"75\"\s*:/.test(lg))bad.push('logger.json missing existing error 75');const ins=fs.readFileSync('packages-user/data-state/src/ins.ts','utf8');if(ins.indexOf('new CoreState(')<0)bad.push('ins.ts: expected existing module-level CoreState singleton (no plugins)');for(const p of ['packages-user/data-state/src/plugin.ts','packages-user/data-state/src/__test__/plugin.test.ts']){if(fs.existsSync(p))bad.push('post-state already exists: '+p)}if(bad.length>0){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK pre-execution state: plugin types + core shells + logger error 75 (user WIP) + unconfigured call sites present; plugin.ts and plugin.test.ts absent')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "PRESTATE FAILED"(插件类型 / core 空壳 / logger error 75 / 未配置插件配置的既有调用点任一缺失,或 `plugin.ts` / `plugin.test.ts` 已存在)</fails_when>
|
||||
<human-check>用户在对话中明确回复「可以执行」(若就 OI-1..OI-10 给出非默认指定,须在汇报与后续任务中据其指定执行)</human-check>
|
||||
<human-check>用户在对话中明确回复「可以执行」(若就 OI-1..OI-11 给出非默认指定,须在汇报与后续任务中据其指定执行)</human-check>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 用户回复中明确包含「可以执行」
|
||||
- 汇报内容涵盖:要解决的问题、范围(数据端驱动 + 渲染端镜像、仅引擎侧)、四段式 `load()` 编排(含第三段非空循环体驱动 `loader.start()`)、依赖拓扑序与环处置、数据端注册表 `plugins` 与 `getDataPlugin`、**运行时守卫(`config.plugins ?? []` / `config.clientPlugins ?? []`)及其理由(既有调用点未提供配置、属用户 WIP)**、渲染端镜像命名与 `ClientCore` 落点、渲染 `state` 参数类型待定、统一 `load()` 数据端先/渲染端后、**全部拟引入命名(含接口成员)与 `private`→`protected` 可见性变更**、产出文件清单、不在本增量的内容、**用户接线前「数据先渲染后」为空转的已知后果**、验证方式(运行期单测含守卫断言 + 结构断言 + 范围门禁,无全仓门禁)、OI-1..OI-10
|
||||
- 汇报中已原样告知:**写操作边界(除 `files_modified` 外不写;不改 `src/**` / `data-base/**` / `client-base/**` / `common/**` / `package.json` / lockfile / 用户并发 WIP / `data-state/src/types.ts`;`.planning/**` 仅新增 `09-20-SUMMARY.md`)**、**不新增依赖 / 不改 `logger.json`(复用 error 75)**、**未经批准的命名一律不得使用**、**用户并发改动须保留(编辑同文件时在其现有内容之上追加)**、**既有未配置 `plugins` 的调用点不改动、其类型不匹配属用户 WIP(D-56 / D-91)**
|
||||
- 汇报内容涵盖:要解决的问题、范围(数据端驱动 + 渲染端镜像、仅引擎侧)、四段式 `load()` 编排(含第三段非空循环体驱动 `loader.start()`)、依赖拓扑序与插件错误处置(环 76 / 依赖不存在 77 / 重复 id 78 且不覆盖 + 报错插件及其下游全部不加载)、数据端注册表 `plugins` 与 `getDataPlugin`、**运行时守卫(`config.plugins ?? []` / `config.clientPlugins ?? []`)及其理由(既有调用点未提供配置、属用户 WIP)**、**`logger.json` 新增 error 76/77/78(保留既有 75 不动)**、渲染端镜像命名与 `ClientCore` 落点、渲染 `state` 参数类型待定、统一 `load()` 数据端先/渲染端后、**全部拟引入命名(含接口成员(含 `missing`))与 `private`→`protected` 可见性变更**、产出文件清单、不在本增量的内容、**用户接线前「数据先渲染后」为空转的已知后果**、验证方式(运行期单测含守卫断言 + 结构断言 + 范围门禁,无全仓门禁)、OI-1..OI-11
|
||||
- 汇报中已原样告知:**写操作边界(除 `files_modified` 外不写;不改 `src/**` / `data-base/**` / `client-base/**` / `package.json` / lockfile / 用户其它并发 WIP / `data-state/src/types.ts`;`packages/common/**` 中**仅**允许对 `src/logger.json` error 段追加 76/77/78、不得改动既有条目;`.planning/**` 仅新增 `09-20-SUMMARY.md`)**、**不新增依赖**、**未经批准的命名一律不得使用**、**用户并发改动须保留(编辑同文件时在其现有内容之上追加)**、**既有未配置 `plugins` 的调用点不改动、其类型不匹配属用户 WIP(D-56 / D-91)**
|
||||
- 汇报中已原样告知**关卡性质**(必须停下等待;不因触发方式或 `yolo` / `auto_advance` 自动放行;回复「可以执行」即通过)
|
||||
</acceptance_criteria>
|
||||
<done>用户明确回复「可以执行」;在此之前未执行任何任务、未修改任何文件、未派发任何执行子代理</done>
|
||||
</task>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: 数据端插件驱动端到端最小切片 —— `orderPlugins`(拓扑序 + 环检测)+ `CoreState` 注册表(守卫)/ 四段式 / `load()` 编排 + 运行期单测</name>
|
||||
<precondition>Task 0 已闭环(用户回复「可以执行」;OI-1 / OI-2 / OI-4 / OI-6 / OI-7 / OI-8 / OI-10 已获批或按用户指定)。`packages-user/data-state/src/types.ts` 的插件类型与 `ICoreStateConfig.plugins` 已就位;`packages-user/data-state/src/core.ts` 已含空壳;既有调用点(`ins.ts:11` 等)**未提供 `plugins`**,故构造器建表必须以 `config.plugins ?? []` 守卫。</precondition>
|
||||
<files>packages-user/data-state/src/plugin.ts, packages-user/data-state/src/core.ts, packages-user/data-state/src/index.ts, packages-user/data-state/src/__test__/plugin.test.ts</files>
|
||||
<name>Task 1: 数据端插件驱动端到端最小切片 —— `logger.json` 新增 76/77/78 + `orderPlugins`(拓扑序 + 环检测 + 依赖不存在检测)+ `CoreState` 注册表(守卫)/ 四段式 / `load()` 编排 + 运行期单测</name>
|
||||
<precondition>Task 0 已闭环(用户回复「可以执行」;OI-1 / OI-2 / OI-4 / OI-10 / OI-11 已获批或按用户指定)。`packages-user/data-state/src/types.ts` 的插件类型与 `ICoreStateConfig.plugins` 已就位;`packages-user/data-state/src/core.ts` 已含空壳;`packages/common/src/logger.json` 的 error 段已含用户 WIP 的 `75`;既有调用点(`ins.ts:11` 等)**未提供 `plugins`**,故构造器建表必须以 `config.plugins ?? []` 守卫。</precondition>
|
||||
<files>packages-user/data-state/src/plugin.ts, packages-user/data-state/src/core.ts, packages-user/data-state/src/index.ts, packages-user/data-state/src/__test__/plugin.test.ts, packages/common/src/logger.json</files>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-87 / D-91;D-04 / D-56)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-87 / D-91 / **D-92**;D-04 / D-56)
|
||||
- packages-user/data-state/src/types.ts(`PluginStatus:10` / `IDataPlugin:21` / `IDataPluginDefine:28`;`ICoreStateConfig.plugins:80` / `ICoreState.getDataPlugin:105` / `ICoreState.load:112`)
|
||||
- packages-user/data-state/src/core.ts(现状全文:import 区、Layer 定义、`awake:148` / `initialize:153` / `afterLoad:158` 空壳、`getDataPlugin:160`、`load:170`、`addSaveableContent:237`(重复 id 处置风格)、constructor:117-141)
|
||||
- packages-user/data-state/src/index.ts(桶导出:需补 `export * from './plugin'`)
|
||||
- packages-user/data-state/src/loader/loader.ts:84-150(`start()` / `loaded()`);packages-user/data-state/src/loader/__test__/loader.test.ts:146-175(`new CoreState` 同级测试范式:`for await ... of loader.start()`)
|
||||
- packages-user/data-state/__test__/coreNode.test.ts:7-12(既有 `new CoreState({ loadStarter, coreURL })` 构造范式,**未提供 `plugins`**);packages-user/data-state/src/ins.ts:11(模块级单例,**未提供 `plugins`**);packages-user/data-system/src/event/system.ts:30(`builtins = []` 默认值)
|
||||
- packages/common/src/logger.json(现状:error 段 `75` 为用户 WIP 已加、缺 76/77/78;维持 4 空格缩进与尾逗号风格);packages/common/src/logger.ts:117(`error(code: number, ...params: string[])` 只收字符串参数)
|
||||
- eslint.config.js:13(`eslint.configs.recommended` → `no-empty` 生效);AGENTS.md(命名须先获批、禁改既有注释、修改方式须预先声明);dev.md(命名规则、对象类型须声明 interface、无副作用 / 无循环引用 / 无 `import type`、非空判断约定、jsDoc 规范)
|
||||
</read_first>
|
||||
<action>
|
||||
(0) **不改既有调用点与 `types.ts`:** 本任务**不得**修改 `packages-user/data-state/src/ins.ts` 或任何 `__test__` / `fixtures` 调用点,**不得**修改 `packages-user/data-state/src/types.ts`。既有调用点缺失 `plugins` / `heroFace` / `events` 的**类型不匹配属用户 WIP**(D-56 / D-91),由用户接线时统一核对;本任务仅通过**运行时守卫**保证它们不崩溃。
|
||||
|
||||
(1) **新增共享纯函数模块 `packages-user/data-state/src/plugin.ts`(命名待批准,OI-1):** 零 import、零顶层副作用;按 dev.md「对象类型须单独声明 interface」声明并导出:
|
||||
(1) **在 `packages/common/src/logger.json` 的 `error` 段追加错误码(D-92,OI-11):** 在现有 `\"75\"` 条目**之后**追加三条(**保持该文件既有 4 空格缩进与条目后接 `,` 的风格**):`\"76\": \"Plugin dependency cycle detected: $1. Cyclic plugins will not be initialized.\"`、`\"77\": \"Plugin $1 depends on a non-existent plugin: $2. Related plugins will not be initialized.\"`、`\"78\": \"Duplicate plugin id: $1. The duplicate will not be registered.\"`(文本须以 OI-11 获批结果为准)。**严禁**改动、删除、重排任何既有条目(尤其用户 WIP 的 `75`)——只做纯追加(D-56)。
|
||||
|
||||
(2) **新增共享纯函数模块 `packages-user/data-state/src/plugin.ts`(命名待批准,OI-1):** 零 import、零顶层副作用;按 dev.md「对象类型须单独声明 interface」声明并导出:
|
||||
- `IPluginDepends`:成员 `id`(`readonly string`)与 `dependencies`(`readonly string[]`)(具备插件标识符与依赖信息的结构)。
|
||||
- `IPluginHolder<T extends IPluginDepends>`:成员 `plugin`(`readonly T`)(包装插件定义的容器结构)。
|
||||
- `IPluginOrderResult<T>`:成员 `order`(`T[]`)与 `cyclic`(`Set<string>`)(排序结果)。
|
||||
- `orderPlugins<H extends IPluginHolder<IPluginDepends>>(plugins: ReadonlyMap<string, H>): IPluginOrderResult<H>`:以 `plugin.id` 与 `plugin.dependencies` 做**依赖拓扑排序**;`order` = 可初始化的拓扑序(依赖先于被依赖者);`cyclic` = **无法初始化的插件 id**(处于依赖环上,或依赖了这些插件);引用不存在的依赖 id 时**忽略该依赖**(OI-7);每个导出成员按 dev.md 写 jsDoc(方法注释用换行风格、成员注释用不换行风格)。
|
||||
- `IPluginOrderResult<T>`:成员 `order`(`T[]`,**可初始化的拓扑序**)/ `cyclic`(`Set<string>`,**处于依赖环中的插件 id**)/ `missing`(`Map<string, string[]>`,**直接引用了不存在依赖的插件 id → 其不存在的依赖 id 数组**)。
|
||||
- `orderPlugins<H extends IPluginHolder<IPluginDepends>>(plugins: ReadonlyMap<string, H>): IPluginOrderResult<H>`:以 `plugin.id` 与 `plugin.dependencies` 计算——(a)**可初始化拓扑序** `order`(依赖先于被依赖者);(b)**环检测**:处于依赖环中的插件 id 记入 `cyclic`;(c)**依赖不存在检测**:`dependencies` 里引用了注册表不存在的 id 时,把该插件 id 与其不存在的依赖 id 列表记入 `missing`。**排除规则(D-92):** `order` **不含** `cyclic` / `missing` 中的插件,**也不含依赖它们的下游插件**(下游仅缺席于 `order`,不单独记码,由消息「related plugins will not be initialized」涵盖)。算法须保证:对无依赖插件保持稳定序;对环上(含下游)与依赖不存在(含下游)的插件既不进 `order`、也不被调用。每个导出成员按 dev.md 写 jsDoc(方法注释用换行风格、成员注释用不换行风格)。
|
||||
- **不修改** `packages-user/data-state/src/types.ts`。
|
||||
|
||||
(2) **补桶导出:** 在 `packages-user/data-state/src/index.ts` 追加 `export * from './plugin';`(同目录项,符合 D-69 桶边界;与既有导出行同风格、不增删其它行)。使渲染端可自 `@user/data-state` 导入 `orderPlugins` 与相关接口。
|
||||
(3) **补桶导出:** 在 `packages-user/data-state/src/index.ts` 追加 `export * from './plugin';`(同目录项,符合 D-69 桶边界;与既有导出行同风格、不增删其它行)。使渲染端可自 `@user/data-state` 导入 `orderPlugins` 与相关接口。
|
||||
|
||||
(3) **改写 `packages-user/data-state/src/core.ts`(在用户现有内容之上补全,不回退其改动):**
|
||||
- **import:** 自 `./plugin` 引入 `orderPlugins`(`IDataPlugin` / `IDataPluginDefine` / `PluginStatus` 已由用户 import)。import 区保持 dev.md 规范(引入外部模块时首行必须是 import、import 之间无注释空行)。
|
||||
(4) **改写 `packages-user/data-state/src/core.ts`(在用户现有内容之上补全,不回退其改动):**
|
||||
- **import:** 自 `./plugin` 引入 `orderPlugins`;`logger` 已由用户/既有代码自 `@motajs/common` 引入(`core.ts:1`);`IDataPlugin` / `IDataPluginDefine` / `PluginStatus` 已由用户 import。import 区保持 dev.md 规范(引入外部模块时首行必须是 import、import 之间无注释空行)。
|
||||
- **注册表字段(OI-2):** 声明 `private readonly plugins: Map<string, IDataPlugin<IDataPluginDefine>> = new Map();`,与既有 `saveables` / `addedSaveables` / `executors` 字段同区(`//#region` 分区合理、显式类型)。
|
||||
- **构造器建表(运行时守卫,关键):** 在 `constructor(config)` 内(L3 定义之后)用 `for (const define of config.plugins ?? [])` 遍历——**必须带 `?? []` 守卫**(现存调用点均未提供 `plugins`,无守卫会 `for … of undefined` 抛错、破坏 `@user/data-state` 全部导入);为每个 `IDataPluginDefine` 写入 `this.plugins.set(id, { status: PluginStatus.NotAwakened, plugin: define })`;**重复 id 后者覆盖(OI-8,不新增告警)**;用 `this.plugins.has(id)` 判重(字面量/对象非空按 dev.md:对象用 `!`、字面量用 `!isNil`)。**不改** 构造器对 `config.loadStarter` / `config.events` 的既有用法。
|
||||
- **私有驱动辅助(复用已批准命名,不新增方法名):** 三个阶段方法共用同一「取拓扑序 → 逐项解析依赖 → 调用插件方法 → 推进状态」逻辑;**依赖解析**把 `plugin.dependencies` 经 `this.plugins.get(id)` 映射为 `IDataPlugin<IDataPluginDefine>[]`(不存在的 id 省略);**环处置**仅在首个阶段(`awake`)对 `orderPlugins` 返回的 `cyclic` 逐 id 记录 `logger.error(75, id, 'Dependency cycle detected...')`(复用既有 error 75,**不改 `logger.json`**;OI-6)。
|
||||
- **构造器建表(运行时守卫 + 重复 id 处置,关键):** 在 `constructor(config)` 内(L3 定义之后)用 `for (const define of config.plugins ?? [])` 遍历——**必须带 `?? []` 守卫**(现存调用点均未提供 `plugins`,无守卫会 `for … of undefined` 抛错、破坏 `@user/data-state` 全部导入);遍历中对每个 `IDataPluginDefine`:若 `this.plugins.has(define.id)` 为真,则 `logger.error(78, define.id)` 并**跳过(不覆盖原插件,D-92 / OI-8)**,否则 `this.plugins.set(define.id, { status: PluginStatus.NotAwakened, plugin: define })`。用 `this.plugins.has(id)` 判重(对象用 `!`、字面量用 `!isNil`,按 dev.md)。**不改** 构造器对 `config.loadStarter` / `config.events` 的既有用法。
|
||||
- **私有驱动辅助(复用已批准命名,不额外引入未获批方法名):** 三个阶段方法共用同一「取拓扑序 → 逐项解析依赖 → 调用插件方法 → 推进状态」逻辑;**依赖解析**把 `plugin.dependencies` 经 `this.plugins.get(id)` 映射为 `IDataPlugin<IDataPluginDefine>[]`(不存在的 id 省略;但此类插件已被 `orderPlugins` 排除、不会进入 `order`,故正常路径下不会有缺失);**错误处置**仅在首个阶段(`awake`)依据 `orderPlugins` 返回结果记录:若 `cyclic` 非空则 `logger.error(76, [...cyclic].join(', '))`(OI-6 / OI-11);对 `missing` 的每一项 `[pluginId, deps]` 逐条 `for (const dep of deps) logger.error(77, pluginId, dep)`(OI-7 / OI-11)。**不得**再使用 `logger.error(75, …)`。
|
||||
- **三阶段(可见性由 `private` 改为 `protected`,OI-4):**
|
||||
- `protected awake(): void`:同步;按拓扑序对 `status === PluginStatus.NotAwakened` 的插件调用 `plugin.awake(this, deps)`,随后置 `status = PluginStatus.Awakened`。
|
||||
- `protected awake(): void`:同步;先执行错误处置(76 / 77,见上);再按 `orderPlugins(...).order` 的拓扑序对 `status === PluginStatus.NotAwakened` 的插件调用 `plugin.awake(this, deps)`,随后置 `status = PluginStatus.Awakened`。
|
||||
- `protected async initialize(): Promise<void>`:异步阻塞;对尚未异步初始化的插件 `await plugin.initialize(this, deps)`,随后置 `status = PluginStatus.Initialized`。
|
||||
- `protected async afterLoad(): Promise<void>`:异步阻塞;对 `status === PluginStatus.Initialized` 的插件 `await plugin.afterLoad(this, deps)`,随后置 `status = PluginStatus.Loaded`。
|
||||
- **环上及其下游插件**在 `orderPlugins(...).order` 中缺席,故三阶段均自然跳过(`status` 保持 `NotAwakened`)。
|
||||
- **环上 / 依赖不存在插件及其下游**在 `orderPlugins(...).order` 中缺席,故三阶段均自然跳过(`status` 保持 `NotAwakened`);重复 id 的插件因未注册而同样不被驱动。
|
||||
- **`getDataPlugin`:** 保持用户实现(`return (this.plugins.get(id) as IDataPlugin<T>) ?? null;`),仅在 `plugins` 字段落地后其引用方可成立。
|
||||
- **`load()` 四段编排(D-87,第三段非空循环体):** `async load(): Promise<void>` —— 依次调用 `this.awake()`;`await this.initialize()`;用一个**非空循环体**的 `for await (const progress of this.loader.start())` 驱动加载器至完成(循环体内以 `void progress;` 消费/忽略产出值,或含等价有效语句,**不得**写空块 `{}`——eslint `no-empty` 会报错;对齐 `loader.test.ts:163`);最后 `await this.afterLoad()`。**不**在本方法内新增加载器配置(接线归用户,D-91)。
|
||||
- **不改** `initializeEnemy` / `initializeSave` / `initializeReplay`(其接线属用户 config 口径,D-91;本计划不调用、不删除、不改其注释);不改任何既有注释 / jsDoc。
|
||||
|
||||
(4) **新增运行期单测 `packages-user/data-state/src/__test__/plugin.test.ts`(OI-10):** vitest;**每个 `it` 前用一行注释说明测试内容**(AGENTS.md 测试文件规则)。
|
||||
- `orderPlugins`:验证无依赖时保持稳定序、有依赖时依赖先于被依赖者、存在环时 `order` 不含环上及其下游插件且 `cyclic` 含相应 id、未知依赖被忽略。
|
||||
- `CoreState` 端到端:构造 `new CoreState({ loadStarter: new WebLoadStarter(), coreURL: 'placeholder', heroFace: 8, plugins: [B(依赖 A), A], events: [] })`,记录各插件三方法的调用序列,`await state.load()` 后断言序列为「先全部 awake(拓扑序)→ 再全部 initialize(拓扑序)→ 再全部 afterLoad(拓扑序)」且 `state.getDataPlugin('a')!.status === PluginStatus.Loaded`;另一 `it` 用互相依赖的两插件断言二者在 `load()` 后 `status` 仍为 `NotAwakened`(环上不初始化)。
|
||||
(5) **新增运行期单测 `packages-user/data-state/src/__test__/plugin.test.ts`(OI-10):** vitest;**每个 `it` 前用一行注释说明测试内容**(AGENTS.md 测试文件规则)。
|
||||
- `orderPlugins`:验证无依赖时保持稳定序、有依赖时依赖先于被依赖者;存在环时 `order` 不含环上及其下游插件且 `cyclic` 含环上 id;某插件依赖不存在 id 时 `order` 不含该插件及其下游且 `missing` 映射该插件 → 不存在的依赖 id 列表。
|
||||
- `CoreState` 端到端:构造 `new CoreState({ loadStarter: new WebLoadStarter(), coreURL: 'placeholder', heroFace: 8, plugins: [B(依赖 A), A], events: [] })`,记录各插件三方法的调用序列,`await state.load()` 后断言序列为「先全部 awake(拓扑序)→ 再全部 initialize(拓扑序)→ 再全部 afterLoad(拓扑序)」且 `state.getDataPlugin('a')!.status === PluginStatus.Loaded`。
|
||||
- **环排除:** 另一 `it` 用互相依赖的两插件,断言 `await state.load()` 后二者 `status` 仍为 `NotAwakened`,且其 `awake` / `initialize` / `afterLoad` 均**从未被调用**(记录调用序为空)。
|
||||
- **依赖不存在排除(含下游):** 又一 `it` 用「C 依赖不存在的 id、D 依赖 C」,断言 `load()` 后 C 与 D 的 `status` 均 `NotAwakened`、且三方法均未被调用。
|
||||
- **重复 id 不覆盖:** 又一 `it` 用两个同 id 的插件 define(第二个不同),断言构造后 `state.getDataPlugin('dup')!.plugin === 第一个 define`(保留先注册者、未覆盖),且重复者从未被驱动。
|
||||
- **运行时守卫(BLOCKER 证明):** 另加一个 `it`,以 `new CoreState({ loadStarter: new WebLoadStarter(), coreURL: 'placeholder' } as any)`(**省略 `plugins` / `heroFace` / `events`**,模拟现存调用点)构造,断言**构造与 `await state.load()` 均不抛错**(注册表为空、四段空转完成),从而证明 `config.plugins ?? []` 守卫有效。此 `as any` 是为规避**用户 WIP** 的既存类型不匹配(vitest 不类型检查;不改 `types.ts`、不改既有调用点)。
|
||||
</action>
|
||||
<behavior>
|
||||
- `orderPlugins` 对 `{a: [], b: [a]}` → `order` 中 `a` 先于 `b`,`cyclic` 为空。
|
||||
- `orderPlugins` 对 `{a: [], b: [a]}` → `order` 中 `a` 先于 `b`,`cyclic` 与 `missing` 皆为空。
|
||||
- `orderPlugins` 对 `{x: [y], y: [x]}` → `cyclic` 含 `x` 与 `y`,`order` 不含 `x`/`y`。
|
||||
- `orderPlugins` 对 `{c: ['nope'], d: [c]}` → `missing` 含 `c → ['nope']`,`order` 不含 `c`/`d`。
|
||||
- `CoreState.load()` 对 `[B(依赖 A), A]` → 调用序列 `a.awake, b.awake, a.initialize, b.initialize, a.afterLoad, b.afterLoad`;结束后 `getDataPlugin('a').status === Loaded`、`getDataPlugin('b').status === Loaded`。
|
||||
- `CoreState.load()` 对互相依赖的 `[X, Y]` → 结束后 `getDataPlugin('x').status === NotAwakened` 且 `getDataPlugin('y').status === NotAwakened`。
|
||||
- `CoreState.load()` 对互相依赖的 `[X, Y]` → 结束后 `getDataPlugin('x').status === NotAwakened` 且 `getDataPlugin('y').status === NotAwakened`,且二者方法从未被调用。
|
||||
- `CoreState.load()` 对 `[C(依赖 'nope'), D(依赖 C)]` → 结束后 `getDataPlugin('c')` 与 `getDataPlugin('d')` 的 `status` 均 `NotAwakened`,且二者方法从未被调用。
|
||||
- 两个同 id `dup` 的插件 → `getDataPlugin('dup').plugin === 第一个 define`(不覆盖)。
|
||||
- `new CoreState({ loadStarter, coreURL } as any)`(无 `plugins`)→ 构造与 `await state.load()` 均不抛错(运行时守卫生效;注册表为空)。
|
||||
</behavior>
|
||||
<reversibility rating="reversible">新增 `plugin.ts` 与测试、补桶导出行、改写 `core.ts` 的插件部分;`git checkout` / 删除可整体回退;未改 `types.ts`、未改既有调用点、未改其它包。</reversibility>
|
||||
<reversibility rating="reversible">新增 `plugin.ts` 与测试、补桶导出行、在 `logger.json` 追加 76/77/78、改写 `core.ts` 的插件部分;`git checkout` / 删除追加块可整体回退;未改 `types.ts`、未改既有调用点、未改其它包。</reversibility>
|
||||
<verify>
|
||||
<automated>pnpm exec vitest run packages-user/data-state/src/__test__/plugin.test.ts</automated>
|
||||
<fails_when>non-zero exit, 或任一审定失败(拓扑序错误 / 环未排除 / 未知依赖未忽略 / `CoreState.load()` 四段调用顺序不符 / `PluginStatus` 推进不符 / 环上插件被初始化 / **省略 `plugins` 构造并 `load()` 抛错**)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const plug=fs.readFileSync('packages-user/data-state/src/plugin.ts','utf8');if(!/export function orderPlugins/.test(plug))bad.push('plugin.ts: orderPlugins not exported');for(const iface of ['IPluginDepends','IPluginHolder','IPluginOrderResult']){if(!new RegExp('export interface '+iface).test(plug))bad.push('plugin.ts: missing '+iface)}if(/(^|[^a-zA-Z])import[^a-zA-Z]/.test(plug))bad.push('plugin.ts: expected zero imports');const core=fs.readFileSync('packages-user/data-state/src/core.ts','utf8');if(core.indexOf('orderPlugins')<0)bad.push('core.ts: does not use orderPlugins');if(!/plugins\s*:\s*Map<string,\s*IDataPlugin/.test(core))bad.push('core.ts: missing plugins registry typed Map<string, IDataPlugin<...>>');if(!/config\.plugins\s*\?\?\s*\[\]/.test(core))bad.push('core.ts: registry build NOT runtime-guarded (config.plugins ?? [])');if(!/protected\s+awake\s*\(/.test(core))bad.push('core.ts: awake not protected');if(!/protected\s+async\s+initialize\s*\(/.test(core))bad.push('core.ts: initialize not protected');if(!/protected\s+async\s+afterLoad\s*\(/.test(core))bad.push('core.ts: afterLoad not protected');if(core.indexOf('this.loader.start()')<0)bad.push('core.ts: load() does not drive loader.start()');if(core.indexOf('PluginStatus.Awakened')<0||core.indexOf('PluginStatus.Initialized')<0||core.indexOf('PluginStatus.Loaded')<0)bad.push('core.ts: PluginStatus transitions missing');if(core.indexOf('logger.error(75')<0)bad.push('core.ts: cycle not logged via logger.error(75, ...)');const idx=fs.readFileSync('packages-user/data-state/src/index.ts','utf8');if(idx.indexOf(\"'./plugin'\")<0)bad.push('index.ts: missing ./plugin barrel export');if(bad.length>0){console.error('DATA PLUGIN GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK data-side plugin driver wiring present (registry runtime-guarded)')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "DATA PLUGIN GATE FAILED"(`plugin.ts` 未导出 `orderPlugins`/三接口或含 import / `core.ts` 未用 `orderPlugins`、缺 `plugins` 注册表、**注册表构建未带 `config.plugins ?? []` 守卫**、三阶段非 `protected`、`load()` 未驱动 `loader.start()`、缺 `PluginStatus` 推进、环未走 `logger.error(75,...)`、`index.ts` 缺 `./plugin` 桶导出)</fails_when>
|
||||
<fails_when>non-zero exit, 或任一审定失败(拓扑序错误 / 环未排除 / 依赖不存在未排除 / `CoreState.load()` 四段调用顺序不符 / `PluginStatus` 推进不符 / 环上或依赖不存在插件被初始化 / 重复 id 覆盖了原插件 / **省略 `plugins` 构造并 `load()` 抛错**)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const plug=fs.readFileSync('packages-user/data-state/src/plugin.ts','utf8');if(!/export function orderPlugins/.test(plug))bad.push('plugin.ts: orderPlugins not exported');for(const iface of ['IPluginDepends','IPluginHolder','IPluginOrderResult']){if(!new RegExp('export interface '+iface).test(plug))bad.push('plugin.ts: missing '+iface)}if(!/missing\s*:/.test(plug))bad.push('plugin.ts: IPluginOrderResult lacks missing member');if(/(^|[^a-zA-Z])import[^a-zA-Z]/.test(plug))bad.push('plugin.ts: expected zero imports');const lg=fs.readFileSync('packages/common/src/logger.json','utf8');for(const code of ['75','76','77','78']){if(!new RegExp('\"'+code+'\"\\\\s*:').test(lg))bad.push('logger.json missing error '+code)}const core=fs.readFileSync('packages-user/data-state/src/core.ts','utf8');if(core.indexOf('orderPlugins')<0)bad.push('core.ts: does not use orderPlugins');if(!/plugins\s*:\s*Map<string,\s*IDataPlugin/.test(core))bad.push('core.ts: missing plugins registry typed Map<string, IDataPlugin<...>>');if(!/config\.plugins\s*\?\?\s*\[\]/.test(core))bad.push('core.ts: registry build NOT runtime-guarded (config.plugins ?? [])');if(!/this\.plugins\.has\(/.test(core))bad.push('core.ts: duplicate id not guarded (no this.plugins.has before set)');if(core.indexOf('logger.error(78')<0)bad.push('core.ts: duplicate id not logged via logger.error(78, ...)');if(/protected\s+awake\s*\(/.test(core)===false)bad.push('core.ts: awake not protected');if(!/protected\s+async\s+initialize\s*\(/.test(core))bad.push('core.ts: initialize not protected');if(!/protected\s+async\s+afterLoad\s*\(/.test(core))bad.push('core.ts: afterLoad not protected');if(core.indexOf('this.loader.start()')<0)bad.push('core.ts: load() does not drive loader.start()');if(core.indexOf('PluginStatus.Awakened')<0||core.indexOf('PluginStatus.Initialized')<0||core.indexOf('PluginStatus.Loaded')<0)bad.push('core.ts: PluginStatus transitions missing');if(core.indexOf('logger.error(76')<0)bad.push('core.ts: cycle not logged via logger.error(76, ...)');if(core.indexOf('logger.error(77')<0)bad.push('core.ts: missing dependency not logged via logger.error(77, ...)');if(core.indexOf('logger.error(75')>=0)bad.push('core.ts: must NOT reuse logger.error(75) for plugin errors');const idx=fs.readFileSync('packages-user/data-state/src/index.ts','utf8');if(idx.indexOf(\"'./plugin'\")<0)bad.push('index.ts: missing ./plugin barrel export');if(bad.length>0){console.error('DATA PLUGIN GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK data-side plugin driver wiring present (registry runtime-guarded, dup-id no-overwrite, 76/77/78 logged)')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "DATA PLUGIN GATE FAILED"(`plugin.ts` 未导出 `orderPlugins`/三接口、缺 `missing` 成员或含 import / `logger.json` 缺 75/76/77/78 任一 / `core.ts` 未用 `orderPlugins`、缺 `plugins` 注册表、**注册表构建未带 `config.plugins ?? []` 守卫**、**重复 id 未用 `this.plugins.has` 判重或未报 78**、三阶段非 `protected`、`load()` 未驱动 `loader.start()`、缺 `PluginStatus` 推进、缺 76 / 77、**仍使用 `logger.error(75`**、`index.ts` 缺 `./plugin` 桶导出)</fails_when>
|
||||
<automated>node -e "const fs=require('fs'),path=require('path');const root='packages-user/data-state/src';const bad=[];const walk=d=>{for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);if(e.isDirectory()){if(e.name==='__test__')continue;walk(p)}else if(e.name.endsWith('.ts')){const t=fs.readFileSync(p,'utf8');if(/from ['\u0022]@user\/client-(modules|base|system)['\u0022]/.test(t))bad.push(p)}}};walk(root);if(bad.length>0){console.error('ACYCLIC GATE FAILED :: data-state imports render-side packages :: '+bad.join(' | '));process.exit(1)}console.log('OK acyclic: data-state/src has no import of @user/client-* (no reverse dependency)')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "ACYCLIC GATE FAILED"(`packages-user/data-state/src/**` 出现对 `@user/client-modules` / `@user/client-base` / `@user/client-system` 的导入)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `plugin.ts` 存在,导出 `orderPlugins` 与 `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`(含成员 `id` / `dependencies` / `plugin` / `order` / `cyclic`),零 import、零顶层副作用(dev.md)
|
||||
- `orderPlugins` 的拓扑序、环检测、未知依赖忽略行为经运行期单测断言(`plugin.test.ts` 全绿)
|
||||
- `core.ts` 含 `plugins` 注册表(`Map<string, IDataPlugin<IDataPluginDefine>>`,构造器以 `config.plugins ?? []` **运行时守卫**建立)、`protected` 的三阶段驱动、`getDataPlugin` 从注册表读取、`load()` 四段编排(含 `this.loader.start()`,第三段非空循环体、符合 eslint `no-empty`)、`PluginStatus` 推进、环走 `logger.error(75,...)`
|
||||
- **运行时守卫经单测证明:** 省略 `plugins` 构造(`as any`)并 `await state.load()` 不抛错
|
||||
- `logger.json` error 段含 `76` / `77` / `78`(OI-11 文本),且既有 `75` 及其它条目**原样保留**(未改动 / 未删除 / 未重排)
|
||||
- `plugin.ts` 存在,导出 `orderPlugins` 与 `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`(含成员 `id` / `dependencies` / `plugin` / `order` / `cyclic` / `missing`),零 import、零顶层副作用(dev.md)
|
||||
- `orderPlugins` 的拓扑序、环检测、依赖不存在检测、下游排除行为经运行期单测断言(`plugin.test.ts` 全绿)
|
||||
- `core.ts` 含 `plugins` 注册表(`Map<string, IDataPlugin<IDataPluginDefine>>`,构造器以 `config.plugins ?? []` **运行时守卫**建立;重复 id `logger.error(78,…)` 且**不覆盖**)、`protected` 的三阶段驱动、`getDataPlugin` 从注册表读取、`load()` 四段编排(含 `this.loader.start()`,第三段非空循环体、符合 eslint `no-empty`)、`PluginStatus` 推进、环走 `logger.error(76,…)`、依赖不存在走 `logger.error(77,…)`;**无 `logger.error(75`**
|
||||
- **运行时守卫经单测证明:** 省略 `plugins` 构造(`as any`)并 `await state.load()` 不抛错;环上 / 依赖不存在插件(含下游)经单测证明 `status` 保持 `NotAwakened` 且方法未被调用;重复 id 经单测证明保留原插件
|
||||
- `index.ts` 新增 `export * from './plugin'`(仅此一行,符合 D-69)
|
||||
- 数据端未引入对渲染端包的反向导入(方向/环结构断言全绿)
|
||||
- 未改 `types.ts` / 未改 `ins.ts` 与任何既有 test/fixture 调用点 / 未改 `initializeEnemy` / `initializeSave` / `initializeReplay` 及其注释 / 未改任何既有注释
|
||||
</acceptance_criteria>
|
||||
<done>`CoreState` 四段式插件驱动端到端成立:注册表(带守卫)建立、拓扑序驱动、状态推进、环排除,均有运行期单测与结构断言兜底;省略 `plugins` 的既有调用点不崩溃;`plugin.ts` 共享排序函数就位;`index.ts` 桶导出就位</done>
|
||||
<done>`CoreState` 四段式插件驱动端到端成立:注册表(带守卫,重复 id 不覆盖)建立、拓扑序驱动、状态推进、环与依赖不存在插件(含下游)排除,均有运行期单测与结构断言兜底;`logger.json` 新增 76/77/78 且保留 75;省略 `plugins` 的既有调用点不崩溃;`plugin.ts` 共享排序函数就位;`index.ts` 桶导出就位</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: 渲染端镜像 —— `IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / `clientPlugins` + `ClientCore` 实现与统一 `load()` 接线,收口门禁与 SUMMARY</name>
|
||||
<precondition>Task 1 已完成(`plugin.ts` 的 `orderPlugins` 与数据端驱动就位、桶导出已加,`@user/data-state` 可对外提供 `orderPlugins`);Task 0 已闭环(OI-3 / OI-4 / OI-5 / OI-9 已获批或按用户指定)。`client-modules/src/client.ts` 的 `ClientCore` 调用点未提供 `clientPlugins`,故渲染端建表必须以 `config.clientPlugins ?? []` 守卫。</precondition>
|
||||
<precondition>Task 1 已完成(`plugin.ts` 的 `orderPlugins` 与数据端驱动就位、桶导出已加,`@user/data-state` 可对外提供 `orderPlugins`;`logger.json` error 段已含 76/77/78);Task 0 已闭环(OI-3 / OI-4 / OI-5 / OI-9 已获批或按用户指定)。`client-modules/src/client.ts` 的 `ClientCore` 调用点未提供 `clientPlugins`,故渲染端建表必须以 `config.clientPlugins ?? []` 守卫。</precondition>
|
||||
<files>packages-user/client-modules/src/types.ts, packages-user/client-modules/src/client.ts, .planning/phases/09-minimum-browser-runnable/09-20-SUMMARY.md</files>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-88 / D-89 / D-90 / D-91)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-88 / D-89 / D-90 / D-91 / **D-92**)
|
||||
- packages-user/data-state/src/types.ts(`PluginStatus:10` / `IDataPlugin:21` / `IDataPluginDefine:28`)
|
||||
- packages-user/data-state/src/plugin.ts(Task 1 产出:`orderPlugins` 与接口签名,供渲染端自 `@user/data-state` 复用)
|
||||
- packages-user/data-state/src/index.ts(Task 1 产出:`export * from './plugin'`,是渲染端导入 `orderPlugins` 的来源)
|
||||
- packages-user/client-modules/src/types.ts(现状:`IClientCoreConfig:4`(`extends ICoreStateConfig`)/ `IClientCore:13`(`extends IClientSystem`))
|
||||
- packages-user/client-modules/src/client.ts(现状全文:`ClientCore:47`、constructor:66-137、`init:142`、`initMapExtensions:150`;`super(...):67-70` **未提供 `plugins`**)
|
||||
- packages-user/client-modules/src/core.ts:6(模块级 `new ClientCore({ clientURL, tilesetReserve, tilesetUnit })` **未提供 `clientPlugins`**);src/client/entry.ts(渲染端调用点,**未提供 `clientPlugins`**)
|
||||
- packages-user/client-modules/src/loader/hook.ts:1(`client-modules` 自 `@motajs/common` 导入 `logger` 的先例);packages/common/src/logger.json(error 76/77/78 状态,Task 1 已加)
|
||||
- packages-user/client-system/src/types.ts:3(`IClientSystem extends IClientBase`);packages-user/client-base/src/types.ts:21(`IClientBase extends ICoreState`)
|
||||
- AGENTS.md / dev.md(命名须先获批、对象类型须声明 interface、模块无副作用、非空判断约定、jsDoc 规范、`private` 方法置调用者之前)
|
||||
</read_first>
|
||||
@ -302,33 +323,33 @@ Phase 9 · 任务 10 · 第 20 个计划 —— **实现数据端插件驱动 +
|
||||
- **不自建 `PluginStatus`**(必须自 `@user/data-state` 导入复用)。
|
||||
|
||||
(2) **`packages-user/client-modules/src/client.ts`(`ClientCore`,在用户现有内容之上追加):**
|
||||
- import 区:自 `./types` 追加 `IClientPlugin` / `IClientPluginDefine`(`IClientCore` / `IClientCoreConfig` 已在用);**自 `@user/data-state` 导入 `orderPlugins`**——正确来源是本包(`packages-user/client-modules`)内不存在的本地模块之外的 `@user/data-state` 包(Task 1 的 `plugin.ts` 经该包桶导出)。import 区保持 dev.md 规范。
|
||||
- import 区:自 `./types` 追加 `IClientPlugin` / `IClientPluginDefine`(`IClientCore` / `IClientCoreConfig` 已在用);**自 `@user/data-state` 导入 `orderPlugins`**——正确来源是本包(`packages-user/client-modules`)内不存在的本地模块之外的 `@user/data-state` 包(Task 1 的 `plugin.ts` 经该包桶导出);另自 `@motajs/common` 导入 `logger`(用于渲染端错误处置 76/77/78)。import 区保持 dev.md 规范。
|
||||
- **注册表字段(OI-3):** 声明 `private readonly clientPlugins: Map<string, IClientPlugin<IClientPluginDefine>> = new Map();`(显式类型、`private readonly`;与既有 Layer 字段同区)。
|
||||
- **构造器建表(运行时守卫,关键):** 在 `constructor(config: IClientCoreConfig)` 内用 `for (const define of config.clientPlugins ?? [])` 遍历——**必须带 `?? []` 守卫**(渲染端调用点 `client-modules/src/core.ts:6` 未提供 `clientPlugins`,无守卫会 `for … of undefined` 抛错);为每个 `IClientPluginDefine` 写入 `this.clientPlugins.set(id, { status: PluginStatus.NotAwakened, plugin: define })`;重复 id 后者覆盖(OI-8 口径,不新增告警)。**不改** `super(...)` 调用(OI-9 默认:不传 `config`)。
|
||||
- **构造器建表(运行时守卫 + 重复 id 处置,关键):** 在 `constructor(config: IClientCoreConfig)` 内用 `for (const define of config.clientPlugins ?? [])` 遍历——**必须带 `?? []` 守卫**(渲染端调用点 `client-modules/src/core.ts:6` 未提供 `clientPlugins`,无守卫会 `for … of undefined` 抛错);若 `this.clientPlugins.has(define.id)` 为真则 `logger.error(78, define.id)` 并**跳过(不覆盖原插件,D-92 / OI-8)**,否则 `this.clientPlugins.set(define.id, { status: PluginStatus.NotAwakened, plugin: define })`。**不改** `super(...)` 调用(OI-9 默认:不传 `config`)。
|
||||
- **`getClientPlugin`:** `getClientPlugin<T extends IClientPluginDefine>(id: string): Readonly<IClientPlugin<T>> | null { return (this.clientPlugins.get(id) as IClientPlugin<T>) ?? null; }`(镜像数据端 `getDataPlugin`)。
|
||||
- **三阶段覆写(OI-4 默认:`CoreState` 三阶段已为 `protected`):** 覆写 `protected awake(): void`、`protected async initialize(): Promise<void>`、`protected async afterLoad(): Promise<void>`,各自先调用 `super.awake()` / `super.initialize()` / `super.afterLoad()`(数据端阶段先,L0-L3)再驱动渲染端插件(L4-L5);渲染端驱动与数据端同构——`orderPlugins(this.clientPlugins)` 取拓扑序、跳过 `cyclic`、逐项解析 `dependencies`、按阶段调用 `plugin.awake/initialize/afterLoad(this, deps)` 并推进 `status`。私有方法(如有)放在其调用者之前(dev.md)。
|
||||
- **三阶段覆写(OI-4 默认:`CoreState` 三阶段已为 `protected`):** 覆写 `protected awake(): void`、`protected async initialize(): Promise<void>`、`protected async afterLoad(): Promise<void>`,各自先调用 `super.awake()` / `super.initialize()` / `super.afterLoad()`(数据端阶段先,L0-L3)再驱动渲染端插件(L4-L5);渲染端驱动与数据端同构——`orderPlugins(this.clientPlugins)` 取结果,`awake` 阶段据结果报 `logger.error(76, [...cyclic].join(', '))` 与逐条 `logger.error(77, pluginId, missingDepId)`(渲染端错误处置,D-92),随后按 `order` 拓扑序跳过错误插件及其下游、逐项解析 `dependencies`、按阶段调用 `plugin.awake/initialize/afterLoad(this, deps)` 并推进 `status`。私有方法(如有)放在其调用者之前(dev.md)。
|
||||
- **不改** `init()` / `initMapExtensions()` 及其注释;不改任何既有注释 / jsDoc。
|
||||
|
||||
(3) **收口与 SUMMARY(`09-20-SUMMARY.md`):** 记录本计划范围、四段式编排与拓扑/环处置口径(含数据端先 / 渲染端后)、**运行时守卫(`config.plugins ?? []` / `config.clientPlugins ?? []`)及其理由(现存调用点未提供配置、属用户 WIP,D-56)**、数据端注册表与驱动的落点、渲染端镜像落点与 `IClientCore` 为 `state` 参数类型的最终情形、**已知后果(D-91):渲染端调用点(`client-modules/src/core.ts`、`src/client/entry.ts`)尚不传 `clientPlugins`、数据端 `ins.ts` 尚不传 `plugins`,故「数据先渲染后」在用户接线前为空转**、`open_items`(OI-1..OI-10)的获批/裁定结果、门禁结果(运行期单测 + 结构断言 + 方向环 + 范围门禁)、未实现内容(入口/接线属用户)。
|
||||
(3) **收口与 SUMMARY(`09-20-SUMMARY.md`):** 记录本计划范围、四段式编排与拓扑/错误处置口径(含数据端先 / 渲染端后;错误码 76=环 / 77=依赖不存在 / 78=重复 id 且不覆盖;报错插件及其下游全部不加载)、**运行时守卫(`config.plugins ?? []` / `config.clientPlugins ?? []`)及其理由(现存调用点未提供配置、属用户 WIP,D-56)**、数据端注册表与驱动的落点、**`logger.json` error 段新增 76/77/78 的最终文本与保留 75 的事实**、渲染端镜像落点与 `IClientCore` 为 `state` 参数类型的最终情形、**已知后果(D-91):渲染端调用点(`client-modules/src/core.ts`、`src/client/entry.ts`)尚不传 `clientPlugins`、数据端 `ins.ts` 尚不传 `plugins`,故「数据先渲染后」在用户接线前为空转**、`open_items`(OI-1..OI-11)的获批/裁定结果、门禁结果(运行期单测 + 结构断言 + 方向环 + 范围门禁)、未实现内容(入口/接线属用户)。
|
||||
</action>
|
||||
<reversibility rating="reversible">在 `types.ts` 追加渲染端插件类型、在 `client.ts` 追加渲染端注册表与三阶段覆写;`git checkout` / 删除追加块可整体回退;未改数据端已落地内容、未改任何调用点。</reversibility>
|
||||
<reversibility rating="reversible">在 `types.ts` 追加渲染端插件类型、在 `client.ts` 追加渲染端注册表与三阶段覆写(含错误处置);`git checkout` / 删除追加块可整体回退;未改数据端已落地内容、未改任何调用点、未改 `logger.json`(76/77/78 已由 Task 1 落盘)。</reversibility>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const ty=fs.readFileSync('packages-user/client-modules/src/types.ts','utf8');for(const t of ['IClientPlugin','IClientPluginDefine','clientPlugins','getClientPlugin']){if(ty.indexOf(t)<0)bad.push('types.ts missing '+t)}if(!/from ['\u0022]@user\/data-state['\u0022]/.test(ty))bad.push('types.ts does not import from @user/data-state');if(ty.indexOf('PluginStatus')<0)bad.push('types.ts does not use PluginStatus');if(/export\s+(const\s+)?enum\s+PluginStatus/.test(ty))bad.push('types.ts must not redefine PluginStatus');const cl=fs.readFileSync('packages-user/client-modules/src/client.ts','utf8');if(!/clientPlugins\s*:\s*Map<string,\s*IClientPlugin/.test(cl))bad.push('client.ts missing clientPlugins registry');if(!/config\.clientPlugins\s*\?\?\s*\[\]/.test(cl))bad.push('client.ts: registry build NOT runtime-guarded (config.clientPlugins ?? [])');if(cl.indexOf('getClientPlugin')<0)bad.push('client.ts missing getClientPlugin');if(!/protected\s+awake\s*\(/.test(cl))bad.push('client.ts awake not overridden as protected');if(!/protected\s+async\s+initialize\s*\(/.test(cl))bad.push('client.ts initialize not overridden');if(!/protected\s+async\s+afterLoad\s*\(/.test(cl))bad.push('client.ts afterLoad not overridden');if(!/super\.awake\s*\(/.test(cl)||!/super\.initialize\s*\(/.test(cl)||!/super\.afterLoad\s*\(/.test(cl))bad.push('client.ts phase overrides must call super.<stage>() first');if(!/import\s*\{[\s\S]*?orderPlugins[\s\S]*?\}\s*from\s*['\u0022]@user\/data-state['\u0022]/.test(cl))bad.push('client.ts: orderPlugins must be imported from @user/data-state');if(/from\s*['\u0022]\.\/plugin['\u0022]/.test(cl))bad.push('client.ts: must not import a local plugin module (client-modules has none)');if(/export\s+(const\s+)?enum\s+PluginStatus/.test(cl))bad.push('client.ts must not redefine PluginStatus');if(bad.length>0){console.error('RENDER PLUGIN GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK render-side plugin mirror wiring present (registry runtime-guarded, orderPlugins from @user/data-state)')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "RENDER PLUGIN GATE FAILED"(`types.ts` 缺 `IClientPlugin`/`IClientPluginDefine`/`clientPlugins`/`getClientPlugin`、未从 `@user/data-state` 导入或未用 `PluginStatus`、或自建了 `PluginStatus` 枚举;`client.ts` 缺 `clientPlugins` 注册表 / **缺 `config.clientPlugins ?? []` 守卫** / `getClientPlugin` / 三阶段 `protected` 覆写 / `super.<stage>()` / **`orderPlugins` 未自 `@user/data-state` 导入或指向本地模块** / 自建 `PluginStatus`)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const ty=fs.readFileSync('packages-user/client-modules/src/types.ts','utf8');for(const t of ['IClientPlugin','IClientPluginDefine','clientPlugins','getClientPlugin']){if(ty.indexOf(t)<0)bad.push('types.ts missing '+t)}if(!/from ['\u0022]@user\/data-state['\u0022]/.test(ty))bad.push('types.ts does not import from @user/data-state');if(ty.indexOf('PluginStatus')<0)bad.push('types.ts does not use PluginStatus');if(/export\s+(const\s+)?enum\s+PluginStatus/.test(ty))bad.push('types.ts must not redefine PluginStatus');const cl=fs.readFileSync('packages-user/client-modules/src/client.ts','utf8');if(!/clientPlugins\s*:\s*Map<string,\s*IClientPlugin/.test(cl))bad.push('client.ts missing clientPlugins registry');if(!/config\.clientPlugins\s*\?\?\s*\[\]/.test(cl))bad.push('client.ts: registry build NOT runtime-guarded (config.clientPlugins ?? [])');if(!/this\.clientPlugins\.has\(/.test(cl))bad.push('client.ts: duplicate id not guarded (no this.clientPlugins.has before set)');if(cl.indexOf('logger.error(78')<0)bad.push('client.ts: duplicate id not logged via logger.error(78, ...)');if(cl.indexOf('getClientPlugin')<0)bad.push('client.ts missing getClientPlugin');if(!/protected\s+awake\s*\(/.test(cl))bad.push('client.ts awake not overridden as protected');if(!/protected\s+async\s+initialize\s*\(/.test(cl))bad.push('client.ts initialize not overridden');if(!/protected\s+async\s+afterLoad\s*\(/.test(cl))bad.push('client.ts afterLoad not overridden');if(!/super\.awake\s*\(/.test(cl)||!/super\.initialize\s*\(/.test(cl)||!/super\.afterLoad\s*\(/.test(cl))bad.push('client.ts phase overrides must call super.<stage>() first');if(cl.indexOf('logger.error(76')<0)bad.push('client.ts: cycle not logged via logger.error(76, ...)');if(cl.indexOf('logger.error(77')<0)bad.push('client.ts: missing dependency not logged via logger.error(77, ...)');if(!/import\s*\{[\s\S]*?orderPlugins[\s\S]*?\}\s*from\s*['\u0022]@user\/data-state['\u0022]/.test(cl))bad.push('client.ts: orderPlugins must be imported from @user/data-state');if(/from\s*['\u0022]\.\/plugin['\u0022]/.test(cl))bad.push('client.ts: must not import a local plugin module (client-modules has none)');if(/export\s+(const\s+)?enum\s+PluginStatus/.test(cl))bad.push('client.ts must not redefine PluginStatus');if(bad.length>0){console.error('RENDER PLUGIN GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK render-side plugin mirror wiring present (registry runtime-guarded, dup-id no-overwrite, 76/77/78 logged, orderPlugins from @user/data-state)')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "RENDER PLUGIN GATE FAILED"(`types.ts` 缺 `IClientPlugin`/`IClientPluginDefine`/`clientPlugins`/`getClientPlugin`、未从 `@user/data-state` 导入或未用 `PluginStatus`、或自建了 `PluginStatus` 枚举;`client.ts` 缺 `clientPlugins` 注册表 / **缺 `config.clientPlugins ?? []` 守卫** / **缺 `this.clientPlugins.has` 判重或未报 78** / `getClientPlugin` / 三阶段 `protected` 覆写 / `super.<stage>()` / 缺 76 / 缺 77 / **`orderPlugins` 未自 `@user/data-state` 导入或指向本地模块** / 自建 `PluginStatus`)</fails_when>
|
||||
<!-- plan-criteria-allow: R4 - SCOPE GATE 内的 git status 由 node execSync 调用并以 try/catch 捕获失败(失败即非零退出),同行的 `|` 属 JS 正则/字符串,非 shell 管道阶段 -->
|
||||
<automated>node -e "const {execSync}=require('child_process');let raw;try{raw=execSync('git status --porcelain',{encoding:'utf8'})}catch(e){console.error('SCOPE GATE FAILED :: git status failed :: '+e.message);process.exit(1)}const lines=raw.split(/\r?\n/).filter(Boolean);const allow=new Set(['packages-user/data-state/src/plugin.ts','packages-user/data-state/src/core.ts','packages-user/data-state/src/index.ts','packages-user/data-state/src/__test__/plugin.test.ts','packages-user/client-modules/src/types.ts','packages-user/client-modules/src/client.ts','.planning/phases/09-minimum-browser-runnable/09-20-SUMMARY.md']);const userWip=new Set(['AGENTS.md','.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md','index.html','packages-user/client-modules/src/index.ts','packages-user/data-base/src/hero/attribute.ts','packages-user/data-base/src/hero/state.ts','packages-user/data-base/src/hero/types.ts','packages-user/data-state/src/types.ts','packages/common/src/logger.json']);const bad=[];for(const l of lines){const p=l.slice(3).trim().replace(/^\u0022|\u0022$/g,'');if(allow.has(p)||userWip.has(p))continue;if(p==='src/bootstrap.ts'||p.startsWith('src/client/')||p.startsWith('src/data/')||p==='src/constant.ts')continue;bad.push(p)}if(bad.length>0){console.error('SCOPE GATE FAILED :: out-of-scope changes :: '+bad.join(' | '));process.exit(1)}console.log('OK scope: only this plan files_modified changed (user WIP ignored)')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SCOPE GATE FAILED"(本计划执行期改动了 `files_modified` 与用户并发 WIP 之外的任何文件——例如 `packages-user/client-base/**`、`packages-user/data-base/**`、`packages-user/data-state/src/types.ts`、`packages-user/data-state/src/ins.ts`、`packages/common/**`、`package.json`、lockfile、`src/**`、其它 `.planning/**`)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const idx=fs.readFileSync('packages-user/client-modules/src/index.ts','utf8');if(idx.indexOf(\"'./types'\")<0)bad.push('index.ts no longer exports ./types');const bad2=[];const ty=fs.readFileSync('packages-user/client-modules/src/types.ts','utf8');if(/\.json/.test(ty))bad2.push('sanity-json');if(bad.length>0||bad2.length>0){console.error('BARREL/ROUTE FAILED');process.exit(1)}console.log('OK barrel routes preserved')"</automated>
|
||||
<fails_when>non-zero exit(`client-modules/src/index.ts` 的 `./types` 桶导出被破坏,或渲染端接线异常)</fails_when>
|
||||
<automated>node -e "const {execSync}=require('child_process');let raw;try{raw=execSync('git status --porcelain',{encoding:'utf8'})}catch(e){console.error('SCOPE GATE FAILED :: git status failed :: '+e.message);process.exit(1)}const lines=raw.split(/\r?\n/).filter(Boolean);const allow=new Set(['packages-user/data-state/src/plugin.ts','packages-user/data-state/src/core.ts','packages-user/data-state/src/index.ts','packages-user/data-state/src/__test__/plugin.test.ts','packages-user/client-modules/src/types.ts','packages-user/client-modules/src/client.ts','packages/common/src/logger.json','.planning/phases/09-minimum-browser-runnable/09-20-SUMMARY.md']);const userWip=new Set(['AGENTS.md','.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md','index.html','packages-user/client-modules/src/index.ts','packages-user/data-base/src/hero/attribute.ts','packages-user/data-base/src/hero/state.ts','packages-user/data-base/src/hero/types.ts','packages-user/data-state/src/types.ts']);const bad=[];for(const l of lines){const p=l.slice(3).trim().replace(/^\u0022|\u0022$/g,'');if(allow.has(p)||userWip.has(p))continue;if(p==='src/bootstrap.ts'||p.startsWith('src/client/')||p.startsWith('src/data/')||p==='src/constant.ts')continue;bad.push(p)}if(bad.length>0){console.error('SCOPE GATE FAILED :: out-of-scope changes :: '+bad.join(' | '));process.exit(1)}console.log('OK scope: only this plan files_modified changed (user WIP ignored)')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "SCOPE GATE FAILED"(本计划执行期改动了 `files_modified` 与用户并发 WIP 之外的任何文件——例如 `packages-user/client-base/**`、`packages-user/data-base/**`、`packages-user/data-state/src/types.ts`、`packages-user/data-state/src/ins.ts`、`packages/common/**`(`src/logger.json` 除外)、`package.json`、lockfile、`src/**`、其它 `.planning/**`)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const idx=fs.readFileSync('packages-user/client-modules/src/index.ts','utf8');if(idx.indexOf(\"'./types'\")<0)bad.push('index.ts no longer exports ./types');const lg=fs.readFileSync('packages/common/src/logger.json','utf8');for(const code of ['76','77','78']){if(!new RegExp('\"'+code+'\"\\\\s*:').test(lg))bad.push('logger.json missing error '+code)}if(bad.length>0){console.error('BARREL/LOGGER FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK barrel routes preserved and logger.json carries 76/77/78')"</automated>
|
||||
<fails_when>non-zero exit(`client-modules/src/index.ts` 的 `./types` 桶导出被破坏,或 `logger.json` 缺 76/77/78)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `client-modules/src/types.ts` 顶层导出 `IClientPlugin` / `IClientPluginDefine`,`IClientCoreConfig.clientPlugins` 与 `IClientCore.getClientPlugin` 就位;`PluginStatus` 自 `@user/data-state` 导入复用、**未自建**
|
||||
- `client.ts` 的 `ClientCore` 含 `clientPlugins` 注册表(以 `config.clientPlugins ?? []` **运行时守卫**建立)、`getClientPlugin`、`protected` 的 `awake` / `initialize` / `afterLoad` 覆写(各自 `super.<stage>()` 先、再驱动渲染端插件,复用 `orderPlugins` 且**自 `@user/data-state` 导入**);未自建 `PluginStatus`
|
||||
- `client.ts` 的 `ClientCore` 含 `clientPlugins` 注册表(以 `config.clientPlugins ?? []` **运行时守卫**建立;重复 id `logger.error(78,…)` 且**不覆盖**)、`getClientPlugin`、`protected` 的 `awake` / `initialize` / `afterLoad` 覆写(各自 `super.<stage>()` 先、再驱动渲染端插件(含错误处置 76 / 77),复用 `orderPlugins` 且**自 `@user/data-state` 导入**);未自建 `PluginStatus`;未使用 `logger.error(75`
|
||||
- 统一 `load()` 保持单一入口(`CoreState.load()`),每阶段数据端先、渲染端后(D-90)
|
||||
- 结构门禁、方向/环断言(Task 1)、范围门禁全绿;未改 `src/**` 接线、未改 `data-base/**` / `client-base/**` / `common/**` / `package.json` / lockfile / `logger.json` / `data-state/src/types.ts` / 任何既有调用点 / 用户并发 WIP / 既有 `.planning` 文件
|
||||
- `09-20-SUMMARY.md` 已写(范围、四段式与拓扑/环口径、运行时守卫及其理由、数据端先/渲染端后、命名获批情形、`state` 参数类型最终裁定、**用户接线前「数据先渲染后」为空转的已知后果**、门禁结果、`open_items` 最终处置)
|
||||
- 结构门禁、方向/环断言(Task 1)、范围门禁全绿;未改 `src/**` 接线、未改 `data-base/**` / `client-base/**` / `package.json` / lockfile / `logger.json` 既有条目 / `data-state/src/types.ts` / 任何既有调用点 / 用户并发 WIP / 既有 `.planning` 文件
|
||||
- `09-20-SUMMARY.md` 已写(范围、四段式与拓扑/错误处置口径(76/77/78 + 不覆盖 + 排除加载流程)、运行时守卫及其理由、数据端先/渲染端后、命名获批情形、`logger.json` 新增条目、`state` 参数类型最终裁定、**用户接线前「数据先渲染后」为空转的已知后果**、门禁结果、`open_items` 最终处置)
|
||||
</acceptance_criteria>
|
||||
<done>渲染端镜像落码(类型 + `ClientCore` 注册表(带守卫)/ `getClientPlugin` / 三阶段覆写,`orderPlugins` 自 `@user/data-state` 导入)且结构断言全绿;统一 `load()` 数据端先 / 渲染端后成立;范围门禁全绿;`09-20-SUMMARY.md` 落盘</done>
|
||||
<done>渲染端镜像落码(类型 + `ClientCore` 注册表(带守卫,重复 id 不覆盖)/ `getClientPlugin` / 三阶段覆写(含错误处置 76/77/78),`orderPlugins` 自 `@user/data-state` 导入)且结构断言全绿;统一 `load()` 数据端先 / 渲染端后成立;范围门禁全绿;`09-20-SUMMARY.md` 落盘</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@ -338,17 +359,17 @@ Phase 9 · 任务 10 · 第 20 个计划 —— **实现数据端插件驱动 +
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 写入边界 | 本增量是写操作;只允许写 `files_modified` 白名单(`data-state` 插件模块 / 注册表 / 桶导出 / 测试;`client-modules` 类型与 `ClientCore`;`09-20-SUMMARY.md`) |
|
||||
| 范围边界 | 不得改 `src/**`(入口与接线)、`data-base/**`、`client-base/**`、`common/**`、`package.json` / lockfile、`data-state/src/types.ts`、既有插件配置调用点与用户并发 WIP |
|
||||
| 命名边界 | 除 D-88 已批准名外,任何新命名(`plugins` / `plugin.ts` / `orderPlugins` / 三接口**及其成员** / `clientPlugins` 注册表字段)与可见性变更(`private`→`protected`)须先经 Task 0 报备批准 |
|
||||
| 决策边界 | 渲染 `state` 参数类型、环处置语义、未知依赖、重复 id、`super(...)` 接线、测试文件是否允许,均以 `open_items` 待裁定;未获批准前取本计划默认 |
|
||||
| 写入边界 | 本增量是写操作;只允许写 `files_modified` 白名单(`data-state` 插件模块 / 注册表 / 桶导出 / 测试;`client-modules` 类型与 `ClientCore`;`common/src/logger.json` 的 error 段追加;`09-20-SUMMARY.md`) |
|
||||
| 范围边界 | 不得改 `src/**`(入口与接线)、`data-base/**`、`client-base/**`、`package.json` / lockfile、`data-state/src/types.ts`、既有插件配置调用点与用户其它并发 WIP;`common/**` 仅允许对 `src/logger.json` error 段追加 76/77/78 |
|
||||
| 命名边界 | 除 D-88 已批准名外,任何新命名(`plugins` / `plugin.ts` / `orderPlugins` / 三接口**及其成员**(含 `missing`)/ `clientPlugins` 注册表字段)与可见性变更(`private`→`protected`)须先经 Task 0 报备批准;logger 码 76/77/78 的消息文本须经 OI-11 批准 |
|
||||
| 决策边界 | 渲染 `state` 参数类型、`super(...)` 接线、测试文件是否允许,均以 `open_items` 待裁定;插件错误语义(76/77/78、不覆盖、排除加载流程)已由 D-92 裁定 |
|
||||
| 枚举复用边界 | 渲染端必须复用数据端 `PluginStatus`,不得自建同义枚举 |
|
||||
| 导入来源边界 | 渲染端 `orderPlugins` 必须自 `@user/data-state` 导入,不得指向 `client-modules` 内不存在的本地模块 |
|
||||
| 运行时安全边界 | 构造器建表必须带 `config.plugins ?? []` / `config.clientPlugins ?? []` 守卫;既有调用点缺失插件配置不得导致崩溃 |
|
||||
| 方向/环边界 | `data-state` 不得反向依赖 `@user/client-*`;插件系统不得引入循环引用 |
|
||||
| 依赖边界 | 不新增依赖;不改 `package.json` / lockfile |
|
||||
| 日志边界 | 复用既有 `error 75`;不改受 D-56 保护的 `packages/common/src/logger.json` |
|
||||
| 并发改动边界 | 编辑 `data-state/src/core.ts` / `client-modules/src/{types,client}.ts` 时只可在用户现有内容之上追加,不回退其改动;提交前 `git status` 逐条核对 |
|
||||
| 日志边界 | 插件错误码必须为新增 76/77/78(error 段);不得改动或删除 `logger.json` 既有条目(尤其用户 WIP 的 75);不得再复用 error 75 |
|
||||
| 并发改动边界 | 编辑 `data-state/src/core.ts` / `client-modules/src/{types,client}.ts` / `common/src/logger.json` 时只可在用户现有内容之上追加,不回退其改动;提交前 `git status` 逐条核对 |
|
||||
|
||||
**威胁级别说明(security_enforcement = ASVS L1,block on high):** 本计划为**引擎侧插件系统机制实现**,不执行第三方代码、不新增依赖、不引入网络面;寄存器逐条给出缓解,`high` 级(含崩溃 DoS 与供应链)均已给出 `mitigate`。
|
||||
|
||||
@ -357,37 +378,40 @@ Phase 9 · 任务 10 · 第 20 个计划 —— **实现数据端插件驱动 +
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-20-11 | Denial of Service | `CoreState` / `ClientCore` 构造器无守卫遍历 `config.plugins` / `config.clientPlugins`,而现存调用点(`ins.ts:11` 及 ~9 处 test/fixture、`client-modules/src/core.ts:6`)均省略该字段 → `for … of undefined` 抛错,破坏全部 `@user/data-state` 导入 | high | mitigate | Task 1 构造器用 `config.plugins ?? []`;Task 2 `ClientCore` 用 `config.clientPlugins ?? []`;Task 1 运行期单测断言省略 `plugins` 时构造与 `load()` 不抛错;两端结构门禁断言守卫字面量存在 |
|
||||
| T-09-20-01 | Tampering | 越界写入(`src/**` 接线 / `data-base/**` / `client-base/**` / `common/**` / `data-state/src/types.ts` / `package.json` / lockfile / 既有调用点) | medium | mitigate | `files_modified` 白名单 + prohibitions + Task 2 `SCOPE GATE`(`git status` 逐条校验,用户 WIP 白名单) |
|
||||
| T-09-20-02 | Tampering | 未经批准即使用新命名(含接口成员)/ 可见性变更 | medium | mitigate | Task 0 `blocking-human` 报备全部拟引入命名(OI-1..OI-4);prohibitions 明列「未获批前不得落码」 |
|
||||
| T-09-20-01 | Tampering | 越界写入(`src/**` 接线 / `data-base/**` / `client-base/**` / `common/**` 既有内容 / `data-state/src/types.ts` / `package.json` / lockfile / 既有调用点) | medium | mitigate | `files_modified` 白名单 + prohibitions + Task 2 `SCOPE GATE`(`git status` 逐条校验,用户 WIP 白名单,`logger.json` 列入允许面) |
|
||||
| T-09-20-02 | Tampering | 未经批准即使用新命名(含接口成员)/ 可见性变更 / 单方面定义 logger 消息文本 | medium | mitigate | Task 0 `blocking-human` 报备全部拟引入命名与 OI-11 消息文本(OI-1..OI-4、OI-11);prohibitions 明列「未获批前不得落码」 |
|
||||
| T-09-20-03 | Tampering | 自建 `PluginStatus` 导致双端枚举分叉 | medium | mitigate | Task 2 `RENDER PLUGIN GATE` 断言渲染端自 `@user/data-state` 导入 `PluginStatus` 且未自建枚举 |
|
||||
| T-09-20-12 | Tampering | 渲染端 `orderPlugins` 误指向 `client-modules` 内不存在的本地模块导致解析失败 | medium | mitigate | Task 2 `RENDER PLUGIN GATE` 断言 `orderPlugins` 自 `@user/data-state` 导入,且 `client.ts` 无本地插件模块导入 |
|
||||
| T-09-20-04 | Tampering | 依赖环 / 循环引用(拓扑排序缺陷或 `data-state` 反向依赖 `client-*`) | medium | mitigate | Task 1 运行期单测断言环上插件不初始化;`ACYCLIC GATE` 断言 `data-state/src` 零 `@user/client-*` 导入 |
|
||||
| T-09-20-04 | Tampering | 依赖环 / 循环引用(拓扑排序缺陷或 `data-state` 反向依赖 `client-*`) | medium | mitigate | Task 1 运行期单测断言环上插件(含下游)不初始化;`ACYCLIC GATE` 断言 `data-state/src` 零 `@user/client-*` 导入 |
|
||||
| T-09-20-13 | Tampering | 插件错误处置不符 D-92(复用 error 75 / 覆盖重复 id / 报错插件仍被驱动 / 下游未排除) | medium | mitigate | Task 1 构造器 `this.plugins.has` 判重 + `logger.error(78,…)` 不覆盖、`awake` 报 76/77;运行期单测断言环 / 依赖不存在插件(含下游)方法未被调用、重复 id 保留原插件;结构门禁断言 76/77/78 存在且无 `logger.error(75` |
|
||||
| T-09-20-14 | Tampering | 改动 / 删除 `logger.json` 既有条目(尤其用户 WIP 的 75) | medium | mitigate | prohibitions 明列仅追加 76/77/78、不得改动既有条目(D-56);Task 1 结构门禁断言 75 与 76/77/78 并存;Task 2 `SCOPE GATE` 仅允许该文件在允许面内 |
|
||||
| T-09-20-05 | Tampering | 破坏四段式语义(阶段顺序 / 数据端先渲染端后 / 状态推进错误) | medium | mitigate | Task 1 运行期单测断言调用序列与 `PluginStatus`;Task 2 结构断言三阶段 `protected` 覆写且 `super.<stage>()` 先 |
|
||||
| T-09-20-06 | Tampering | 回退 / 破坏用户并发改动(`data-state/src/core.ts` 空壳、`client-modules/src/types.ts` 注释改动) | medium | mitigate | prohibitions 明列「只能在其现有内容之上追加」;Task 2 `SCOPE GATE`;提交前逐条核对暂存面 |
|
||||
| T-09-20-06 | Tampering | 回退 / 破坏用户并发改动(`data-state/src/core.ts` 空壳、`client-modules/src/types.ts` 注释改动、`logger.json` error 75) | medium | mitigate | prohibitions 明列「只能在其现有内容之上追加」;Task 2 `SCOPE GATE`;提交前逐条核对暂存面 |
|
||||
| T-09-20-07 | Tampering | 改坏加载(`load()` 未驱动 `loader.start()` 或新增加载配置) | medium | mitigate | Task 1 结构断言 `load()` 含 `this.loader.start()`、第三段非空循环体,且不新增 `addCoreConfig` 接线;运行期单测证明 `load()` 完成 |
|
||||
| T-09-20-08 | Elevation of Privilege | 未经批准即执行 / 擅自新增依赖或 logger 码 | medium | mitigate | Task 0 `blocking-human`;prohibitions 明列不新增依赖、不改 `logger.json`(复用 error 75) |
|
||||
| T-09-20-08 | Elevation of Privilege | 未经批准即执行 / 擅自新增依赖或擅自改动 logger 既有码 | medium | mitigate | Task 0 `blocking-human`;prohibitions 明列不新增依赖、`logger.json` 仅追加 76/77/78、不改既有条目、不复用 75 |
|
||||
| T-09-20-09 | Information Disclosure | 改动既有注释 / jsDoc(AGENTS.md 禁改既有注释) | low | mitigate | prohibitions 明列不改任何既有注释 / jsDoc;`core.ts` / `types.ts` / `client.ts` 仅追加 |
|
||||
| T-09-20-10 | Repudiation | 未跑门禁即收口 / 未记录命名获批与 `open_items` | low | mitigate | Task 1/2 各有运行期 + 结构 + 范围门禁;`09-20-SUMMARY.md` 记录命名获批情形、运行时守卫理由与 `open_items` 最终处置 |
|
||||
| T-09-20-10 | Repudiation | 未跑门禁即收口 / 未记录命名获批、logger 消息文本与 `open_items` | low | mitigate | Task 1/2 各有运行期 + 结构 + 范围门禁;`09-20-SUMMARY.md` 记录命名获批情形、logger 新增条目、运行时守卫理由与 `open_items` 最终处置 |
|
||||
| T-09-20-SC | Tampering | npm/pip/cargo installs | high | mitigate | 本增量**不新增依赖**(prohibitions 明列;门禁范围拒绝 `package.json` / lockfile 变更);如需新增依赖须回 Task 0 报备获批 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- **Task 0**:预执行状态(插件类型 + `core.ts` 空壳 + `logger.json` error 75 + 未配置插件配置的既有调用点就位;`plugin.ts` / `plugin.test.ts` 不存在);人工关卡(用户回复「可以执行」;OI-1..OI-10 若有指定按其指定)。
|
||||
- **Task 1**:`plugin.ts` 导出 `orderPlugins` 与三接口、零 import;`orderPlugins` 拓扑序 / 环检测 / 未知依赖忽略经**运行期单测**断言;`CoreState.load()` 对样例插件断言四段调用顺序与 `PluginStatus` 推进、环上插件保持 `NotAwakened`;**省略 `plugins` 构造并 `load()` 不抛错(运行时守卫)**;`core.ts` 结构断言(`plugins` 注册表、`config.plugins ?? []` 守卫、三阶段 `protected`、`load()` 含 `this.loader.start()`、`logger.error(75`);`index.ts` 含 `./plugin` 桶导出;`ACYCLIC GATE`(`data-state/src` 零 `@user/client-*` 导入)。
|
||||
- **Task 2**:`types.ts` 顶层 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin` 就位、`PluginStatus` 复用(未自建);`client.ts` 的 `clientPlugins` 注册表(`config.clientPlugins ?? []` 守卫)/ `getClientPlugin` / 三阶段 `protected` 覆写(`super.<stage>()` 先)/ `orderPlugins` 自 `@user/data-state` 导入就位;`SCOPE GATE` 无越界改动(用户 WIP 白名单);桶路由未破坏;`09-20-SUMMARY.md` 存在。
|
||||
- **一致性**:统一 `load()` 单一入口且数据端阶段先 / 渲染端阶段后;渲染端 → 数据端单向依赖(无环);两端 `PluginStatus` 同源;注册表键与配置字段对应(`config.plugins` / `config.clientPlugins`);两端建表均带运行时守卫。
|
||||
- **Task 0**:预执行状态(插件类型 + `core.ts` 空壳 + `logger.json` error 75(用户 WIP)+ 未配置插件配置的既有调用点就位;`plugin.ts` / `plugin.test.ts` 不存在);人工关卡(用户回复「可以执行」;OI-1..OI-11 若有指定按其指定)。
|
||||
- **Task 1**:`logger.json` error 段新增 76/77/78 且保留 75 与其它既有条目;`plugin.ts` 导出 `orderPlugins` 与三接口(含 `missing`)、零 import;`orderPlugins` 拓扑序 / 环检测 / 依赖不存在检测 / 下游排除经**运行期单测**断言;`CoreState.load()` 对样例插件断言四段调用顺序与 `PluginStatus` 推进、环上及依赖不存在插件(含下游)保持 `NotAwakened` 且方法未被调用、重复 id 保留原插件;**省略 `plugins` 构造并 `load()` 不抛错(运行时守卫)**;`core.ts` 结构断言(`plugins` 注册表、`config.plugins ?? []` 守卫、`this.plugins.has` 判重 + `logger.error(78`、三阶段 `protected`、`load()` 含 `this.loader.start()`、`logger.error(76` / `logger.error(77` 且无 `logger.error(75`);`index.ts` 含 `./plugin` 桶导出;`ACYCLIC GATE`(`data-state/src` 零 `@user/client-*` 导入)。
|
||||
- **Task 2**:`types.ts` 顶层 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin` 就位、`PluginStatus` 复用(未自建);`client.ts` 的 `clientPlugins` 注册表(`config.clientPlugins ?? []` 守卫 + `this.clientPlugins.has` 判重 + `logger.error(78`)/ `getClientPlugin` / 三阶段 `protected` 覆写(`super.<stage>()` 先)/ `orderPlugins` 自 `@user/data-state` 导入 / 错误处置 `logger.error(76` / `logger.error(77` 就位;`SCOPE GATE` 无越界改动(用户 WIP 白名单、`logger.json` 允许面);桶路由未破坏;`09-20-SUMMARY.md` 存在。
|
||||
- **一致性**:统一 `load()` 单一入口且数据端阶段先 / 渲染端阶段后;渲染端 → 数据端单向依赖(无环);两端 `PluginStatus` 同源;注册表键与配置字段对应(`config.plugins` / `config.clientPlugins`);两端建表均带运行时守卫;插件错误码 76/77/78 唯一来源为 `logger.json` error 段且两端一致。
|
||||
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全量 `pnpm test:ci` / 全仓 TS 诊断数(用户 WIP 存在历史类型错误)。
|
||||
- **人工复核**:结论写入 `09-20-SUMMARY.md`(含范围、四段式与拓扑/环口径、运行时守卫及其理由、数据端先/渲染端后、命名获批情形、`state` 参数类型最终裁定、用户接线前空转的已知后果、门禁结果、`open_items` 最终处置)。
|
||||
- **人工复核**:结论写入 `09-20-SUMMARY.md`(含范围、四段式与拓扑/错误处置口径、运行时守卫及其理由、数据端先/渲染端后、命名获批情形、`logger.json` 新增条目、`state` 参数类型最终裁定、用户接线前空转的已知后果、门禁结果、`open_items` 最终处置)。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- **运行时安全(D-91 / D-56):** `CoreState` 以 `config.plugins ?? []`、`ClientCore` 以 `config.clientPlugins ?? []` 守卫建表;既有未配置插件配置的调用点(`ins.ts` 等)不改动且不崩溃;其既存类型不匹配记为用户 WIP。
|
||||
- **数据端驱动(D-87):** `CoreState` 由 `config.plugins`(守卫后)建立注册表;`awake`(同步)→ `initialize`(异步阻塞)→ loader 加载 → `afterLoad`(异步阻塞)四段式成立;各阶段按依赖拓扑序驱动并推进 `PluginStatus`;`getDataPlugin` 从注册表读取;依赖环经 `logger.error(75,...)` 记录且环上(含下游)插件不初始化(复用 error 75,未改 `logger.json`)。
|
||||
- **渲染端镜像(D-88):** `client-modules/src/types.ts` 顶层新增 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin`;复用数据端 `PluginStatus`(未自建);实现在 `ClientCore`;`orderPlugins` 自 `@user/data-state` 导入。
|
||||
- **数据端驱动(D-87):** `CoreState` 由 `config.plugins`(守卫后)建立注册表;`awake`(同步)→ `initialize`(异步阻塞)→ loader 加载 → `afterLoad`(异步阻塞)四段式成立;各阶段按依赖拓扑序驱动并推进 `PluginStatus`;`getDataPlugin` 从注册表读取。
|
||||
- **插件错误处置(D-92,修订 D-87):** 循环依赖 → `logger.error(76,…)`、依赖不存在 → `logger.error(77,…)`、重复 id → `logger.error(78,…)` 且**不覆盖原插件**;**报错插件及其下游全部不进入加载流程**(`status` 保持 `NotAwakened`、方法不被调用);`logger.json` error 段新增 76/77/78 且既有 75 及其它条目原样保留;不再复用 error 75。
|
||||
- **渲染端镜像(D-88 / D-92):** `client-modules/src/types.ts` 顶层新增 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin`;复用数据端 `PluginStatus`(未自建);实现在 `ClientCore`;`orderPlugins` 自 `@user/data-state` 导入;错误处置 76/77/78 与数据端一致(含不覆盖、排除加载流程)。
|
||||
- **统一 `load()`(D-89 / D-90):** 渲染端三阶段语义与数据端一致;统一 `load()` 中数据端阶段先、渲染端阶段后。
|
||||
- **边界(D-86 / D-91):** 仅引擎侧;未接入口/配置;未改 `initializeL0..L3` 等 config 口径;未处理 `initializeEnemy` / `initializeSave` / `initializeReplay` 接线;未新增依赖;未改 `package.json` / lockfile / `logger.json` / `data-state/src/types.ts` / 任何既有调用点 / 用户并发 WIP / 既有 `.planning` 文件。
|
||||
- **命名(AGENTS.md / D-04):** 拟引入命名(含接口成员)与 `private`→`protected` 变更均经 Task 0 报备获批后方才落码。
|
||||
- **门禁:** 运行期单测(数据端,含守卫断言)+ 结构断言(两端)+ 方向/环断言 + 范围门禁全绿;产出经 Task 0 获批。
|
||||
- **边界(D-86 / D-91):** 仅引擎侧;未接入口/配置;未改 `initializeL0..L3` 等 config 口径;未处理 `initializeEnemy` / `initializeSave` / `initializeReplay` 接线;未新增依赖;未改 `package.json` / lockfile / `logger.json` 既有条目 / `data-state/src/types.ts` / 任何既有调用点 / 用户并发 WIP / 既有 `.planning` 文件。
|
||||
- **命名(AGENTS.md / D-04):** 拟引入命名(含接口成员)与 `private`→`protected` 变更均经 Task 0 报备获批后方才落码;logger 码 76/77/78 消息文本经 OI-11 批准。
|
||||
- **门禁:** 运行期单测(数据端,含守卫 / 环 / 依赖不存在 / 重复 id 断言)+ 结构断言(两端 + `logger.json`)+ 方向/环断言 + 范围门禁全绿;产出经 Task 0 获批。
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user