- 受控面 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>
15 KiB
Gate0 报告 —— T1b-α lane-core 插件协议地基(core-protocol-v0)
owner:T1b-α 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 | scripts(build/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 子集 lane(lane-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 单例)、§8(per-plugin 派生随机:独立性/复现/deriveSeed 确定性)、§9(useContext 三守卫)——计 14 新测 |
src/plugins/_example/{impl.js,api.d.ts,PLUGIN.md,test/example.test.mjs} |
样板演示新两面:init 订阅 pointerdown(句柄留 dispose 注销)+ lazy 取音频(容忍 null);probe 增 inputEvents/hasAudio;新增 2 例测(输入驱动/音频降级与注入);既有 5 例测注入 mock audioFactory 保 RESULT.txt 干净 |
README.md |
协议版本声明位填「v0(2026-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()→ lazy:host 注入工厂,首调即创建、后续返回同一实例(全 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,注册器据此把 plaincontext升级为每插件派生上下文;非枚举 → 不污染受控面、不影响既有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。