mirror of
https://github.com/motajs/template.git
synced 2026-10-16 18:40:17 +08:00
docs(09): revise phase plan 09-20 (plugin system) for revision 2
This commit is contained in:
parent
1727230932
commit
8739141aff
@ -21,35 +21,39 @@ estimate:
|
||||
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()` 关系、**全部拟引入命名(`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` 给出指定,按指定执行。"
|
||||
- "**数据端插件驱动(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`(异步、阻塞,加载完毕后)。"
|
||||
- "**本计划的第一个 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` 给出指定,按指定执行。"
|
||||
- "**运行时安全(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`)。"
|
||||
- "**渲染端镜像(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` 内不存在含该导出的本地模块)。"
|
||||
- "**统一 `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`)、**不改 `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` / 三个接口、`private`→`protected` 可见性变更、`clientPlugins` 注册表字段)一律先入 `open_items`,由 Task 0 报备待批准后方可使用;未获批前不得擅自使用。"
|
||||
- "**仅引擎侧与范围边界(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 覆盖接口成员命名)。"
|
||||
artifacts:
|
||||
- path: packages-user/data-state/src/plugin.ts
|
||||
provides: "新增共享纯函数 `orderPlugins`(依赖拓扑排序 + 依赖环检测)与其结果/约束接口(`IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`);零 import、零副作用,可被数据端与渲染端复用(命名待批准,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 ?? []` 建立);实现 `awake` / `initialize` / `afterLoad` 四段式插件驱动(拓扑序 + `PluginStatus` 推进 + 依赖环处置 `logger.error(75,...)`);`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)"
|
||||
provides: "可运行单测:`orderPlugins` 的拓扑序与环检测;`CoreState` 四段式驱动的调用顺序与 `PluginStatus` 推进(构造时提供 `plugins` / `heroFace` / `events`,不依赖 DOM);**以及省略 `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`;覆写 `awake` / `initialize` / `afterLoad`(`super.` + 渲染端插件的同阶段驱动),使统一 `load()` 中数据端阶段先、渲染端阶段后"
|
||||
provides: "`ClientCore` 增 `clientPlugins` 注册表(由 `config.clientPlugins ?? []` 建立)与 `getClientPlugin`;自 `@user/data-state` 复用 `orderPlugins`;覆写 `awake` / `initialize` / `afterLoad`(`super.` + 渲染端插件的同阶段驱动),使统一 `load()` 中数据端阶段先、渲染端阶段后"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-20-SUMMARY.md
|
||||
provides: "本计划执行摘要:范围、四段式编排与拓扑/环处置口径、命名获批情形、渲染端镜像落点与 `state` 参数类型最终裁定、门禁结果、`open_items` 最终处置"
|
||||
provides: "本计划执行摘要:范围、四段式编排与拓扑/环处置口径、运行时守卫及其理由、命名获批情形、渲染端镜像落点与 `state` 参数类型最终裁定、**用户接线前「数据先渲染后」为空转的已知后果(D-91)**、门禁结果、`open_items` 最终处置"
|
||||
key_links:
|
||||
- "`orderPlugins`(`data-state/src/plugin.ts`)↔ `CoreState.plugins` / `ClientCore.clientPlugins` 两个注册表 —— 拓扑序与环检测的唯一实现,数据/渲染两端复用"
|
||||
- "`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()` 模式)"
|
||||
- "`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`)"
|
||||
- "`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)"
|
||||
assumptions:
|
||||
- "**注册表构建必须运行时安全:** 现存全部 `new CoreState({...})` 调用点(`data-state/src/ins.ts:11`、`data-state/__test__/coreNode.test.ts:8`、`data-state/src/replay/__test__/commands.test.ts:39`、`data-state/src/event/__test__/event.test.ts:81`、`data-state/src/__test__/coreEventLayer.test.ts:43`、`data-state/__test__/saveablesRoundTrip.test.ts:52`、`data-state/__test__/replayPlayback.test.ts:57`、`data-state/__test__/enemyCombination.test.ts:29`、`data-state/__test__/dataClosure.test.ts:24`、`data-state/__test__/fixtures/closed-loop.ts:65`)均未提供 `plugins`;`ClientCore` 调用点(`client-modules/src/core.ts:6`)亦未提供 `clientPlugins`。故注册表构建必须以 `?? []` 守卫(否则 `for … of undefined` 抛错)。这些调用点**不在本计划 `files_modified` 内、不改动**。"
|
||||
- "**既有调用点的类型不匹配属用户 WIP:** `ICoreStateConfig` 的 `plugins` / `heroFace` / `events` 为必填(用户已新增),而上述既有调用点与 `ClientCore` 的 `super({ loadStarter, coreURL })` 未提供——此为**既存类型错误**,属用户 WIP(D-56);本计划**不改 `data-state/src/types.ts`、不改那些调用点**,交由用户后续接线时统一核对;本计划门禁以**运行期行为 + 结构断言**为准(vitest 由 esbuild 转译、不做类型检查)。"
|
||||
- "**`config.events` 缺省为安全值:** `GameEventSystem` 构造器形参 `builtins: ReadonlyArray<BuiltInFunction> = []`(`packages-user/data-system/src/event/system.ts:30`),故测试中传入 `events: []`(或省略)不会导致运行期错误;数据端测试可安全构造 `CoreState`。"
|
||||
- "**测试可运行且不依赖 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()` 仍可完成。"
|
||||
@ -57,8 +61,11 @@ must_haves:
|
||||
- "**行号可能漂移:** 本计划引用行号为规划日实测;门禁以「文件存在 + 结构/行为断言」为准,不硬编码行号。"
|
||||
prohibitions:
|
||||
- "不得在用户回复通过 Task 0 关卡之前执行本计划的任何任务、或派发任何执行子代理。"
|
||||
- "**未经批准的命名一律不得使用:** 本计划拟引入的 `plugins` / `plugin.ts` / `orderPlugins` / `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult` / `clientPlugins`(注册表字段)以及 `awake`·`initialize`·`afterLoad` 的 `private`→`protected` 可见性变更,**必须先经 Task 0 报备并获用户批准**;未获批前不得落码。"
|
||||
- "**写入边界(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`)。"
|
||||
- "**未经批准的命名一律不得使用:** 本计划拟引入的 `plugins` / `plugin.ts` / `orderPlugins` / `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`(**及其成员** `id` / `dependencies` / `plugin` / `order` / `cyclic`)/ `clientPlugins`(注册表字段)以及 `awake`·`initialize`·`afterLoad` 的 `private`→`protected` 可见性变更,**必须先经 Task 0 报备并获用户批准**;未获批前不得落码。"
|
||||
- "**注册表构建必须运行时守卫(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`)。"
|
||||
- "**`.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` 逐条核对暂存面。"
|
||||
- "**不自建 `PluginStatus`:** 渲染端必须复用 `@user/data-state` 的 `PluginStatus`;不得在 `packages-user/client-modules` 内新建同义枚举或常量。"
|
||||
@ -67,7 +74,7 @@ must_haves:
|
||||
- "不得把门禁建立在「全仓 `pnpm check:type`」「全仓 `pnpm test:ci`」「`pnpm build`」或全仓 TS 诊断数之上(用户 WIP 存在历史类型错误)。"
|
||||
- "不得触碰、暂存、提交用户的并发改动(`AGENTS.md` / `09-CONTEXT.md` / `index.html` / `data-base/hero/*` / `common/logger.json` / `src/**`)。"
|
||||
open_items:
|
||||
- "**OI-1(共享排序模块与命名):** 提议新增文件 `packages-user/data-state/src/plugin.ts`,导出纯函数 `orderPlugins`(依赖拓扑排序 + 环检测)及接口 `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`(dev.md 要求对象类型须单独声明 interface,故拆为三接口)。**待用户报备批准**;备选 = 各端内联私有方法(但会引入两个不同名的私有方法且逻辑重复)。"
|
||||
- "**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-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 个新私有方法名(须另行报备)。"
|
||||
@ -76,7 +83,7 @@ must_haves:
|
||||
- "**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-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-10(新增测试文件):** 提议新增 `packages-user/data-state/src/__test__/plugin.test.ts`(测试文件按 AGENTS.md 免命名审批,但列入 `files_modified`)。若不允许新增测试,改为纯结构门禁(`orderPlugins` 与 `CoreState` 驱动的结构断言 + 既有调用点的运行时守卫断言)。**待用户确认**。"
|
||||
---
|
||||
|
||||
<objective>
|
||||
@ -84,12 +91,14 @@ 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`)。本计划把这套机制在**引擎侧**实现,并让渲染端镜像同一套系统(数据端先、渲染端后),**不接入口配置**(用户最后自行接线)。
|
||||
|
||||
**大致解决方案(本计划 = 09-20):**
|
||||
- **Task 0(`checkpoint:decision@blocking-human`)= 预执行汇报关卡**:汇报要做什么 / 为什么 / 大致怎么做 / 影响范围(含四段式编排、拓扑序与环处置、全部拟引入命名、写操作边界、门禁方式、`open_items` OI-1..OI-10),停下等待用户回复**「可以执行」**。
|
||||
- **Task 1(`tracer`)= 数据端插件驱动端到端最小切片**:新增共享纯函数 `orderPlugins`(拓扑排序 + 环检测);在 `CoreState` 建立 `plugins` 注册表、实现 `awake` / `initialize` / `afterLoad` 四段式驱动与 `load()` 编排;`getDataPlugin` 从注册表读取;数据端以**运行期单测**(拓扑序、四段调用顺序、`PluginStatus` 推进、环排除)验证端到端。
|
||||
- **Task 2 = 渲染端镜像 + 收口**:在 `client-modules/src/types.ts` 顶层新增 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin`(复用 `PluginStatus`);在 `client.ts` 的 `ClientCore` 建立 `clientPlugins` 注册表、实现 `getClientPlugin`、覆写 `awake` / `initialize` / `afterLoad` 使统一 `load()` 中数据端阶段先、渲染端阶段后;跑最终结构门禁与范围门禁并写 `09-20-SUMMARY.md`。
|
||||
**关键约束(运行时安全):** `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),由用户接线时统一核对。
|
||||
|
||||
**为何分两个执行任务且 Task 1 为 tracer:** 本增量是**分层功能实现**——数据端驱动是所有后续逻辑的地基(渲染端只在其上镜像同阶段);Task 1 先把「一条路径」端到端打通(注册 → 拓扑排序 → 四阶段调用 → 状态推进 → 环排除),用**运行期单测**证明机制成立,Task 2 再在其上横向铺开渲染端镜像。相比直接横向分「数据端/渲染端各写一半」,tracer 能更早暴露拓扑/阶段语义问题。渲染端因 `ClientCore` 依赖 canvas 无法在 Node 下运行期验证,故其门禁为结构断言。
|
||||
**大致解决方案(本计划 = 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 1 为 tracer:** 本增量是**分层功能实现**——数据端驱动是所有后续逻辑的地基(渲染端只在其上镜像同阶段);Task 1 先把「一条路径」端到端打通(构造守卫 → 注册 → 拓扑排序 → 四阶段调用 → 状态推进 → 环排除),用**运行期单测**证明机制成立,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` 所列路径外无任何写入。
|
||||
|
||||
@ -120,16 +129,17 @@ 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` 存在;`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...);`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)
|
||||
- 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/client-modules/src/types.ts(`IClientCoreConfig:4` / `IClientCore:13`);packages-user/client-modules/src/client.ts(`ClientCore:47`、`super(...):67-70`)
|
||||
- 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-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`);AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明);dev.md(命名规则、对象类型须声明 interface、模块无副作用、无循环引用)
|
||||
- packages/common/src/logger.json(error `75`);eslint.config.js(`eslint.configs.recommended` → 启用 `no-empty`);AGENTS.md(执行前须获批、命名须先反馈、修改方式须预先声明);dev.md(命名规则、对象类型须声明 interface、模块无副作用、无循环引用)
|
||||
</read_first>
|
||||
<action>
|
||||
**停下并向用户汇报,等待用户回复「可以执行」后才继续;汇报本身只读。**
|
||||
@ -140,35 +150,39 @@ Phase 9 · 任务 10 · 第 20 个计划 —— **实现数据端插件驱动 +
|
||||
|
||||
③ **数据端设计(D-87):** 构造器由 `config.plugins` 建注册表(每项 `{ status: PluginStatus.NotAwakened, plugin: define }`);`awake`(同步)→ `initialize`(异步阻塞)→ 执行 loader 加载(驱动 `this.loader.start()`)→ `afterLoad`(异步阻塞);各阶段按**依赖拓扑序**调用 `plugin.awake/initialize/afterLoad(this, deps)` 并推进 `status`;`getDataPlugin` 从注册表读取。
|
||||
|
||||
④ **依赖拓扑序与环:** 由共享纯函数 `orderPlugins` 计算「可初始化拓扑序」与「环上(含下游)插件 id」;环上插件三阶段均跳过、`status` 保持 `NotAwakened`,记录 `logger.error(75, id, info)`(**复用既有 error 75,不改 `logger.json`**)。
|
||||
④ **运行时守卫(关键理由,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`**,由用户后续接线时统一核对。
|
||||
|
||||
⑤ **渲染端镜像(D-88):** 类型放 `packages-user/client-modules/src/types.ts` **顶层**(与数据端对齐):`IClientPlugin` / `IClientPluginDefine`、`IClientCoreConfig.clientPlugins`、`IClientCore.getClientPlugin`;**复用数据端 `PluginStatus`**(自 `@user/data-state` 导入,不自建枚举);实现在 `ClientCore`。
|
||||
⑤ **依赖拓扑序与环:** 由共享纯函数 `orderPlugins` 计算「可初始化拓扑序」与「环上(含下游)插件 id」;环上插件三阶段均跳过、`status` 保持 `NotAwakened`,记录 `logger.error(75, id, info)`(**复用既有 error 75,不改 `logger.json`**)。
|
||||
|
||||
⑥ **统一 `load()`(D-89 / D-90):** 渲染端 `awake` / `initialize` / `afterLoad` 语义与数据端一致;**数据端阶段先、渲染端阶段后**——每阶段先驱动数据插件(L0-L3),再驱动渲染插件(L4-L5)。
|
||||
⑥ **渲染端镜像(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` 内不存在含该导出的本地模块)。
|
||||
|
||||
⑦ **拟引入命名(须报备批准,OI-1..OI-4):** 数据端注册表字段 `plugins`(OI-2);共享模块 `packages-user/data-state/src/plugin.ts` 及其导出 `orderPlugins` / `IPluginDepends` / `IPluginHolder` / `IPluginOrderResult`(OI-1);渲染端注册表字段 `clientPlugins`(OI-3);`CoreState.awake` / `initialize` / `afterLoad` 由 `private` 改为 `protected`(供 `ClientCore` 覆写,OI-4)。**D-88 已批准可直接使用:** `IClientPlugin` / `IClientPluginDefine` / `getClientPlugin` / 配置字段 `clientPlugins`。
|
||||
⑦ **统一 `load()`(D-89 / D-90):** 渲染端 `awake` / `initialize` / `afterLoad` 语义与数据端一致;**数据端阶段先、渲染端阶段后**——每阶段先驱动数据插件(L0-L3),再驱动渲染插件(L4-L5)。`load()` 第三段的 `for await` 循环体**非空**(消费进度值),以符合 eslint `no-empty`。
|
||||
|
||||
⑧ **待裁定项(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-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`。
|
||||
|
||||
⑨ **产出文件:** 新增 `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`。
|
||||
⑨ **待裁定项(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)。
|
||||
|
||||
⑩ **不在本增量(D-86 / D-91):** 不接入口/配置(`src/**`);不改 `initializeL0..L3` 等 config 口径;不处理 `initializeEnemy` / `initializeSave` / `initializeReplay` 的接线;不新增依赖、不改 `package.json` / lockfile / `logger.json`。
|
||||
⑩ **产出文件:** 新增 `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`。
|
||||
|
||||
⑪ **验证方式:** 数据端 = **运行期单测**(`orderPlugins` 拓扑序与环检测;`CoreState.load()` 的四段调用顺序与 `PluginStatus` 推进;环上插件不初始化)——`pnpm exec vitest run packages-user/data-state/src/__test__/plugin.test.ts`;两端 = **结构断言**(`plugin.ts` 导出 `orderPlugins`;`core.ts` 含 `plugins` 注册表 / `protected` 三阶段 / `load()` 四段 / `PluginStatus` 推进 / `logger.error(75`;`client-modules/src/types.ts` 含 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin` 且 `PluginStatus` 自 `@user/data-state` 导入、未自建;`client.ts` 含 `clientPlugins` / `getClientPlugin` / 三阶段覆写);**方向/环**结构断言(`data-state/src/**` 不导入 `@user/client-modules` / `@user/client-base`);**范围门禁**(`git status` 仅 `files_modified` 变更,用户 WIP 零触碰)。**不设全仓 `check:type` / `build` / 全量 `test:ci` 门禁。**
|
||||
⑪ **不在本增量(D-86 / D-91):** 不接入口/配置(`src/**`);不改 `initializeL0..L3` 等 config 口径;不处理 `initializeEnemy` / `initializeSave` / `initializeReplay` 的接线;不新增依赖、不改 `package.json` / lockfile / `logger.json`。
|
||||
|
||||
⑫ **修改方式声明(AGENTS.md):** 本计划执行含文件写入,由**派发子代理执行**(`gsd-executor`),非内联执行。
|
||||
⑫ **已知后果(D-91,须一并告知并记入 `09-20-SUMMARY.md`):** 渲染端调用点(`client-modules/src/core.ts`、`src/client/entry.ts`)尚不传 `clientPlugins`,数据端 `ins.ts` 尚不传 `plugins`,故「数据先渲染后」的顺序在**用户接线前是空转的**(注册表为空、无插件可驱动);本计划只保证机制成立且既有调用点不因缺配置而崩溃。
|
||||
|
||||
⑬ **关卡性质:** **无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户回复;回复「可以执行」即视为通过。** 若用户就 OI-1..OI-10 给出指定,按指定执行。
|
||||
⑬ **验证方式:** 数据端 = **运行期单测**(`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` 门禁。**
|
||||
|
||||
⑭ **修改方式声明(AGENTS.md):** 本计划执行含文件写入,由**派发子代理执行**(`gsd-executor`),非内联执行。
|
||||
|
||||
⑮ **关卡性质:** **无论触发方式或 `mode: yolo` / `workflow.auto_advance`,执行者 / 子代理必须停下等待用户回复;回复「可以执行」即视为通过。** 若用户就 OI-1..OI-10 给出指定,按指定执行。
|
||||
</action>
|
||||
<decision>是否按本计划(09-20)执行「任务 10 · 数据端插件驱动 + 渲染端插件系统镜像(仅引擎侧)」——在 `CoreState` 实现注册表 + 四段式驱动(`awake` 同步 → `initialize` 异步阻塞 → loader 加载 → `afterLoad` 异步阻塞)+ 依赖拓扑序与环处置 + `load()` 编排;在渲染端 `client-modules/src/types.ts` 顶层新增 `IClientPlugin` / `IClientPluginDefine` / `clientPlugins` / `getClientPlugin`(复用 `PluginStatus`)并在 `ClientCore` 实现,使统一 `load()` 中数据端阶段先、渲染端阶段后;不接入口/配置、不改 config 口径、不新增依赖、不动用户并发 WIP。</decision>
|
||||
<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>
|
||||
<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` 条目。渲染端因 `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`)。本计划拟引入若干新命名(`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 下运行期验证,故以结构断言为门禁。行号为规划日实测、可能漂移,门禁以结构/行为断言为准。
|
||||
</context>
|
||||
<options>
|
||||
<option id="execute-default">
|
||||
<name>可以执行(按本计划默认口径:`IClientCore` 为渲染 `state` 参数类型、环处置为记录 `logger.error(75,...)` + 环上排除不抛出、依赖 id 不存在时忽略、重复 id 后者覆盖不告警、不改 `super(...)`、允许新增测试文件)</name>
|
||||
<pros>四段式编排与数据端先/渲染端后与 D-87/D-89/D-90 逐字一致;拓扑/环机制有运行期单测兜底;复用既有 error 75、不触碰受保护的 `logger.json`;仅引擎侧、不动用户 WIP</pros>
|
||||
<cons>引入若干待批准命名(`plugins` / `plugin.ts` / `orderPlugins` / 三接口 / `clientPlugins` 注册表字段)与一处 `private`→`protected` 可见性变更</cons>
|
||||
<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>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>修订 OI-1..OI-10 的任一取向后重新汇报</name>
|
||||
@ -178,22 +192,22 @@ Phase 9 · 任务 10 · 第 20 个计划 —— **实现数据端插件驱动 +
|
||||
</options>
|
||||
<resume-signal>回复「可以执行」即可(默认采用 OI-5=`IClientCore`、OI-6=`logger.error(75,...)`+不抛出、OI-7=忽略、OI-8=后者覆盖不告警、OI-9=不改 `super(...)`、OI-10=允许新增测试)。如需调整任一 `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/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');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 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>
|
||||
<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>
|
||||
<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>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 用户回复中明确包含「可以执行」
|
||||
- 汇报内容涵盖:要解决的问题、范围(数据端驱动 + 渲染端镜像、仅引擎侧)、四段式 `load()` 编排(含第三段驱动 `loader.start()`)、依赖拓扑序与环处置、数据端注册表 `plugins` 与 `getDataPlugin`、渲染端镜像命名与 `ClientCore` 落点、渲染 `state` 参数类型待定、统一 `load()` 数据端先/渲染端后、**全部拟引入命名与 `private`→`protected` 可见性变更**、产出文件清单、不在本增量的内容、验证方式(运行期单测 + 结构断言 + 范围门禁,无全仓门禁)、OI-1..OI-10
|
||||
- 汇报中已原样告知:**写操作边界(除 `files_modified` 外不写;不改 `src/**` / `data-base/**` / `client-base/**` / `common/**` / `package.json` / lockfile / 用户并发 WIP;`.planning/**` 仅新增 `09-20-SUMMARY.md`)**、**不新增依赖 / 不改 `logger.json`(复用 error 75)**、**未经批准的命名一律不得使用**、**用户并发改动须保留(编辑同文件时在其现有内容之上追加)**
|
||||
- 汇报内容涵盖:要解决的问题、范围(数据端驱动 + 渲染端镜像、仅引擎侧)、四段式 `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)**
|
||||
- 汇报中已原样告知**关卡性质**(必须停下等待;不因触发方式或 `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` 已含空壳。</precondition>
|
||||
<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>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-87 / D-91;D-04 / D-56)
|
||||
@ -201,23 +215,25 @@ Phase 9 · 任务 10 · 第 20 个计划 —— **实现数据端插件驱动 +
|
||||
- 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 })` 构造范式);packages-user/data-system/src/event/system.ts:30(`builtins = []` 默认值)
|
||||
- AGENTS.md(命名须先获批、禁改既有注释、修改方式须预先声明);dev.md(命名规则、对象类型须声明 interface、无副作用 / 无循环引用 / 无 `import type`、非空判断约定、jsDoc 规范)
|
||||
- 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 = []` 默认值)
|
||||
- eslint.config.js:13(`eslint.configs.recommended` → `no-empty` 生效);AGENTS.md(命名须先获批、禁改既有注释、修改方式须预先声明);dev.md(命名规则、对象类型须声明 interface、无副作用 / 无循环引用 / 无 `import type`、非空判断约定、jsDoc 规范)
|
||||
</read_first>
|
||||
<action>
|
||||
(1) **新增共享纯函数模块 `packages-user/data-state/src/plugin.ts`(命名待批准,OI-1):** 零 import、零顶层副作用;按 dev.md「对象类型须单独声明 interface」声明并导出:
|
||||
- `IPluginDepends`:`{ readonly id: string; readonly dependencies: string[] }`(具备插件标识符与依赖信息的结构)。
|
||||
- `IPluginHolder<T extends IPluginDepends>`:`{ readonly plugin: T }`(包装插件定义的容器结构)。
|
||||
- `IPluginOrderResult<T>`:`{ readonly order: T[]; readonly 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(方法注释用换行风格、成员注释用不换行风格)。
|
||||
- **不修改** `packages-user/data-state/src/types.ts`(用户新增类型保持原样)。
|
||||
(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),由用户接线时统一核对;本任务仅通过**运行时守卫**保证它们不崩溃。
|
||||
|
||||
(2) **补桶导出:** 在 `packages-user/data-state/src/index.ts` 追加 `export * from './plugin';`(同目录项,符合 D-69 桶边界;与既有导出行同风格、不增删其它行)。
|
||||
(1) **新增共享纯函数模块 `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(方法注释用换行风格、成员注释用不换行风格)。
|
||||
- **不修改** `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/core.ts`(在用户现有内容之上补全,不回退其改动):**
|
||||
- **import:** 自 `./plugin` 引入 `orderPlugins`(`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 定义之后)遍历 `config.plugins`,为每个 `IDataPluginDefine` 写入 `this.plugins.set(id, { status: PluginStatus.NotAwakened, plugin: define })`;**重复 id 后者覆盖(OI-8,不新增告警)**;用 `this.plugins.has(id)` 判重(字面量/对象非空按 dev.md:对象用 `!`、字面量用 `!isNil`)。
|
||||
- **构造器建表(运行时守卫,关键):** 在 `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)。
|
||||
- **三阶段(可见性由 `private` 改为 `protected`,OI-4):**
|
||||
- `protected awake(): void`:同步;按拓扑序对 `status === PluginStatus.NotAwakened` 的插件调用 `plugin.awake(this, deps)`,随后置 `status = PluginStatus.Awakened`。
|
||||
@ -225,89 +241,94 @@ Phase 9 · 任务 10 · 第 20 个计划 —— **实现数据端插件驱动 +
|
||||
- `protected async afterLoad(): Promise<void>`:异步阻塞;对 `status === PluginStatus.Initialized` 的插件 `await plugin.afterLoad(this, deps)`,随后置 `status = PluginStatus.Loaded`。
|
||||
- **环上及其下游插件**在 `orderPlugins(...).order` 中缺席,故三阶段均自然跳过(`status` 保持 `NotAwakened`)。
|
||||
- **`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 _ of this.loader.start()) {} await this.afterLoad();`(第三段驱动 `this.loader.start()` 至完成,对齐 `loader.test.ts:163`;第三段循环体为空、符合 dev.md 未使用变量约定)。**不**在本方法内新增加载器配置(接线归用户,D-91)。
|
||||
- **`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`(环上不初始化)。
|
||||
- **运行时守卫(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` 对 `{x: [y], y: [x]}` → `cyclic` 含 `x` 与 `y`,`order` 不含 `x`/`y`。
|
||||
- `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`。
|
||||
- `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` 与测试、补桶导出行、改写 `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` 推进不符 / 环上插件被初始化)</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(!/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')"</automated>
|
||||
<fails_when>non-zero exit, 或 stderr 含 "DATA PLUGIN GATE FAILED"(`plugin.ts` 未导出 `orderPlugins`/三接口或含 import / `core.ts` 未用 `orderPlugins`、缺 `plugins` 注册表、三阶段非 `protected`、`load()` 未驱动 `loader.start()`、缺 `PluginStatus` 推进、环未走 `logger.error(75,...)`、`index.ts` 缺 `./plugin` 桶导出)</fails_when>
|
||||
<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>
|
||||
<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`,零 import、零顶层副作用(dev.md)
|
||||
- `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()`)、`PluginStatus` 推进、环走 `logger.error(75,...)`
|
||||
- `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()` 不抛错
|
||||
- `index.ts` 新增 `export * from './plugin'`(仅此一行,符合 D-69)
|
||||
- 数据端未引入对渲染端包的反向导入(方向/环结构断言全绿)
|
||||
- 未改 `types.ts` / 未改 `initializeEnemy` / `initializeSave` / `initializeReplay` 及其注释 / 未改任何既有注释
|
||||
- 未改 `types.ts` / 未改 `ins.ts` 与任何既有 test/fixture 调用点 / 未改 `initializeEnemy` / `initializeSave` / `initializeReplay` 及其注释 / 未改任何既有注释
|
||||
</acceptance_criteria>
|
||||
<done>`CoreState` 四段式插件驱动端到端成立:注册表建立、拓扑序驱动、状态推进、环排除,均有运行期单测与结构断言兜底;`plugin.ts` 共享排序函数就位;`index.ts` 桶导出就位</done>
|
||||
<done>`CoreState` 四段式插件驱动端到端成立:注册表(带守卫)建立、拓扑序驱动、状态推进、环排除,均有运行期单测与结构断言兜底;省略 `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` 与数据端驱动就位、桶导出已加);Task 0 已闭环(OI-3 / OI-4 / OI-5 / OI-9 已获批或按用户指定)。</precondition>
|
||||
<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>
|
||||
<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)
|
||||
- packages-user/data-state/src/types.ts(`PluginStatus:10` / `IDataPlugin:21` / `IDataPluginDefine:28`)
|
||||
- packages-user/data-state/src/plugin.ts(Task 1 产出:`orderPlugins` 与接口签名,供渲染端复用)
|
||||
- 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`)
|
||||
- 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-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>
|
||||
<action>
|
||||
(1) **`packages-user/client-modules/src/types.ts`(顶层,与数据端对齐,D-88;在用户现有内容之上追加):**
|
||||
- import 区追加 `PluginStatus`(自 `@user/data-state`;`IClientCoreConfig` 已在用 `ICoreStateConfig`,合并为同一 import 语句,保持 dev.md「import 之间无注释空行」)。
|
||||
- 新增并导出 `IClientPlugin<T extends IClientPluginDefine>`:`{ status: PluginStatus; plugin: T }`(镜像 `IDataPlugin<T>`;写 jsDoc,成员注释不换行风格)。
|
||||
- 新增并导出 `IClientPluginDefine`:`{ readonly id: string; readonly dependencies: string[]; awake(state, dependencies): void; initialize(state, dependencies): Promise<void>; afterLoad(state, dependencies): Promise<void> }`(镜像 `IDataPluginDefine`;**`state` 参数类型 = `IClientCore`(OI-5 默认)**;`dependencies` 类型 = `IClientPlugin<IClientPluginDefine>[]`;三方法写换行风格 jsDoc、方法间空行)。
|
||||
- 新增并导出 `IClientPlugin<T extends IClientPluginDefine>`:成员 `status`(`PluginStatus`)与 `plugin`(`T`)(镜像 `IDataPlugin<T>`;写 jsDoc,成员注释不换行风格)。
|
||||
- 新增并导出 `IClientPluginDefine`:成员 `id`(`readonly string`)、`dependencies`(`readonly string[]`)、以及 `awake(state, dependencies): void` / `initialize(state, dependencies): Promise<void>` / `afterLoad(state, dependencies): Promise<void>`(镜像 `IDataPluginDefine`;**`state` 参数类型 = `IClientCore`(OI-5 默认)**;`dependencies` 类型 = `IClientPlugin<IClientPluginDefine>[]`;三方法写换行风格 jsDoc、方法间空行)。
|
||||
- `IClientCoreConfig` 追加 `readonly clientPlugins: IClientPluginDefine[];`(**D-88 已批准的配置字段名**;写 jsDoc)。
|
||||
- `IClientCore` 追加 `getClientPlugin<T extends IClientPluginDefine>(id: string): Readonly<IClientPlugin<T>> | null;`(**D-88 已批准**;写换行风格 jsDoc)。
|
||||
- **不自建 `PluginStatus`**(必须自 `@user/data-state` 导入复用)。
|
||||
|
||||
(2) **`packages-user/client-modules/src/client.ts`(`ClientCore`,在用户现有内容之上追加):**
|
||||
- import 区:自 `./types` 追加 `IClientPlugin` / `IClientPluginDefine`(`IClientCore` / `IClientCoreConfig` 已在用);自 `./plugin` 追加 `orderPlugins`(经 `@user/data-state` 导入,指向 Task 1 的模块)。import 区保持 dev.md 规范。
|
||||
- import 区:自 `./types` 追加 `IClientPlugin` / `IClientPluginDefine`(`IClientCore` / `IClientCoreConfig` 已在用);**自 `@user/data-state` 导入 `orderPlugins`**——正确来源是本包(`packages-user/client-modules`)内不存在的本地模块之外的 `@user/data-state` 包(Task 1 的 `plugin.ts` 经该包桶导出)。import 区保持 dev.md 规范。
|
||||
- **注册表字段(OI-3):** 声明 `private readonly clientPlugins: Map<string, IClientPlugin<IClientPluginDefine>> = new Map();`(显式类型、`private readonly`;与既有 Layer 字段同区)。
|
||||
- **构造器建表:** 在 `constructor(config: IClientCoreConfig)` 内遍历 `config.clientPlugins`,为每个 `IClientPluginDefine` 写入 `this.clientPlugins.set(id, { status: PluginStatus.NotAwakened, plugin: define })`;重复 id 后者覆盖(OI-8 口径,不新增告警)。**不改** `super(...)` 调用(OI-9 默认:不传 `config`)。
|
||||
- **构造器建表(运行时守卫,关键):** 在 `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`)。
|
||||
- **`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.<stage>()`(数据端阶段先,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)` 取拓扑序、跳过 `cyclic`、逐项解析 `dependencies`、按阶段调用 `plugin.awake/initialize/afterLoad(this, deps)` 并推进 `status`。私有方法(如有)放在其调用者之前(dev.md)。
|
||||
- **不改** `init()` / `initMapExtensions()` 及其注释;不改任何既有注释 / jsDoc。
|
||||
|
||||
(3) **收口与 SUMMARY(`09-20-SUMMARY.md`):** 记录本计划范围、四段式编排与拓扑/环处置口径(含数据端先 / 渲染端后)、数据端注册表与驱动的落点、渲染端镜像落点与 `IClientCore` 为 `state` 参数类型的最终情形、`open_items`(OI-1..OI-10)的获批/裁定结果、门禁结果(运行期单测 + 结构断言 + 方向环 + 范围门禁)、未实现内容(入口/接线属用户)。
|
||||
(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)的获批/裁定结果、门禁结果(运行期单测 + 结构断言 + 方向环 + 范围门禁)、未实现内容(入口/接线属用户)。
|
||||
</action>
|
||||
<reversibility rating="reversible">在 `types.ts` 追加渲染端插件类型、在 `client.ts` 追加渲染端注册表与三阶段覆写;`git checkout` / 删除追加块可整体回退;未改数据端已落地内容。</reversibility>
|
||||
<reversibility rating="reversible">在 `types.ts` 追加渲染端插件类型、在 `client.ts` 追加渲染端注册表与三阶段覆写;`git checkout` / 删除追加块可整体回退;未改数据端已落地内容、未改任何调用点。</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(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('orderPlugins')<0)bad.push('client.ts does not reuse orderPlugins');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')"</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` 注册表 / `getClientPlugin` / 三阶段 `protected` 覆写 / `super.<stage>()` / `orderPlugins` 复用 / 自建 `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(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>
|
||||
<!-- 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/common/**`、`package.json`、lockfile、`src/**`、其它 `.planning/**`)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const cl=fs.readFileSync('packages-user/client-modules/src/client.ts','utf8');if(/@user\/client-base/.test(cl)&&!/IClientPlugin/.test(cl))bad.push('sanity');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, 或 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>
|
||||
</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`);未自建 `PluginStatus`
|
||||
- `client.ts` 的 `ClientCore` 含 `clientPlugins` 注册表(以 `config.clientPlugins ?? []` **运行时守卫**建立)、`getClientPlugin`、`protected` 的 `awake` / `initialize` / `afterLoad` 覆写(各自 `super.<stage>()` 先、再驱动渲染端插件,复用 `orderPlugins` 且**自 `@user/data-state` 导入**);未自建 `PluginStatus`
|
||||
- 统一 `load()` 保持单一入口(`CoreState.load()`),每阶段数据端先、渲染端后(D-90)
|
||||
- 结构门禁、方向/环断言(Task 1)、范围门禁全绿;未改 `src/**` 接线、未改 `data-base/**` / `client-base/**` / `common/**` / `package.json` / lockfile / `logger.json` / 用户并发 WIP / 既有 `.planning` 文件
|
||||
- `09-20-SUMMARY.md` 已写(范围、四段式与拓扑/环口径、数据端先/渲染端后、命名获批情形、`state` 参数类型最终裁定、门禁结果、`open_items` 最终处置)
|
||||
- 结构门禁、方向/环断言(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` 最终处置)
|
||||
</acceptance_criteria>
|
||||
<done>渲染端镜像落码(类型 + `ClientCore` 注册表 / `getClientPlugin` / 三阶段覆写)且结构断言全绿;统一 `load()` 数据端先 / 渲染端后成立;范围门禁全绿;`09-20-SUMMARY.md` 落盘</done>
|
||||
<done>渲染端镜像落码(类型 + `ClientCore` 注册表(带守卫)/ `getClientPlugin` / 三阶段覆写,`orderPlugins` 自 `@user/data-state` 导入)且结构断言全绿;统一 `load()` 数据端先 / 渲染端后成立;范围门禁全绿;`09-20-SUMMARY.md` 落盘</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@ -318,50 +339,55 @@ 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、以及用户并发 WIP |
|
||||
| 命名边界 | 除 D-88 已批准名外,任何新命名(`plugins` / `plugin.ts` / `orderPlugins` / 三接口 / `clientPlugins` 注册表字段)与可见性变更(`private`→`protected`)须先经 Task 0 报备批准 |
|
||||
| 范围边界 | 不得改 `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` 待裁定;未获批准前取本计划默认 |
|
||||
| 枚举复用边界 | 渲染端必须复用数据端 `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` 逐条核对 |
|
||||
|
||||
**威胁级别说明(security_enforcement = ASVS L1,block on high):** 本计划为**引擎侧插件系统机制实现**,不执行第三方代码、不新增依赖、不引入网络面,**不存在未缓解的 `high` 级威胁**;寄存器均为 `low`/`medium` 并逐条给出缓解。
|
||||
**威胁级别说明(security_enforcement = ASVS L1,block on high):** 本计划为**引擎侧插件系统机制实现**,不执行第三方代码、不新增依赖、不引入网络面;寄存器逐条给出缓解,`high` 级(含崩溃 DoS 与供应链)均已给出 `mitigate`。
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-20-01 | Tampering | 越界写入(`src/**` 接线 / `data-base/**` / `client-base/**` / `common/**` / `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-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-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-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-07 | Tampering | 改坏加载(`load()` 未驱动 `loader.start()` 或新增加载配置) | medium | mitigate | Task 1 结构断言 `load()` 含 `this.loader.start()` 且不新增 `addCoreConfig` 接线;运行期单测证明 `load()` 完成 |
|
||||
| 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-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 | 未跑门禁即收口 / 未记录命名获批与 `open_items` | low | mitigate | Task 1/2 各有运行期 + 结构 + 范围门禁;`09-20-SUMMARY.md` 记录命名获批情形、运行时守卫理由与 `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`;`core.ts` 结构断言(`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` 注册表 / `getClientPlugin` / 三阶段 `protected` 覆写(`super.<stage>()` 先)/ `orderPlugins` 复用就位;`SCOPE GATE` 无越界改动(用户 WIP 白名单);桶路由未破坏;`09-20-SUMMARY.md` 存在。
|
||||
- **一致性**:统一 `load()` 单一入口且数据端阶段先 / 渲染端阶段后;渲染端 → 数据端单向依赖(无环);两端 `PluginStatus` 同源;注册表键与配置字段对应(`config.plugins` / `config.clientPlugins`)。
|
||||
- **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`);两端建表均带运行时守卫。
|
||||
- **无全仓门禁**:全程不使用 `pnpm check:type` / `pnpm build` / 全量 `pnpm test:ci` / 全仓 TS 诊断数(用户 WIP 存在历史类型错误)。
|
||||
- **人工复核**:结论写入 `09-20-SUMMARY.md`(含范围、四段式与拓扑/环口径、数据端先/渲染端后、命名获批情形、`state` 参数类型最终裁定、门禁结果、`open_items` 最终处置)。
|
||||
- **人工复核**:结论写入 `09-20-SUMMARY.md`(含范围、四段式与拓扑/环口径、运行时守卫及其理由、数据端先/渲染端后、命名获批情形、`state` 参数类型最终裁定、用户接线前空转的已知后果、门禁结果、`open_items` 最终处置)。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- **数据端驱动(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`。
|
||||
- **运行时安全(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` 导入。
|
||||
- **统一 `load()`(D-89 / D-90):** 渲染端三阶段语义与数据端一致;统一 `load()` 中数据端阶段先、渲染端阶段后。
|
||||
- **边界(D-86 / D-91):** 仅引擎侧;未接入口/配置;未改 `initializeL0..L3` 等 config 口径;未处理 `initializeEnemy` / `initializeSave` / `initializeReplay` 接线;未新增依赖;未改 `package.json` / lockfile / `logger.json` / 用户并发 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 报备获批后方才落码。
|
||||
- **门禁:** 运行期单测(数据端,含守卫断言)+ 结构断言(两端)+ 方向/环断言 + 范围门禁全绿;产出经 Task 0 获批。
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user