zizi 6f854851f5 feat(game-runtime): core-protocol-v0 冻结(Gate0.1 受控面补丁,38/38 绿)
- 受控面 v0 六项冻结:getContext2d/onFrame/getInput/getAudioContext/time/random
- getInput=归一化事件订阅面(5枚举,host注入桥+_emit测试口,dispose自动清理防泄漏);getAudioContext=lazy单例无工厂返null告警降级
- per-plugin 派生随机流(FNV-1a子种子,序列互不影响同seed复现,向后兼容回退);useContext 守卫(友好错误带插件名)
- 向后兼容增量不bump版本;manifest schema 未动;三 lane 按此冻结面开工

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:28:52 +00:00

151 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Gate0 报告 —— T1b-α lane-core 插件协议地基core-protocol-v0
> ownerT1b-α lane-core续作工位状态Gate0 七件齐备,待主会话审 → 冻结为 `core-protocol-v0`。
> 配对 spec`docs/agent-specs/2026-06-12-T1b-α插件库与参考件-execution.md`v1.1 §2
> 纪律ESM JS + JSDoc + 手写 d.ts零 npm 依赖,本机只跑 `node`,不跑 git不碰 game-runtime/ 外文件。
---
## 1. 七件清单状态(执行版 §2
| # | 件 | 路径 | 状态 | 验证 |
|---|---|---|---|---|
| 1 | Plugin 接口 + 注册器 + 受控面 + d.ts | `src/core/plugin.js` + `src/core/api.d.ts` | 前任交付(收编) | core 17 测全绿 |
| 2 | manifest schema | `schema/plugin-manifest.schema.json` | 前任交付(收编) | 正反向校验通过(见 §4 |
| 3 | `_example` 空插件(五件全) | `src/plugins/_example/{impl.js,api.d.ts,manifest.json,PLUGIN.md,test/example.test.mjs}` | 本工位补齐 | _example 5 测全绿 + manifest 过 schema |
| 4 | PLUGIN.md 规格模板 | `docs/PLUGIN-TEMPLATE.md` | 本工位补齐 | `_example/PLUGIN.md` 即其活样板 |
| 5 | scriptsbuild/size/test + 校验) | `scripts/{build.mjs,size.mjs,test.sh,validate-manifest.mjs}` | 本工位补齐 | test.sh/validate 本机跑绿size `--selftest` 绿build/size 打包段仅集成段 |
| 6 | 浏览器证据 harness 骨架 | `test/harness/browser-evidence.cjs` | 本工位补齐(纯计算口径已实现 + CDP 占位) | 纯计算口径自检通过CDP 真跑留集成段 |
| 7 | 目录与证据约定 README | `README.md` | 本工位补齐 | 含协议版本声明位 + 证据约定表 |
**附加件(超出七件、为守纪律而加)**`scripts/validate-manifest.mjs`(零依赖 manifest 校验器,替代 ajv 守「本机零依赖」纪律)。
> 命名取舍:执行版 §2 第3条示例文件名给的是 `test/`prompt 允许 `.cjs 或 .mjs`。本工位选 **`test/example.test.mjs`**ESM与 core 的 `plugin.test.mjs` 同形、与 `impl.js` 的 ESM 导出最自然对接。harness 按执行版明确要求用 **`.cjs`**。
---
## 2. 前任 4 件收编声明
本工位以前任落的 4 件为**既成权威**,逐件精读后**未改一字**,所有补件严格对齐其 API/风格/约定:
- `src/core/plugin.js`(注册器/生命周期17 测全绿)—— 收编,作为协议唯一实现源。
- `src/core/api.d.ts` —— 收编,作为类型契约一等入口;`_example/api.d.ts``import type { Plugin, PluginContext } from '../../core/api.d.ts'` 复用其类型,不重复定义。
- `src/core/plugin.test.mjs` —— 收编,本工位重跑确认 17/17 绿(见 `test/RESULT.txt`)。
- `schema/plugin-manifest.schema.json` —— 收编,`_example/manifest.json``validate-manifest.mjs` 均以它为契约源。
**无冲突**:本工位补件与 4 件无任何 API/字段冲突。`_example/manifest.json` 字段name/version/gzBudgetBytes/dependencies/exports{impl,types,named}/description与 schema 实际字段逐项对齐 —— 即 prompt 所给「前任死前草稿」与 schema 校验对齐后的落地版(草稿值全部合法,已采纳)。
---
## 3. 三个关键协议取舍(从既有 api.d.ts / plugin.js 反读 + 本工位判断)
### 取舍一受控面宁小勿大YAGNI—— PluginContext 只 4 项
**反读依据**`api.d.ts``PluginContext` 仅声明 `getContext2d / onFrame / time / random` 四项,且注释钉死「插件能从引擎拿到的**全部**能力,仅此 4 项」「绝不暴露 littlejsengine 裸类型——保引擎可换」。
**取舍**:能力面做减法而非加法;插件要更多能力须走 Gate0 后扩展申请,不在地基预扩。
**代价/收益**:代价 = lane 可能短期觉得不够用(如缺直接的输入/音频句柄);收益 = 「引擎可换」(换引擎只换 host 实现,插件零改)+ 受控面是取证可复现的唯一闸口time/random 全可 mock。本工位 `_example` 严格只用这 4 项,作减法纪律的活证。
### 取舍二:错误隔离不连坐 —— 单插件崩不带崩全局
**反读依据**`api.d.ts``InitResult.errors` 是数组(`Array<{name,error}>`)而非「首错即抛」;`initAll` 注释「重复调用幂等」「依赖声明校验」;`plugin.js``initAll` 对单插件 init 抛错 `try/catch``_initErrors``continue`host-dev `tick` 对帧回调抛错亦逐个隔离。
**取舍**init/dispose/帧回调三处都「单点故障隔离,记错误继续」,不回滚已 init 的插件(回滚语义复杂,留 v2 边界)。
**代价/收益**:代价 = 部分 init 失败时系统处于「部分可用」态(须靠 `InitResult.ok=false` + `errors` 显式感知,不能假设全成功);收益 = 取证/运行不被单插件崩溃带垮,符合「可调试、可审计」红线。**lane 须注意**:插件不能假设「同批其它插件都 init 成功了」,跨插件依赖要靠 `dependencies` 声明 + 运行时防御。
### 取舍三:生命周期严格有序 + dispose 幂等,但依赖不做拓扑重排
**反读依据**`api.d.ts` 注释「init 正序/dispose 逆序」「dispose 逆序、幂等」「本波不做拓扑重排(注册顺序即 init 顺序)」;`plugin.js``disposeAll``_disposed` 标记保幂等、逆序遍历 `_initOrder``_missingDeps` 只校验「声明依赖是否已注册」不重排。
**取舍**:注册顺序 = init 顺序正序、dispose 逆序(后进先出,符合资源释放直觉);`dependencies` 仅作「缺失即隔离」的声明式校验,**不**自动拓扑排序。
**代价/收益**:代价 = 有依赖关系的插件**必须由 host 按正确顺序 register**(先注册被依赖者),地基不替你排;收益 = 地基最小Gate0 骨架不背拓扑排序复杂度),幂等 dispose 防重复释放。**lane 须注意**:若你的插件 A 依赖 B务必保证 register(B) 在 register(A) 之前,否则 A 会因依赖缺失被隔离。
---
## 4. schema 校验证据
```
正向node scripts/validate-manifest.mjs
OK src/plugins/_example/manifest.json [example@1.0.0, gzBudget=2048]
共 1 件,通过 1失败 0
反向自检(故意构造非法 manifest校验器须拒绝防空过假阳性
逮住 6 处违规:未声明字段 evil / name 非 kebab-case / version 非 semver /
gzBudgetBytes 超 65536 / exports 缺 types / exports.impl 非 .js -> 退出码 1正确拦截
```
> 校验器为零依赖手写(守「本机零 npm 依赖」纪律),覆盖本 schema 实际用到的全部约束required / additionalProperties:false / type / pattern / min-max / uniqueItems。集成段如需 draft-07 全量校验可另接 ajv属工具链集成段跑
---
## 5. 其余 lane 开工前,主会话该审的 3 个重点
### 重点一受控面够不够YAGNI 边界是否需在 Gate0 松动)
seed 子集 lanelane-math/vfx/sys的真实需求是否能被 **4 项受控面**满足?已知潜在缺口:
- **输入**gamefeel(P4) 的「输入缓冲/coyote」需要输入事件源 —— 当前受控面无 input 项。是经 `onFrame` 内自取输入host 注入输入快照到哪?),还是必须扩 `PluginContext.input`**这是最可能逼迫 Gate0 受控面扩项的点,须先裁。**
- **音频**audio-music(P6) 的发声需要 AudioContext 句柄 —— 当前受控面无。spec 说「P6 node 层只测序列器数据流,发声=集成段」,故 Gate0 可不扩,但须确认「发声句柄走哪条受控面」的方向,免得 lane-sys 卡壳。
**建议**:开 lane 前主会话先就「input 受控面是否纳入 v0」拍板要么扩为 5 项并补 core 测试与 d.ts要么明确 lane 走何种 workaround 并记边界)。
### 重点二context 注入模型(单 context 共享 vs 每插件独享)
当前 `plugin.js``initAll` 把**同一份 `this._context`**host 经 `useContext` 注入)传给所有插件 init。隐含约定**全插件共享一个受控上下文**(共享 time/random/canvas。须审
- 共享 `random` 意味着插件 A 取随机会推进序列、影响插件 B 的随机 —— 对「每插件确定性」是否构成问题?(取证复现时要不要每插件独立子随机源?)
- `useContext` 必须在 `initAll` 前调用,否则 `this._context` 为 undefined -> 插件 init 时 `ctx` 为 undefined 崩。**core 当前未对「忘记 useContext」给出友好报错**(会以 TypeError 形式在插件内崩)。建议主会话评估是否要在 `initAll` 入口加「未注入 context 即显式报错」的守卫(属 core 微调,需补测试,归 lane-core
### 重点三vfx/audio 视觉/发声证据的集成段闭环(禁 node-canvas 的落地)
spec §2/§5 钉死vfx 渲染哈希、audio 发声证据**必须**集成段浏览器出,**禁 node-canvas 兜底**。`browser-evidence.cjs` 已把**纯计算口径**FNV-1a 哈希/非空/直方图/几何)在 Gate0 实现并冻结,但 **CDP 连真 Chrome 的 4 个占位函数**connectCdp/driveFrames/captureImageData/saveScreenshot尚未实现。须审
- 这 4 个占位的实现归属lane-vfx 自己接,还是 lane-core 在集成段统一接?)—— 建议**集成段由主会话统一接**,避免各 lane 重复造 CDP 轮子、口径漂移。
- 「mock 时钟接管 onFrame 驱动」在集成段如何与 LittleJS 主循环对接(当前 host-dev 用 `tick`,集成段需把 tick 桥到引擎更新且时钟可注入)—— 这是 vfx 确定性证据的前提,须在 lane-vfx 开工前定方案。
---
## 6. 本工位自验汇总
| 验证项 | 命令 | 结果 |
|---|---|---|
| core 17 测 | `node --test src/core/plugin.test.mjs` | 17/17 |
| _example 5 测 | `node --test src/plugins/_example/test/example.test.mjs` | 5/5 |
| 全量遍历 | `bash scripts/test.sh` | 22/22原始输出见 `test/RESULT.txt` |
| manifest 正向校验 | `node scripts/validate-manifest.mjs` | 通过 |
| manifest 反向自检 | 非法样例 | 逮 6 违规、退出码 1 |
| size gz 口径 | `node scripts/size.mjs --selftest` | 通过 |
| harness 纯计算口径 | 临时自检(哈希/非空/几何/占位/路径) | 通过 |
**未在本机跑(按纪律留集成段)**esbuild 打包build.mjs/size.mjs 打包段、CDP 浏览器真跑harness 4 占位)—— 均仅 mini-desktop 跑,本机无 esbuild、禁 headless Chrome。
---
## 7. Gate0.1 受控面补丁(主会话 Gate0 终审三裁定落地2026-06-12
> 主会话 Gate0 终审在 §5 三重点基础上拍板三项裁定,本节记「改了什么 + 三裁定如何落的 + 自验」。
> 定性:**向后兼容增量**(既有 22 测零改判全绿),随 `core-protocol-v0` 一并冻结,**不 bump 版本号**。
### 7.1 改动文件清单
| 文件 | 改动 |
|---|---|
| `src/core/plugin.js` | 受控面扩 `getInput`/`getAudioContext`;新增 `HostDevInputBridge`(输入桥,带 `_emit`/`disposeScope``createHostDevContext``audioFactory` 选项 + `inputBridge`/`deriveContextFor` 出口 + 非枚举派生钩子;`PluginRegistry` 增 per-plugin 上下文派生(`_contextFor`)、`disposeAll` 协作回收输入订阅、`useContext`/`initAll` 友好守卫;新增 `deriveSeed`FNV-1a 子种子派生);导出 `HostDevInputBridge`/`deriveSeed`/`INPUT_EVENT_TYPE_LIST` |
| `src/core/api.d.ts` | 新增 `InputEventType`/`InputEvent`/`InputSubscription`/`InputSource` 类型;`PluginContext` 增两方法 + per-plugin random 说明;`HostDevContextOptions``audioFactory``HostDevContextBundle``inputBridge`/`deriveContextFor`;新增 `HostDevInputBridge`/`deriveSeed`/`INPUT_EVENT_TYPE_LIST` 声明 |
| `src/core/plugin.test.mjs` | 新增 §7受控面两项输入订阅/退订/归一化/时间戳/dispose 自动清理/非法类型/handler 隔离/音频降级+lazy 单例、§8per-plugin 派生随机:独立性/复现/deriveSeed 确定性、§9useContext 三守卫)——计 14 新测 |
| `src/plugins/_example/{impl.js,api.d.ts,PLUGIN.md,test/example.test.mjs}` | 样板演示新两面init 订阅 `pointerdown`(句柄留 dispose 注销)+ lazy 取音频(容忍 nullprobe 增 `inputEvents`/`hasAudio`;新增 2 例测(输入驱动/音频降级与注入);既有 5 例测注入 mock `audioFactory` 保 RESULT.txt 干净 |
| `README.md` | 协议版本声明位填「v02026-06-12 冻结,含 Gate0.1 受控面补丁)」+ 受控面 6 项冻结口径;测数 17→31 / 5→7 |
### 7.2 三裁定如何落的
**裁定一·受控面 v0 扩两项(具名消费者 P4/P10/P6**
- `getInput()` → 返回 `InputSource``on/off`5 枚举 `pointerdown/pointermove/pointerup/keydown/keyup`);事件对象 = 归一化 `{type,x,y,key,tMs}`**时间戳取自受控时间源**(取证可复现,禁原生 `event.timeStamp`。host 注入事件桥 `HostDevInputBridge`,测试口 `_emit(type,payload)`。**dispose 自动清理**:注册器为每插件派生上下文时给该插件 `scope``disposeAll` 中**无条件**对每插件调 `inputBridge.disposeScope(name)` 回收其订阅(对「无 dispose 方法但订阅过输入」的插件同样生效,且插件 dispose 抛错也仍回收,防泄漏)。
- `getAudioContext()` → lazyhost 注入工厂,首调即创建、后续返回同一实例(全 host 单例);**无工厂返回 null 并 `console.warn` 仅一次(绝不抛错)**——P6 容错降级。工厂抛错亦降级为 null不连坐插件
**裁定二·per-plugin 派生随机流**
- `context` 改为每插件实例化:共享 canvas/帧/时间源/事件桥/音频,**random 独立**——`deriveSeed(mainSeed, pluginName)`**FNV-1a 32-bit**(与全发行版证据哈希族同口径)派生子种子,注册器 `initAll` 时为每插件 `deriveContextFor(name)`。根治 §5 重点二的「共享 random 互推」隐患。
- 实现接缝host-dev `context` 上挂**非枚举** Symbol 钩子指回 bundle注册器据此把 plain `context` 升级为每插件派生上下文;非枚举 → 不污染受控面、不影响既有 `deepEqual`/形状断言。**兼容回退**:手搓无钩子的 plain context 仍可用(退化为共享上下文,不参与 per-plugin 随机隔离)——既有「直接 `plugin.init(context)`」用法零改。
**裁定三·useContext 守卫**
- 忘调 `useContext``initAll` → 抛含修复提示的友好错误(「未注入受控上下文:请在 initAll() 之前调用 useContext(context)」+ host-dev 用法示例),而非插件内裸 `TypeError`
- `useContext` 传 null/非对象 → 友好抛错;`initAll` 之后再 `useContext` → 抛错并点出当前阶段。
### 7.3 自验汇总Gate0.1
| 验证项 | 命令 | 结果 |
|---|---|---|
| core 31 测17 旧 + 14 新) | `node --test src/core/plugin.test.mjs` | 31/31 |
| _example 7 测5 旧 + 2 新) | `node --test src/plugins/_example/test/example.test.mjs` | 7/7 |
| 全量遍历 | `bash scripts/test.sh` | **38/38**(原始输出见 `test/RESULT.txt` |
| 既有 22 测向后兼容 | 同上(未改一条旧断言) | 全绿 |
| manifest 正/反向校验 | `node scripts/validate-manifest.mjs`manifest 未改) | 通过 / 逮 6 违规 |
**受控面 v0 最终成员冻结口径6 项)**`getContext2d` / `onFrame` / `getInput` / `getAudioContext` / `time` / `random`