Runner v2 全弧第一相(HJ-RUNNER-ARC-001 / P1):用王蓝莓把"上层游戏宿主装载契约"
从零 spike 到"引擎真掌帧 + 游戏美术与引擎粒子同屏可玩",解锁 P2 泛化 / P3 生成。
契约与共享件
- 新增装载契约 game-runtime/src/core/game-host.d.ts(第9类 additive·暂名):
GameHostBootContext / GameInstance / GameHostFactory;游戏=纯模块工厂,零引擎 import,
能力经下层 ctx.getEngine()(受控面 6 项零改),绘制面 render(g) 的 g=引擎 mainContext
- makeEngineCaps 闭包提升为可 import 模块 host-dev/engine-caps.js(createEngineCaps):
host-dev/ref/未来生成游戏共用;engineMode 短路 / __engineCalls 探针 / null-engine 语义逐行不变
王蓝莓 real 通道(games/wanglanmei-ref)
- 增 ?engine=real:主循环从浏览器 RAF 改挂 engineInit 五回调(掌帧唯一源=引擎);
游戏美术绘制面从 #game 改引擎 mainContext(同屏合并);bundle 延后到 setupReal 内以
mainContext 建(getContext2d 闭包快照修);保 ?engine=stub 逐像素确定性回滚
- game-shop.js 抽为独立 GameHostFactory(零引擎 import,P3 生成 agent 可复制范本)
- entry.js 动态 import('littlejsengine')=node-import-safe ∧ esbuild iife 入包两全
- 驱动 e2e-input.cdp.cjs real 改造:禁 stepFrames 手摇(防双掌帧),真触摸+轮询;
门②同屏像素判据(暖色 warmHits ∧ 引擎粒子 brightHits 同张 #game-engine)
真机验收(mini-desktop real 引擎,5 门)
引擎掌帧 dFrame=31∈[20,45] / 同屏 warmHits=682∧brightHits=13028 同张 #game-engine /
particles.spawnEmitter+synthSfx call-ID 运行时真调引擎 / CDP 517 真触摸跑通整局
(第1天接待18·营收¥79.9·连击9) / console 零错;本机 88 单测全绿(含修复 4 红+补回归)
证据:games/wanglanmei-ref/evidence/p1-real/(e2e-evidence.json pass=true + 5 截图肉眼复核)
诚实 caveat:门② brightHits 在结算屏混入亮 UI,同屏合并由 warmHits(美术落引擎画布)+
call-ID(粒子真调引擎)+ 引擎架构(粒子渲同一 mainContext)三证成立,非阻断。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
45 KiB
P1 · 王蓝莓上引擎(spike 游戏宿主装载契约)· 执行版
状态:execution 版 · 2026-06-13 · 给执行 agent 实现。配套 review 版
2026-06-13-P1-王蓝莓上引擎-review.md;母梁2026-06-13-runner-v2-arc-review.md§5.1。 修订:2026-06-13 对抗复审(deep·全程读真源码)→ 纳入 4 major(real 就绪标记 / driver 范式切换 / 同屏暖色阈值坐实 / bundle 延后建)+ 5 项不明假设处置(IDL 落点改 game-runtime/src·缺省分叉显式声明·game 工厂硬独立·部署改 git-clone-scratch·步骤0 降常规核对)。逐条裁决见 §12。 模型分流:一律 opus(创始人拍)。 前提(创始人已拍):装载契约 = 新立 additive 第 9 类契约(不污染PluginContext);生命周期 =init/update/render/onInput+destroy;boot 注入引擎mainContext给游戏层。 铁律:受控面 6 项契约(getContext2d/onFrame/getInput/getAudioContext/time/random,api.d.ts:269/281/286/303)零改;引擎 import 只活 entry/host(Q4);区分 verified/inferred/assumption,凡引代码事实带 file:line。 装载契约落点(复审改):IDL 落game-runtime/src/core/game-host.d.ts(与api.d.ts同目录),不落contracts/——contracts/是跨三仓五工位的业务契约 SSOT(API/DB/SDK/event…,README §1),装载契约是 game-runtime 内部「host↔游戏工厂」接线面,消费方只在 game-runtime(host boot + 生成 agent),且让跨仓 SSOT 反向相对 import../game-runtime/src/*违「契约即 SSOT」且 contracts 拆独立仓时即断(README §1 明列拆仓计划)。落同目录后 import 方向自然(import type { PluginContext } from './api.d.ts')。详见 §12-A1。
1. 目标 / 范围边界
1.1 目标
给 wanglanmei-ref 加 ?engine=real 通道:① 引擎真掌帧(engineInit 五回调驱动游戏 update/render,废浏览器 RAF);② 游戏美术与引擎粒子同屏可玩(游戏绘制面从 #game 2D 改引擎 mainContext,合并成一张画布);③ 人在 mini-desktop 浏览器真玩;④ 保 ?engine=stub 回滚(逐像素确定性)。同时把 makeEngineCaps(host.js:197-318)提升为可 import 模块 host-dev/engine-caps.js,host-dev / ref 共用。
1.2 范围内
- 新立装载契约工件
game-runtime/src/core/game-host.d.ts(additive 第 9 类契约;落 game-runtime/src,非 contracts/,详见 §12-A1)。 host-dev/engine-caps.js提升(搬host.js:197-318闭包为createEngineCaps({LJS, engineMath, engineMode, recHook}));host.js改 import。wanglanmei-refreal 通道改造:entry.jsimport 引擎、main.js主循环挂引擎五回调、绘制面改mainContext、createHostDevContext注engineFactory、mockNowMs改逻辑帧钟、RAF 门控。- ref 内拆出符合装载契约的 game 工厂(硬约束:独立文件
game-shop.js,默认导出GameHostFactory,零引擎 import;禁 main.js 内联糊过,详见步骤 3 + §12-A3)。 e2e-input.cdp.cjs加engine=real通道断言 + 同屏像素判据 + real 通道 driver 范式切换(真触摸+轮询,禁 stepFrames 手摇,详见步骤 8 + §12-M2)。index.html加__refBootedReal就绪标记接线(real 通道;详见步骤 4.3 + §12-M1)。SIZES.md增"游戏产物"口径。
1.3 范围外(非目标)
- 受控面 6 项契约(零改)。
- P2(泛化宿主/装载任意 GamePackage)→ 母 spec Phase D(O2 已拍)。
- P3(生成游戏按契约产出)→ W-G1 自身 spec。
- 后端发布主链 / game-studio / feed / 生成管线。
- 不接 OSS;不改 feed 引擎选型。
- 不动 stub 通道既有行为(逐行保旧)。
2. 前置
| # | 前置 | 状态 / 证据 |
|---|---|---|
| P-1 | A0 引擎掌帧地基已收口(real 通道范式可用) | verified:host.js:917 engineInit 五回调 + host.js:919 setupAfterEngine(mainContext);门0 8/8(3e5cab4) |
| P-2 | A1-A4 引擎能力包装已落地(makeEngineCaps 已接线) | verified:host.js:359 engineFactory: makeEngineCaps;engine-math.js 已独立模块 |
| P-3 | mini-desktop 具备 esbuild + chrome(6c6g 禁 chrome) | verified:a6-runbook.sh / ref-e2e-runbook.sh 均跑 mini-desktop |
| P-4 | ref 现状:49 单测绿 + e2e 四件套(stub 基线) | verified:BUILD.md RESULT.txt 49/49 |
| P-5 | LittleJS 已是 ref devDep(构建可引擎入包) | inferred:host-dev 同仓共用 littlejsengine;执行首步须核 games/wanglanmei-ref 经 monorepo node_modules 可解析(与 host-dev entry 同口径) |
3. 涉及文件路径
| 文件 | 动作 | 关键行锚 |
|---|---|---|
game-runtime/src/core/game-host.d.ts |
新增 | 装载契约 IDL(§5);落 src/core 与 api.d.ts 同目录(非 contracts/,§12-A1) |
contracts/README.md |
改(索引指针) | 不折进跨仓 8 类;加一行指针:「游戏宿主装载契约(game-runtime 内部,落 game-runtime/src/core/game-host.d.ts)」(§12-A1) |
host-dev/engine-caps.js |
新增 | createEngineCaps(搬 host.js:197-318) |
host-dev/host.js |
改 | makeEngineCaps 闭包 → import createEngineCaps(host.js:197-318 删,host.js:204/359 改) |
games/wanglanmei-ref/entry.js |
改 | entry.js:15 增 import * as LJS from 'littlejsengine' |
games/wanglanmei-ref/src/main.js |
改(real 通道) | main.js:112(engineFactory,real 须延后到 engineInit().then 内建·§12-M4)/617-661(frame→game.update/render)/648(drawFrame g→mainContext)/668-691(RAF 门控)/618(mockNowMs)/583-602(real 输入改挂 mainCanvas·§12-M4) |
games/wanglanmei-ref/src/game-shop.js |
新增(硬约束·非建议) | 把 main.js 游戏逻辑/绘制段封成装载契约 game 工厂;默认导出 GameHostFactory、零引擎 import(步骤 3 + §12-A3) |
games/wanglanmei-ref/index.html |
改 | real 通道 __refBootedReal 就绪标记接线(§12-M1);stub 仍用 __refBooted |
games/wanglanmei-ref/test/e2e-input.cdp.cjs |
改 | 加 engine=real 通道(rect 选择器切 #game-engine、waitRefBoot 等 __refBootedReal、real 范式=真触摸+轮询禁 stepFrames、同屏像素判据)(步骤 8 + §12-M1/M2/M3) |
games/wanglanmei-ref/test/ref-e2e-runbook.sh |
改(或旁建 real 版) | 沿 git-clone-scratch 口径(ref-e2e-runbook.sh:20-21,非 tar-pipe);URL 加 &engine=real;构建后 grep -q ParticleEmitter 核引擎入包(§12-A4) |
SIZES.md |
改 | 增"游戏产物=引擎+包装层"口径 |
4. 数据流
sequenceDiagram
participant HTML as index.html
participant Boot as bootRef(main.js)
participant Caps as engine-caps.js
participant LJS as littlejsengine
participant Game as game(装载契约工厂)
HTML->>Boot: bootRef({canvas,statusEl})
alt engine=real
Boot->>Boot: setGLEnable(false)+setDebugWatermark(false)+setCanvasClearColor(BLACK)
Boot->>Caps: createEngineCaps({LJS,engineMath,engineMode:'real',recHook})
Caps-->>Boot: engineFactory(particles/audio/math 三面)
Boot->>LJS: engineInit(gameInit,gameUpdate,...,gameRender,...)
LJS-->>Boot: .then() → mainContext 就绪
Boot->>Boot: createHostDevContext({context2d:mainContext, engineFactory, clock:逻辑帧钟})
Boot->>Game: game.init(ctx, mainContext)
loop 引擎内部 RAF
LJS->>Game: gameUpdate → game.update(FIXED_DT)
LJS->>Game: gameRender → game.render(mainContext)
Game->>LJS: ctx.getEngine().particles.spawnEmitter(粒子自渲落 mainContext)
end
else engine=stub(保旧)
Boot->>Boot: prepareCanvas(#game) 2D + createHostDevContext(无 engineFactory)
Boot->>Boot: 浏览器 RAF + mockNowMs 墙钟 → frame(dtReal) → drawFrame(#game)
end
关键数据流不变量:
- real:掌帧唯一源=引擎(游戏不自起 RAF);绘制面唯一=引擎
mainContext(游戏美术+引擎粒子同屏);插件能力经ctx.getEngine()(下层已冻)。 - real 时序硬约束(复审 M1/M4):
createHostDevContext(bundle)、game.init、输入桥挂mainCanvas、就绪标记__refBootedReal=true全部在engineInit().then(setupReal)内(上图.then() → mainContext 就绪之后)——引擎掌帧的必然结构:引擎先起、mainContext/mainCanvas才有;顶层提前建 bundle 会快照错 ctx(host.js:323-324)。 - stub:与今天
main.js逐行等价(回滚锚);就绪标记仍用index.html的__refBooted(index.html:81)。
5. 装载契约 IDL(新立 additive 第 9 类契约 · game-runtime/src/core/game-host.d.ts)
设计原则:游戏 = 纯模块工厂(零引擎 import,Q4)。host 经
engineInit五回调驱动游戏生命周期;游戏绘制面render(g)的g= 引擎mainContext(boot 注入);插件能力经下层已冻结ctx.getEngine()(不在本契约重复定义)。与api.d.ts是 additive 关系:本契约只新增GameHostFactory/GameInstance/GameHostBootContext三个类型,受控面 6 项零改。 落点(复审改):落game-runtime/src/core/game-host.d.ts(与api.d.ts同目录),故 import 方向自然=同目录 sibling./api.d.ts;不落 contracts/(理由见 §12-A1)。
// game-runtime/src/core/game-host.d.ts —— 游戏宿主装载契约(第 9 类契约 · additive · game-runtime 内部)
// owner:Runner v2 P1 | 消费方:集成段 host(boot 游戏)+ 生成 agent(产出靶)
//
// 【为什么新立、不折进 api.d.ts】
// api.d.ts 的 PluginContext 是「插件↔引擎」面(getEngine() 等,api.d.ts:303,已冻结);
// 本契约是「游戏整体↔引擎宿主」面——游戏的 update/render 主体不是插件、不走 PluginContext。
// 两者正交(边界模型「同一职不留两条并行路」):插件经 getEngine() 拿能力,游戏经本契约挂宿主。
// 引擎 import 只活集成段 host(Q4 铁律),游戏工厂零引擎 import。
// 【为什么落 game-runtime/src 而非 contracts/】
// contracts/ 是跨三仓五工位的业务契约 SSOT(API/DB/SDK/event…,contracts/README.md §1);
// 本契约是 game-runtime 内部「host↔游戏工厂」接线面,消费方只在 game-runtime,
// 故与 api.d.ts 同目录、import 方向顺;落 contracts 会让跨仓 SSOT 反向相对 import ../game-runtime/src(拆仓即断)。
import type { PluginContext } from './api.d.ts';
/**
* 游戏挂宿主时拿到的 boot 上下文。host 在游戏 init 时一次性注入。
* - ctx:下层已冻结受控面(PluginContext)。游戏经 ctx.getEngine() 拿插件能力(粒子/音频/数学),
* 经 ctx.getInput() 拿受控输入,经 ctx.random/time 拿受控随机/时钟。**不得绕过 ctx 触碰裸引擎/DOM。**
* - mainContext:引擎 mainContext(setGLEnable(false) 后的纯 2D 绘制面)。游戏 render(g) 的 g 即它;
* 游戏美术画在此面 → 与引擎粒子(引擎自渲落同一 mainContext)同屏。**此字段是装载契约的核心**
* (游戏取引擎绘制面的唯一约定,不借 PluginContext.getContext2d——那是插件面)。
* - seed:主随机种子(确定性);canvas:可见画布引用(输入 rect 映射用,real=引擎 mainCanvas)。
*/
export interface GameHostBootContext {
ctx: PluginContext; // 下层已冻结受控面(复用)
mainContext: CanvasRenderingContext2D; // 引擎 mainContext(boot 注入,render 绘制面)
canvas: HTMLCanvasElement; // 可见画布(real=引擎 mainCanvas,输入 rect 映射用)
seed: number; // 主随机种子
}
/**
* 游戏实例:host 经引擎五回调驱动的稳定生命周期。
* 生命周期对齐 engineInit 五回调(host.js:883-887 engGameInit/Update/Render):
* · init ← gameInit(引擎首帧前一次性):建游戏对象、订阅 ctx.getInput()、装资产。
* · update(dt) ← gameUpdate(引擎每逻辑步,dt=FIXED_DT 恒定):core.update/事件/特效计时。
* · render(g) ← gameRender(引擎清屏后,g=mainContext):画游戏美术;引擎粒子已自渲落同一 g。
* · onInput(ev)(可选):归一化输入事件回调。**注**:ref 经 ctx.getInput().on 订阅即可,
* onInput 是「不便用 ctx.getInput 的极简游戏」的备选入口;二者择一,ref 用前者。
* · destroy():卸载卫生(P2 装载多游戏复位用;P1 ref 单游戏可空实现但须声明)。
*/
export interface GameInstance {
init(boot: GameHostBootContext): void | Promise<void>; // 异步:装资产(ref loadAssets 异步)
update(dt: number): void; // 逻辑步(dt 秒,引擎恒定步长)
render(g: CanvasRenderingContext2D): void; // g=mainContext(boot 注入的同一面)
onInput?(ev: { type: string; x?: number; y?: number; key?: string }): void; // 可选输入入口
destroy(): void; // 卸载(幂等)
}
/**
* 游戏工厂:纯模块导出(零引擎 import)。host 调它建 GameInstance。
* 生成 agent 的唯一产出靶 = 实现并默认导出一个 GameHostFactory。
* 【硬约束·§12-A3】必须是「独立、可被 P3 生成 agent 复制的工厂模块」:
* · 独立文件默认导出(ref=games/wanglanmei-ref/src/game-shop.js 的 `export default createShopGame`);
* · 零引擎 import(引擎能力一律经 boot.ctx.getEngine());
* · 禁在 main.js 内用内联闭包「形似实现」糊过——否则 P3 缺真实参照实现,违 P1「spike 装载契约到可被 P3 复制」目标。
* @param opts 工厂选项(无玩法语义;预留 difficulty/variant 等 additive 扩展位)。
*/
export type GameHostFactory = (opts?: Record<string, unknown>) => GameInstance;
IDL 三条不可破约定(对齐边界模型):
- 掌帧唯一源=引擎:
GameInstance不含任何 RAF/计时器;update/render只被引擎五回调驱动。推论(§12-M2):取证 driver 在 real 通道亦不得手摇stepFrames(否则与引擎 RAF 双掌帧,破此条)——真触摸后等引擎自然推帧 + 轮询状态。 - 绘制面唯一=引擎
mainContext:render(g)的 g 必为 boot 注入的mainContext;游戏美术与引擎粒子同屏。推论(§12-M4):real 通道bundle必须在engineInit().then内、以context2d=mainContext建(受控面 getContext2d 一次性快照闭包捕获值,host.js:323-324);不得在顶层先建再改 ctx。 - 能力唯一面=下层
ctx.getEngine():粒子/音频/easing 经它(不在本契约重复定义引擎能力面)。
additive 关系明确:game-host.d.ts 仅 import type { PluginContext } from './api.d.ts',不修改 api.d.ts 任何导出;受控面 6 项零改。
6. 关键实现步骤(有序)
步骤 0:核前置(执行首步,read-only · 常规核对)
- mini-desktop scratch clone 后核
games/wanglanmei-ref/entry.js加import * as LJS from 'littlejsengine'能被 esbuild 解析(与 host-dev entry 同 monorepo node_modules)。口径(复审降级·§12-A5):此项已被 host-dev 既有事实强力支撑(host-dev/entry.js末行已import * as LJS from 'littlejsengine'且 host-dev real 门 6/6 已过,同 monorepo node_modules)→ 不是可能阻断的高不确定项,作常规构建核对:构建 ref-bundle 后grep -q ParticleEmitter dist/ref-bundle.js即可(无需专门 /tmp dry-build 阻断门)。
步骤 1:新立装载契约工件 game-runtime/src/core/game-host.d.ts(§12-A1)
- 按 §5 IDL 落盘到
game-runtime/src/core/game-host.d.ts(与api.d.ts同目录);import 方向=同目录 sibling./api.d.ts。 contracts/README.md不把它折进跨仓 8 类清单,仅加一行指针登记:「游戏宿主装载契约(game-runtime 内部,落game-runtime/src/core/game-host.d.ts),消费方=host boot + 生成 agent」(理由 §12-A1)。- 验证:
tsc --noEmit(若仓有 ts 校验)或 node 类型注释自洽;确认只import type { PluginContext } from './api.d.ts'、不改api.d.ts任何导出(diff 证 api.d.ts 零改)。
步骤 2:提升 engine-caps.js(搬运已有件,语义零变)
2.1 新建 host-dev/engine-caps.js,导出 createEngineCaps({ LJS, engineMath, engineMode, recHook }):
- 把
host.js:197-318makeEngineCaps闭包体整体搬入,逐行保语义:- 首行
if (engineMode !== 'real') return null;(保 null-engine 回归短路,host.js:198)。 rec改为参数注入:const rec = recHook || (() => {});(默认 no-op;原闭包内window.__engineCalls.push语义由调用方经 recHook 提供)。mathRaw = engineMath(原buildEngineMath(LJS)的结果由调用方传入,或createEngineCaps内部buildEngineMath(LJS)——二选一,保host.js:204同口径)。- particles/audio.synth 三面包装体(
host.js:214-316)逐字搬入,引擎引用经入参LJS.*。 2.2host.js改:删内联makeEngineCaps(197-318),改import { createEngineCaps } from './engine-caps.js';在buildBundle(host.js:353)处engineFactory: () => createEngineCaps({ LJS, engineMath: buildEngineMath(LJS), engineMode, recHook: id => { try { (window.__engineCalls||(window.__engineCalls=[])).push(id); } catch(_){} } })。
- 首行
- 关键:host-dev 仍注入 rec(门② 探针在 host-dev 取证产物里有,符合现状——host-dev 不是渠道产物)。
2.3 验证(回归基线):mini-desktop 跑
a6-runbook.sh(门0 间接 + 门②)+ gate0 driver → 门0 8/8 + real 门 6/6 逐项不变(提升不得改任何判据数值)。
步骤 3:ref 拆出装载契约 game 工厂(game-shop.js · 硬约束独立文件 · §12-A3)
- 硬约束:必须是独立文件
games/wanglanmei-ref/src/game-shop.js,export default createShopGame(GameHostFactory形态),零引擎 import(引擎能力一律经boot.ctx.getEngine())。禁在 main.js 内联闭包形似实现糊过——P3 生成 agent 需要一个真实、可复制的参照工厂(§12-A3)。stub 通道亦经此工厂的 stub 适配驱动(绘制面 g=#gamectx),保 stub 行为不变由逐像素门⑤兜。 - 把
main.js的游戏逻辑段与绘制段封成createShopGame(): GameInstance(默认导出GameHostFactory):init(boot):搬main.js:158-178游戏对象创建(createShopCore/createLinePicker/createHostFx等)+main.js:165-167异步loadAssets+main.js:582-611输入订阅(经boot.ctx.getInput())。保存boot.mainContext为绘制面。update(dt):搬main.js:617-647逻辑段(mockNowMs += dt*1000;core.update(dtWorld);bundle.tick(dt);applyEvents;maybeReplayBufferedServe;fx.step;rush 联动;闲时台词;ghosts 清理;rebuildZones)。注:bundle.tick仍由游戏 update 调(驱动插件 onFrame 计时;引擎粒子自渲与此解耦,同 host.js A2 注释host.js:726-728)。render(g):搬main.js:648-658绘制段(drawFrame(g, {...})+palettePost.renderPost(g))。g=boot.mainContext。destroy():cancel 输入订阅 + dispose registry(P1 可最小实现,须声明)。
- 难点(real 坐标系):引擎
mainContext无 DPR 变换(引擎每帧updateCanvas重置为单位阵)。游戏按逻辑 390×844 绘制 →render(g)须在g.save()内g.setTransform(DPR,0,0,DPR,0,0)+g.imageSmoothingEnabled=false,对齐host.js:489-490renderParticles 的补偿做法。stub 通道 g=prepareCanvas(#game)已自带 DPR 变换(main.js:69)→ 不补。
步骤 4:main.js bootRef real 通道接线
4.1 读 engineMode = getParam('engine', 'stub')(缺省 stub 保旧;real 显式开。注:host-dev 缺省 real,但 ref 缺省 stub 更稳——真玩取证显式带 &engine=real)。
4.2 stub 分支:逐行保旧(prepareCanvas(#game) + createHostDevContext 无 engineFactory + 浏览器 RAF + frame(dtReal) + drawFrame(#game))。把现有 main.js 主体收进 stub 分支(或经 game 工厂的 stub 适配,但 stub 务必行为不变——逐像素门⑤兜)。
4.3 real 分支(仿 host.js:897-928;控制流=实质重构,不是局部插桩——§12-M4):
LJS.setGLEnable(false)/setShowSplashScreen(false)/setDebugWatermark(false)/setCanvasClearColor(LJS.BLACK)/setCanvasFixedSize(vec2(390*DPR, 844*DPR))(全在 engineInit 前)。- 建独立
engineRootdiv 挂 body(不复用#wrap,引擎会覆盖容器样式,host.js:911-915)。 engineMath = buildEngineMath(LJS);engineFactory = () => createEngineCaps({ LJS, engineMath, engineMode:'real', recHook })(取证时注 recHook,渠道不注 —— 见步骤 7)。engineInit(engGameInit, engGameUpdate, engGameUpdatePost, engGameRender, engGameRenderPost, [], engineRoot).then(() => setupReal(LJS.mainContext)).catch(e => { window.__refBootError=String(e&&e.message||e); window.__refBootedReal=false; ... })。setupReal(mainContext)—— 一切 bundle/game/输入/就绪标记都在此(engineInit().then 内):- 【M4 硬约束·bundle 必须延后到此建】
bundle = createHostDevContext({ context2d: mainContext, seed, audioFactory, clock: () => mockNowMs, engineFactory })。context2d 必为 mainContext——受控面 getContext2d 一次性快照闭包捕获的 ctx(host.js:323-324注释明确「读闭包捕获值,故必须就绪后建 bundle」);绝不可在顶层先用#gamectx 建 bundle 再改,否则 getContext2d 快照到 stub#gamectx,受控面 context2d 与游戏 render 面分裂(= spec 要消除的渲染面分裂,会换形式复现)。可直接引host.js:353-371(buildBundle)+host.js:379-381(setupAfterEngine 在 then 内建 bundle)作样板。 - 建 game 工厂 →
game.init({ ctx: bundle.context, mainContext, canvas: LJS.mainCanvas, seed })(异步,init内 await loadAssets)。 - 挂
window.__refHost、window.__refForensics、window.__refChecklist(全部移进 then 内挂,real 下顶层不可早挂——顶层时 bundle/game 尚不存在)。 LJS.mainCanvas.id='game-engine'(CDP 取证选择器,仿host.js:830);暴露window.__engine双时钟(仿host.js:840)。- 【M1 硬约束·real 专属就绪标记】 game.init 完成后置
window.__refBootedReal = true(仿 host-dev__hostBootedReal,host.js:920)。这是 real 通道唯一就绪信号——必须在 engineInit().then 内、game.init/资产/受控面全就绪后置。stub 通道照旧用index.html的__refBooted(bootRef().then 置,index.html:81),real 通道额外置__refBootedReal。driver 在 real 下改等__refBootedReal(步骤 8 + §12-M1)。
- 【M4 硬约束·bundle 必须延后到此建】
- 五回调:
engGameUpdate(){ game.update(FIXED_DT); };engGameRender(){ game.render(mainContext); };其余空/留痕(engGameInit(){ window.__refEngineInitFired=true; })。五回调函数定义在 bootRef 作用域(engineInit 须先拿到),实体经 holder 转发到 setupReal 内真实现(仿host.js:880-887的_engStep/_renderParticlesholder 范式)。 - mockNowMs:real 下由
game.update内mockNowMs += dt*1000累加(逻辑帧钟,非墙钟,同host.js:526)。 - RAF 门控:
startRaf/rafLoop(main.js:668-691)仅 stub 调;real 分支不调(引擎内部 RAF 驱动五回调)。 4.4 输入(real·控制流重构 §12-M4):real 下输入监听改挂LJS.mainCanvas(不再硬绑传入的opts.canvas=#game;坐标按引擎 canvas rect,仿host.js:536-538inputTarget = real ? LJS.mainCanvas : canvas);toCanvasXY的getBoundingClientRect改读LJS.mainCanvas。监听挂载 +inputBridge取用同样移进setupReal(bundle 就绪后才有bundle.inputBridge)。经bundle.inputBridge._emit灌受控面;游戏经boot.ctx.getInput().on订阅(同main.js:604-611,订阅在 game 工厂init内)。stub 通道输入逐行不变(挂#game,main.js:583-602)。
步骤 5:entry.js 引擎入包
entry.js:15后增import * as LJS from 'littlejsengine';+export const Engine = LJS;(仿host-dev/entry.js:21/28)。Q4:引擎 import 只活 entry/host,game 工厂零引擎 import(构建后核game-shop.js编译产物经 LJS namespace 间接,不直 import)。
步骤 6:SIZES.md 口径
- 增"游戏产物(引擎+包装层)"表:ref stub=197KB(不打引擎)/ ref real≈540KB raw min(引擎真入);标 gz 实测(
size.mjs);注"插件增量法不覆盖游戏产物口径"。
步骤 7:探针绝不入渠道产物(物理保证)
createEngineCaps的recHook默认 no-op;engineFactory默认不传 recHook。- 王蓝莓真玩取证:取证脚本经 URL/全局开关让 main.js real 分支注入
recHook = id => window.__engineCalls.push(id)(仅取证路径)。 - 渠道产物(未来 P2/P3 出仓 bundle):esbuild 入口不引取证注入点 → recHook 恒 no-op → 探针代码无副作用、不挂
window.__engineCalls。P1 阶段 ref-bundle 即取证用,注 recHook 可接受(ref-bundle 非渠道产物,同 host-bundle 现状host.js:202)。
步骤 8:e2e driver real 通道改造清单(范式切换·非增量 · §12-M1/M2/M3)
背景(复审 major 2/1):现行
e2e-input.cdp.cjsdriver 主体建在window.__refHost.stepFrames(n)手摇逻辑帧 上(e2e-input.cdp.cjs:109,贯穿:144tapAt 内stepFrames(3)、:236/:277营业循环stepFrames(20)),坐标 rect 写死document.getElementById('game')(:127),就绪只等__refBooted/__refHost(:76)。这三处在 real 通道整体失效——必须按下列清单改造,不是「加个 engine=real 通道断言」的增量。
real 通道 driver 改造(保 stub 通道路径逐行不变,real 走新分支):
(a) 就绪标记(M1):waitRefBoot 在 engine=real 时改等 window.__refBootedReal(而非 __refBooted)。real 下 __refBooted(index.html:81 bootRef().then 置)会在引擎/game 就绪前被置 true(bootRef 顶层同步段先 resolve),driver 据它驱动会打空/读 undefined → 假失败或间歇绿。host-dev 正是为此引入 __hostBootedReal(host.js:920,注释「CDP 取证须等 __hostBootedReal 再驱动」host.js:851/931)。新分支:evaluate('({ b: !!window.__refBootedReal, e: window.__refBootError||null, h: !!window.__refHost })')。
(b) 坐标 rect 选择器(M3):initMapping 的 document.getElementById('game')(:127)在 engine=real 时切到 document.getElementById('game-engine')(引擎自建可见画布,host.js:830;real 下 #game 不是可见画布)。否则取错画布/null → 坐标全错。stub 通道仍用 #game。
(c) 确定性序列范式切换(M2·硬·边界铁律):real 通道禁用 stepFrames 手摇——引擎内部 RAF 已掌帧(装载契约第 1 条「掌帧唯一源=引擎」),game 不暴露/不手摇 stepFrames,否则与引擎 RAF 双掌帧违铁律。原「tapAt 后 stepFrames(3)」「循环 stepFrames(20)」改为:真触摸 → delay(ms) 等引擎自然推帧 → 轮询 __refForensics.state()/measures() 判相位推进(轮询到目标态或超时)。这与 stub 的「手摇定量推帧」是范式切换(harness §5-坑6 已订:逐像素门交 stub 通道独占手摇,引擎通道只证掌帧+渲染落 2D+真玩闭环,不比逐像素哈希)。real 真玩去掉 mode=evidence(real 取证不靠 stepFrames,靠真触摸+轮询)。
(d) 同屏像素扫描:合 gate0 G0-7 + a6 A6-5 像素扫描,CANVAS=#game-engine,判据见 §8 门②(暖色阈值已开工前坐实,见门②)。
7. 边界失败路径
| 失败 | 触发 | 处置 |
|---|---|---|
| 引擎 engineInit 失败 | WebGL/资源异常 | 镜像 window.__refBootError(仿 host.js:925),status 提示"可加 ?engine=stub 回滚";门④/CDP 据此判 real 失败→回滚 stub |
| driver 在引擎就绪前驱动(打空/读 undefined) | real 下 __refBooted 早于引擎/game 就绪被置(§12-M1) |
__refBootedReal 只在 engineInit().then 内 game.init 完成后置;driver real 分支等它(步骤 8a) |
| 资产装载缺失 | 图片/mp3 缺 | 游戏 init 走程序化回退(保 main.js:165 容错),照常启动;取证 waitAssets 超时如实记录续跑(e2e-input.cdp.cjs:87) |
| 粒子渲到分离画布(假阳性) | 绘制面未真改 mainContext | 门② 同屏像素判据逮(游戏画布粒子命中=0=fail),见 §8 门② |
| AudioContext headless suspended | 无手势 | --autoplay-policy=no-user-gesture-required(harness §5);门② 音频降软门(同 a6-engine-wiring.cjs:89) |
| engine-caps 提升回归 | 探针/短路语义漂移 | 步骤 2.3 门0 8/8 + real 门 6/6 回归卡住;不过即 revert engine-caps.js |
| 受控面被改 | 误扩 6 项 | code review + 契约 diff 卡死(硬约束) |
8. 验证方法(harness 门)
全在 mini-desktop 跑(6c6g 禁 chrome)。部署口径=沿现行
ref-e2e-runbook.sh的 git-clone-scratch(复审改·§12-A4):git clone --depth 50 --branch dev/2.0.0 ssh://git@101.200.34.71:2222/...→git checkout <精确SHA>到 scratch(ref-e2e-runbook.sh:20-21)→ esbuild 锁参构建 → IP gate → systemd-run serve(static-serve.cjs根=games/wanglanmei-ref端口 4320,:44)+chrome 两单元(:47)→ 就绪 10×0.5s + serve 5×6s 在线 + CDP 10×2s 两段探活(:55/:59/:64)→ driver(:69)→ journal 归档 → 停单元。chrome 旗标含--autoplay-policy=no-user-gesture-required(ref-e2e-runbook.sh:48,harness §5)。real 版只需在 runbook 的 driver URL 加&engine=real+ 构建后grep -q ParticleEmitter dist/ref-bundle.js核引擎入包。严禁裸./rsync / tar-pipe 整树(6c6g cwd 易重置到仓根吞整树,harness §6-律4);git-clone-scratch 是 ref runbook 既有口径,勿改成 host-dev/a6 的 tar-pipe staging 配方。
构建命令(mini-desktop,cwd=game-runtime):
node scripts/build.mjs games/wanglanmei-ref/entry.js games/wanglanmei-ref/dist/ref-bundle.js --global-name=WanglanmeiRef
# 此时引擎真入包 ~540KB;核对:grep -q ParticleEmitter dist/ref-bundle.js
真玩 URL:http://localhost:4320/?engine=real&seed=305441741(取证模式 &mode=evidence 用 __refHost 确定性驱动;真人交互去掉 mode)。
| 门 | 判据(可执行阈值) | driver / 口径 |
|---|---|---|
| ① 掌帧 | window.__engine.snapshot() 两采样间隔 500ms,frame 增量 ∈ [20,45](@60fps ~30,排单步假推进+失控空转) |
复用 gate0 G0-2(gate0-engine-takeover.cjs:75);ref driver 加同款断言(ref 暴露 window.__engine) |
| ② 同屏像素(堵假阳性) | 截 #game-engine(引擎接管后 DOM 可见、=mainContext 的 canvas,host.js:830)。两类命中均 >0:(a) 游戏美术·暖色(开工前已坐实,非占位):店面暖色主色像素命中数 warmHits > N(N=200,按 780×1688 抽样网格调,详见下「门②(a) 暖色判据」);(b) 引擎粒子·亮色:burst 白 brightHits>0 ∧ maxCh>80(a6 A6-5 口径,a6-engine-wiring.cjs:111)。为何这证合并非两画布:若粒子渲到另一张分离 canvas,则截 #game-engine 时粒子命中=0(粒子不在这张)→ (b) fail;若游戏渲到 #game 旧画布、引擎渲 mainContext,则截 #game-engine 时游戏命中=0→ (a) fail。两类同时命中 ⟺ 两者真在同一张 #game-engine 上 |
ref driver 新增(合 gate0 G0-7 + a6 A6-5 像素扫描,CANVAS=#game-engine) |
| ③ 门②真接线 | 取证注 recHook 后,window.__engineCalls 含 particles.spawnEmitter call-ID(经 particles-juice 真实 spawnEmitter,非直调引擎) |
复用 a6 A6-2(a6-engine-wiring.cjs:79);ref driver 触发游戏内真粒子(serve_ok 飞行/burst)后读 __engineCalls |
| ④ CDP 真玩 + 四件套 | CDP 真触摸逐 zone 真玩闭环(开店→进货→营业→结算→升级),首日接待数>0、serve_ok>0、到达结算、升级 upgrades.speed1=true;四件套 evidence(输入轨迹 JSONL/截图+canvas 哈希/probe JSONL/checklist JSON)落 evidence/round-N/。zone 名以 main.js computeZones 为唯一真相(serve-/inc-/up-,render.js:967/975/981),驱动器猜语义名必踩坑(harness §2-律4) |
扩 e2e-input.cdp.cjs 加 real 通道(步骤 8:&engine=real、选择器 #game-engine、等 __refBootedReal、真触摸+轮询,harness §2-律2) |
| ⑤ stub 回归 | ?engine=stub&mode=evidence 菜单帧双开哈希一致(清档 reload 后比,harness §5-坑6);ref 49 单测仍绿 |
现有 e2e-input.cdp.cjs 门 B(stub 通道不变) |
| ⑥ 提升回归 | engine-caps 提升后:host-dev 门0 8/8 + real 门(a6)6/6 逐项不变 | 步骤 2.3 |
| ⑦ console 干净 | uncaught=[](real 通道,含未捕获 error/unhandledrejection) |
ref driver(main.js:90-96 uncaught 归集) |
门④ driver 范式(复审 M2):real 通道 driver=真触摸+轮询(禁 stepFrames 手摇,否则双掌帧违铁律),坐标走
#game-enginerect,就绪等__refBootedReal——见步骤 8。zone 名以main.js computeZones为唯一真相(serve-/inc-/up-,render.js:967/975/981),驱动器猜语义名必踩坑(harness §2-律4)。
门②(a) 暖色判据(开工前坐实 · 复审 major 3)
背景:gate0 G0-7 的
coloredHits判据是 青蓝(b>120 && b>r+30,gate0-engine-takeover.cjs:111),a6 A6-5 是 亮度(max(r,g,b)>80,:111,注释明告青蓝口径对引擎白粒子失效)——二者均不适配王蓝莓暖色店面(R 高 B 低,与青蓝正相反)。harness §6-2 已订「换引擎能力后像素门颜色假设必同步重核」。故门②(a) 判据开工前从palette.js坐实,不留执行时占位。
已坐实色域(源:games/wanglanmei-ref/src/palette.js:23-41 BASE 暖调家族,已读真源核对):
| 用途 | 色名 | RGB | R−B | 命中 |
|---|---|---|---|---|
| 柜台/货架木带 | wood |
(169,118,79) | 90 | ✓ |
| 深木描边 | woodDark |
(124,84,55) | 69 | (R<140 不计,wood 覆盖) |
| 砖橙按钮/警示 | brick |
(201,111,74) | 127 | ✓ |
| 金/钱强调 | gold |
(232,169,66) | 166 | ✓ |
| 琥珀进度条 | amber |
(240,196,117) | 123 | ✓ |
| 差评红 | danger |
(178,72,54) | 124 | ✓ |
| 纸底(须排除) | paper #f6e7c9 |
(246,231,201) | 45 | ✗ 反例 |
| 高亮纸(须排除) | paperHi |
(255,246,227) | 28 | ✗ 反例 |
| 暖天光(须排除) | sky |
(243,217,168) | 75 | ✗ 反例 |
| 奶油粒子亮色(须排除) | cream |
(255,243,220) | 35 | ✗ 反例(近白,归引擎粒子域) |
判据(暖木/砖/金色域,排纸底与暖天光):
warmHit ⟺ (r > 140) && (r - b > 85) && (g < r)
- 命中:wood(R169,R−B90,118<169)✓ / brick(201,127,✓) / gold(232,166,✓) / amber(240,123,✓) / danger(178,124,✓)——店面木带/货架/按钮/HUD 金额(
render.js大量pal.c('wood'|'brick'|'gold'|'amber'),已读真源:render.js:120/165/179/200/253/308等); - 排除:paper(R−B45<85)✗ / paperHi(28)✗ / sky(75<85)✗ / cream(35)✗——纸底/天光/近白粒子均落空,堵「把暖纸背景误判成店面美术」的假阳性;
- 鲁棒性:rush 压力态色板经
shiftHsl(hue −10, sat 1.18, light 0.9)(palette.js:57)把暖色更暖(R−B 进一步拉大、饱和升),仍稳在色域内,不会因 rush 漂出; - 阈值
warmHits > 200:扫 780×1688 backing(或抽样网格,与 gate0/a6 同扫法),店面木带+货架+按钮+招牌占屏面积远超 200 抽样点;过窄(真渲也判 0=假阴性)与过宽(纸底误命中=假阳性)两侧均已由上面色域不等式+反例锚定。N 在 driver 落地时按实际抽样网格密度微调,但色域不等式不动(不等式是堵假阳性的客观锚,开工前定死)。
9. 完成条件
- 装载契约
game-runtime/src/core/game-host.d.ts落盘(§12-A1)+contracts/README.md指针登记(不折进跨仓 8 类);只import type { PluginContext } from './api.d.ts'、api.d.ts零改(diff 证)。 - game 工厂
game-shop.js为独立文件、默认导出GameHostFactory、零引擎 import(非 main.js 内联,§12-A3 硬约束)。 host-dev/engine-caps.js提升完成;host.js import 它;门0 8/8 + real 门 6/6 回归逐项不变(evidence 证)。- ref
?engine=real通道:门① 掌帧 ∈[20,45] ∧ 门② 同屏(#game-engine上游戏美术warmHits>200(§门②(a) 坐实暖色判据)+ 引擎粒子brightHits>0∧maxCh>80双命中)∧ 门③particles.spawnEmittercall-ID ∧ 门④ CDP 真玩闭环(真触摸+轮询范式,非 stepFrames 手摇)+四件套 ∧ 门⑦ console 零未捕获——全绿。 - real 通道就绪标记
__refBootedReal接线正确(driver real 分支等它,§12-M1);bundle 在 engineInit().then 内以context2d=mainContext建(§12-M4)。 - ref
?engine=stub门⑤ 逐像素双开哈希一致 + 49 单测绿(stub 通道行为逐行不变)。 - SIZES.md 增游戏产物口径(gz 实测)。
- 受控面 6 项契约零改(硬约束守住)。
- 人在 mini-desktop 浏览器
?engine=real真玩一局(开店→营业→结算→升级),截图归档。
10. 回滚
- 代码:
?engine=stub与今天main.js逐行等价;real 过不了→用户/取证走 stub,既有交付零连累。 - 契约:
game-runtime/src/core/game-host.d.tsadditive(落 src/core,§12-A1),移除不破存量;受控面零改。 - 模块:engine-caps.js 提升若回归→git revert 该文件 + host.js import 行(host.js 退内联闭包),ref real 自带 engineFactory 经 createEngineCaps 不连累。
- 构建:ref-bundle stub 版(197KB,不打引擎)随时可重出,回退引擎入包。
11. 风险与假设(诚实标注)
- [verified] 装载契约全仓不存在(grep 零命中);ref 游戏层 file:line(
main.js:112/583-602/618/621/648/668-691,stepFrames手摇:664/:109);host.js real 范式(host.js:323-324/353-371/379-381/488-490/536-538/830/840/904/917-920/930-932,缺省 real:340);index.html__refBooted置位时机(index.html:80-81);driver 现状(e2e-input.cdp.cjs:76/109/127/144/236/277);makeEngineCaps 闭包(host.js:197-318)+ engineFactory 注入(host.js:359);burst colorStart 白(particles-juice/impl.js:217,gate0 青蓝:111/ a6 亮度:111);palette 暖色 BASE(palette.js:23-41,rush shiftHsl:57);zone kind 枚举(render.js:967/975/981);ref runbook git-clone-scratch(ref-e2e-runbook.sh:20-21/44/47/48);contracts 跨仓 SSOT 定位(contracts/README.md:1-3§1,含拆仓计划);sdk-interface.d.ts 自洽无 game-runtime import(先例)。 - [verified·复审降级] ref entry 加引擎 import 能被 esbuild 解析入包——host-dev/entry.js 末行已
import * as LJS from 'littlejsengine'且 host-dev real 门 6/6 已过(同 monorepo node_modules),已被既有事实强力支撑;作常规构建核对(grep -q ParticleEmitter,步骤 0),非阻断门(§12-A5)。 - [inferred] ref real ~540KB 不触 B1 入场券——基于 host-dev entry 544KB 实测 + gz 量级;P1 用
size.mjs实测游戏产物 gz 坐实(步骤 6)。 - [已裁·非待确认] game 工厂必须独立文件
game-shop.js、默认导出、零引擎 import(§12-A3 硬约束,不再留「可 main.js 内联」口子)。 - [已裁·非占位] 门②(a) 暖色判据已开工前从
palette.js坐实具体色域(r>140 && r−b>85 && g<r,排纸底/天光/近白,见 §8「门②(a) 暖色判据」)——核心门判据不带占位落地。 - [待确认·留创始人] 工件命名
game-host.d.ts(暂名);若创始人偏好其它名(如game-loadable.d.ts),执行版改名即可,形态/落点不变。这是唯一留给创始人的 open item,不阻断执行(命名改名零成本)。
12. 复审裁决依据(2026-06-13 对抗复审 · 逐条)
复审口径:execution 是落地契约 + 高风险(立新契约 + 改真游戏主循环),deep 校准全程读真源码、凡引代码事实带 file:line。无 fatal;4 major 全纳入、5 项不明假设逐条处置。下表为各条裁决与依据,正文已落地。
| 编号 | 复审条目 | 裁决 | 落地位置 / 依据 |
|---|---|---|---|
| M1 | real 就绪标记缺失(__refBooted 早于引擎/game 就绪被置,driver 据它打空/flaky) |
纳入·硬约束:新增 real 专属 window.__refBootedReal(只在 engineInit().then 内、game.init 完成后置);driver real 分支改等它 |
步骤 4.3(setupReal 末置 __refBootedReal)+ 步骤 8a(driver 等它)+ §7 失败路径;依据 index.html:81/host.js:920/851/931/e2e-input.cdp.cjs:76 |
| M2 | stepFrames 手摇范式在引擎掌帧下失效(且沿用=双掌帧违铁律);改造被低估为「加通道断言」 | 纳入·范式切换:real driver 禁 stepFrames,改真触摸+delay等引擎自然推帧+轮询状态;明列这是相对 stub 的范式切换非增量 |
步骤 8c(确定性序列范式切换)+ §8 门④范式注 + IDL 不可破约定 1 推论;依据 e2e-input.cdp.cjs:109/144/236/277、harness §5-坑6 |
| M3 | 门②(a) 暖色阈值与 gate0/a6 青蓝/亮度口径相反,却留执行时占位 | 纳入·开工前坐实:从 palette.js BASE 取定暖色色域不等式 r>140 && r−b>85 && g<r + 纸底/天光/近白反例排除;核心门判据不带占位 |
§8「门②(a) 暖色判据」(已坐实色域表+不等式+反例);依据 palette.js:23-41/57、gate0:111、a6:111、harness §6-2 |
| M4 | render(g) 注入路径 / bundle 重建顺序未钉死;ref 顶层同步建 bundle 会快照错 ctx | 纳入·硬步骤:real bundle 必须在 engineInit().then(setupReal) 内、以 context2d=mainContext 建(不得顶层先建再改);输入桥+game.init+就绪标记同移进 setupReal;明示这是 bootRef 控制流重构 |
步骤 4.3(M4 硬约束)+ 4.4(输入移进 setupReal)+ IDL 不可破约定 2 推论;依据 host.js:323-324/353-371/379-381/536-538、main.js:112/584/590-602 |
| A1 | IDL 落 contracts/ 并反向相对 import ../game-runtime/src/api.d.ts(import 方向/落点合理性) |
改落点:落 game-runtime/src/core/game-host.d.ts(与 api.d.ts 同目录,import=sibling ./api.d.ts);不折进 contracts/ 跨仓 8 类(仅 README 指针)。理由:contracts/ 是跨三仓五工位业务契约 SSOT,本契约是 game-runtime 内部接线面,让 SSOT 反向相对依赖业务源码违「契约即 SSOT」且拆仓即断 |
头部「装载契约落点」+ §3 文件表 + §5 IDL 落点注 + 步骤 1;依据 contracts/README.md:1-3(拆仓计划)、sdk-interface.d.ts(自洽先例) |
| A2 | ref 缺省 stub vs host-dev 缺省 real(两套缺省心智) | 显式声明此分叉是有意为之:真玩取证显式带 &engine=real 更稳、stub 兜逐像素回归;P3 复制 ref 作模板时须知缺省 stub=「新游戏默认不上引擎」是过渡现状,引擎是正路(boot 注入装载契约) |
步骤 4.1(缺省 stub)+ 本表显式声明;依据 host.js:340(host-dev real)vs main.js:82(ref play/stub) |
| A3 | game 工厂「建议非强制」留 main.js 内联口子(P3 缺真实参照实现) | 钉成硬边界:game 工厂必须独立文件、默认导出、零引擎 import、禁 main.js 内联糊过 | §1.2 + 步骤 3 开头 + IDL GameHostFactory 注 + §9 完成条件 + §11;依据 P1 目标(spike 装载契约到可被 P3 复制) |
| A4 | §8 部署写 tar-pipe,与现行 ref-e2e-runbook.sh(git-clone-scratch)不符 | 统一为 git-clone-scratch:real 版只在 runbook driver URL 加 &engine=real + 构建后 grep ParticleEmitter;勿用 host-dev/a6 的 tar-pipe staging 配方 |
§8 部署口径段;依据 ref-e2e-runbook.sh:20-21/44/47/48 |
| A5 | 步骤0 inferred 风险被高估(host-dev 已证可入包) | 降级为常规构建核对:grep ParticleEmitter 即可,非可能阻断的高不确定项 | 步骤 0 + §11;依据 host-dev/entry.js 已 import 引擎且 real 门 6/6 已过 |