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

15 KiB
Raw Permalink Blame History

Gate0 报告 —— T1b-α lane-core 插件协议地基core-protocol-v0

ownerT1b-α lane-core续作工位状态Gate0 七件齐备,待主会话审 → 冻结为 core-protocol-v0。 配对 specdocs/agent-specs/2026-06-12-T1b-α插件库与参考件-execution.mdv1.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.mjsESM与 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.tsimport 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.jsonvalidate-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.tsPluginContext 仅声明 getContext2d / onFrame / time / random 四项,且注释钉死「插件能从引擎拿到的全部能力,仅此 4 项」「绝不暴露 littlejsengine 裸类型——保引擎可换」。 取舍:能力面做减法而非加法;插件要更多能力须走 Gate0 后扩展申请,不在地基预扩。 代价/收益:代价 = lane 可能短期觉得不够用(如缺直接的输入/音频句柄);收益 = 「引擎可换」(换引擎只换 host 实现,插件零改)+ 受控面是取证可复现的唯一闸口time/random 全可 mock。本工位 _example 严格只用这 4 项,作减法纪律的活证。

取舍二:错误隔离不连坐 —— 单插件崩不带崩全局

反读依据api.d.tsInitResult.errors 是数组(Array<{name,error}>)而非「首错即抛」;initAll 注释「重复调用幂等」「依赖声明校验」;plugin.jsinitAll 对单插件 init 抛错 try/catch_initErrorscontinuehost-dev tick 对帧回调抛错亦逐个隔离。 取舍init/dispose/帧回调三处都「单点故障隔离,记错误继续」,不回滚已 init 的插件(回滚语义复杂,留 v2 边界)。 代价/收益:代价 = 部分 init 失败时系统处于「部分可用」态(须靠 InitResult.ok=false + errors 显式感知,不能假设全成功);收益 = 取证/运行不被单插件崩溃带垮,符合「可调试、可审计」红线。lane 须注意:插件不能假设「同批其它插件都 init 成功了」,跨插件依赖要靠 dependencies 声明 + 运行时防御。

取舍三:生命周期严格有序 + dispose 幂等,但依赖不做拓扑重排

反读依据api.d.ts 注释「init 正序/dispose 逆序」「dispose 逆序、幂等」「本波不做拓扑重排(注册顺序即 init 顺序)」;plugin.jsdisposeAll_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.jsinitAll同一份 this._contexthost 经 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/disposeScopecreateHostDevContextaudioFactory 选项 + inputBridge/deriveContextFor 出口 + 非枚举派生钩子;PluginRegistry 增 per-plugin 上下文派生(_contextFor)、disposeAll 协作回收输入订阅、useContext/initAll 友好守卫;新增 deriveSeedFNV-1a 子种子派生);导出 HostDevInputBridge/deriveSeed/INPUT_EVENT_TYPE_LIST
src/core/api.d.ts 新增 InputEventType/InputEvent/InputSubscription/InputSource 类型;PluginContext 增两方法 + per-plugin random 说明;HostDevContextOptionsaudioFactoryHostDevContextBundleinputBridge/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() → 返回 InputSourceon/off5 枚举 pointerdown/pointermove/pointerup/keydown/keyup);事件对象 = 归一化 {type,x,y,key,tMs}时间戳取自受控时间源(取证可复现,禁原生 event.timeStamp。host 注入事件桥 HostDevInputBridge,测试口 _emit(type,payload)dispose 自动清理:注册器为每插件派生上下文时给该插件 scopedisposeAll无条件对每插件调 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 守卫

  • 忘调 useContextinitAll → 抛含修复提示的友好错误(「未注入受控上下文:请在 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.mjsmanifest 未改) 通过 / 逮 6 违规

受控面 v0 最终成员冻结口径6 项)getContext2d / onFrame / getInput / getAudioContext / time / random