games-development-ai/docs/agent-specs/2026-06-13-P1-王蓝莓上引擎-execution.md
zizi 99b6c82dec feat(runner-v2): P1 王蓝莓上引擎=游戏宿主装载契约 spike 实(mini-desktop real 引擎 5 门全过)
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>
2026-06-13 18:32:50 +00:00

371 lines
45 KiB
Markdown
Raw 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.

# P1 · 王蓝莓上引擎spike 游戏宿主装载契约)· 执行版
> **状态**execution 版 · 2026-06-13 · 给执行 agent 实现。配套 review 版 [`2026-06-13-P1-王蓝莓上引擎-review.md`](./2026-06-13-P1-王蓝莓上引擎-review.md);母梁 [`2026-06-13-runner-v2-arc-review.md`](./2026-06-13-runner-v2-arc-review.md) §5.1。
> **修订**2026-06-13 对抗复审deep·全程读真源码→ 纳入 4 majorreal 就绪标记 / 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/hostQ4区分 verified/inferred/assumption凡引代码事实带 file:line。
> **装载契约落点(复审改)**IDL 落 `game-runtime/src/core/game-host.d.ts`(与 `api.d.ts` 同目录),**不**落 `contracts/`——`contracts/` 是跨三仓五工位的**业务契约** SSOTAPI/DB/SDK/event…README §1装载契约是 game-runtime 内部「host↔游戏工厂」接线面消费方只在 game-runtimehost 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-ref` real 通道改造:`entry.js` import 引擎、`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 DO2 已拍)。
- 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 + chrome6c6g 禁 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构建可引擎入包 | inferredhost-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`engineFactoryreal 须延后到 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-M1stub 仍用 `__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-pipeURL 加 `&engine=real`;构建后 `grep -q ParticleEmitter` 核引擎入包§12-A4 |
| `SIZES.md` | 改 | 增"游戏产物=引擎+包装层"口径 |
---
## 4. 数据流
```mermaid
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: engineFactoryparticles/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`
> **设计原则**:游戏 = 纯模块工厂(零引擎 importQ4。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
```typescript
// game-runtime/src/core/game-host.d.ts —— 游戏宿主装载契约(第 9 类契约 · additive · game-runtime 内部)
// ownerRunner v2 P1 消费方:集成段 hostboot 游戏)+ 生成 agent产出靶
//
// 【为什么新立、不折进 api.d.ts】
// api.d.ts 的 PluginContext 是「插件↔引擎」面getEngine() 等api.d.ts:303已冻结
// 本契约是「游戏整体↔引擎宿主」面——游戏的 update/render 主体不是插件、不走 PluginContext。
// 两者正交(边界模型「同一职不留两条并行路」):插件经 getEngine() 拿能力,游戏经本契约挂宿主。
// 引擎 import 只活集成段 hostQ4 铁律),游戏工厂零引擎 import。
// 【为什么落 game-runtime/src 而非 contracts/】
// contracts/ 是跨三仓五工位的业务契约 SSOTAPI/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引擎 mainContextsetGLEnable(false) 后的纯 2D 绘制面)。游戏 render(g) 的 g 即它;
* 游戏美术画在此面 → 与引擎粒子(引擎自渲落同一 mainContext同屏。**此字段是装载契约的核心**
* (游戏取引擎绘制面的唯一约定,不借 PluginContext.getContext2d——那是插件面
* - seed主随机种子确定性canvas可见画布引用输入 rect 映射用real=引擎 mainCanvas
*/
export interface GameHostBootContext {
ctx: PluginContext; // 下层已冻结受控面(复用)
mainContext: CanvasRenderingContext2D; // 引擎 mainContextboot 注入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=mainContextboot 注入的同一面)
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 三条不可破约定(对齐边界模型)**
1. **掌帧唯一源=引擎**`GameInstance` 不含任何 RAF/计时器;`update/render` 只被引擎五回调驱动。**推论§12-M2**:取证 driver 在 real 通道亦不得手摇 `stepFrames`(否则与引擎 RAF 双掌帧,破此条)——真触摸后等引擎自然推帧 + 轮询状态。
2. **绘制面唯一=引擎 `mainContext`**`render(g)` 的 g 必为 boot 注入的 `mainContext`;游戏美术与引擎粒子同屏。**推论§12-M4**real 通道 `bundle` 必须在 `engineInit().then` 内、以 `context2d=mainContext` 建(受控面 getContext2d 一次性快照闭包捕获值,`host.js:323-324`);不得在顶层先建再改 ctx。
3. **能力唯一面=下层 `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-318` `makeEngineCaps` 闭包体整体搬入,**逐行保语义**
- 首行 `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.2 `host.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 逐项不变**(提升不得改任何判据数值)。
### 步骤 3ref 拆出装载契约 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=`#game` ctx保 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 registryP1 可最小实现,须声明)。
- **难点real 坐标系)**:引擎 `mainContext` 无 DPR 变换(引擎每帧 `updateCanvas` 重置为单位阵)。游戏按逻辑 390×844 绘制 → `render(g)` 须在 `g.save()``g.setTransform(DPR,0,0,DPR,0,0)` + `g.imageSmoothingEnabled=false`,对齐 `host.js:489-490` renderParticles 的补偿做法。stub 通道 g=`prepareCanvas(#game)` 已自带 DPR 变换(`main.js:69`)→ 不补。
### 步骤 4main.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 前)。
- 建独立 `engineRoot` div 挂 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」**绝不可在顶层先用 `#game` ctx 建 bundle 再改**,否则 getContext2d 快照到 stub `#game` ctx受控面 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
- 五回调:`engGameUpdate(){ game.update(FIXED_DT); }``engGameRender(){ game.render(mainContext); }`;其余空/留痕(`engGameInit(){ window.__refEngineInitFired=true; }`)。五回调函数定义在 bootRef 作用域engineInit 须先拿到),实体经 holder 转发到 setupReal 内真实现(仿 `host.js:880-887``_engStep`/`_renderParticles` holder 范式)。
- **mockNowMs**real 下由 `game.update``mockNowMs += dt*1000` 累加(逻辑帧钟,非墙钟,同 `host.js:526`)。
- **RAF 门控**`startRaf/rafLoop``main.js:668-691`)仅 stub 调real 分支不调(引擎内部 RAF 驱动五回调)。
4.4 输入real·控制流重构 §12-M4real 下输入监听**改挂 `LJS.mainCanvas`**(不再硬绑传入的 `opts.canvas`=`#game`;坐标按引擎 canvas rect仿 `host.js:536-538` `inputTarget = 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`)。
### 步骤 5entry.js 引擎入包
- `entry.js:15` 后增 `import * as LJS from 'littlejsengine';` + `export const Engine = LJS;`(仿 `host-dev/entry.js:21/28`)。**Q4**:引擎 import 只活 entry/hostgame 工厂零引擎 import构建后核 `game-shop.js` 编译产物经 LJS namespace 间接,不直 import
### 步骤 6SIZES.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`)。
### 步骤 8e2e driver real 通道改造清单(范式切换·非增量 · §12-M1/M2/M3
> **背景(复审 major 2/1**:现行 `e2e-input.cdp.cjs` driver 主体建在 **`window.__refHost.stepFrames(n)` 手摇逻辑帧** 上(`e2e-input.cdp.cjs:109`,贯穿 `:144` tapAt 内 `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 就绪前被置 truebootRef 顶层同步段先 resolvedriver 据它驱动会打空/读 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-律4git-clone-scratch 是 ref runbook 既有口径,勿改成 host-dev/a6 的 tar-pipe staging 配方。
**构建命令**mini-desktopcwd=`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-坑6ref 49 单测仍绿 | 现有 `e2e-input.cdp.cjs` 门 Bstub 通道不变) |
| **⑥ 提升回归** | engine-caps 提升后host-dev 门0 8/8 + real 门a66/6 逐项不变 | 步骤 2.3 |
| **⑦ console 干净** | `uncaught=[]`real 通道,含未捕获 error/unhandledrejection | ref driver`main.js:90-96` uncaught 归集) |
> **门④ driver 范式(复审 M2**real 通道 driver=真触摸+轮询(**禁 stepFrames 手摇**,否则双掌帧违铁律),坐标走 `#game-engine` rect就绪等 `__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 | RB | 命中 |
|---|---|---|---|---|
| 柜台/货架木带 | `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,RB90,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(RB45<85)✗ / paperHi(28)✗ / sky(75<85)✗ / cream(35)✗——纸底/天光/近白粒子均落空把暖纸背景误判成店面美术的假阳性
- 鲁棒性rush 压力态色板经 `shiftHsl(hue 10, sat 1.18, light 0.9)``palette.js:57`把暖色**更暖**RB 进一步拉大饱和升仍稳在色域内不会因 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.spawnEmitter` call-ID CDP 真玩闭环**真触摸+轮询范式 stepFrames 手摇**+四件套 console 零未捕获——**全绿**。
- real 通道就绪标记 `__refBootedReal` 接线正确driver real 分支等它,§12-M1bundle 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.ts` additive 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 && rb>85 && g<r`排纸底/天光/近白 §8②(a) 暖色判据」)——核心门判据不带占位落地
- **[待确认·留创始人]** 工件命名 `game-host.d.ts`暂名若创始人偏好其它名 `game-loadable.d.ts`执行版改名即可形态/落点不变。**这是唯一留给创始人的 open item不阻断执行**命名改名零成本)。
---
## 12. 复审裁决依据2026-06-13 对抗复审 · 逐条)
> 复审口径execution 是落地契约 + 高风险(立新契约 + 改真游戏主循环deep 校准全程读真源码、凡引代码事实带 file:line。无 fatal4 major 全纳入、5 项不明假设逐条处置。下表为各条裁决与依据,正文已落地。
| 编号 | 复审条目 | 裁决 | 落地位置 / 依据 |
|---|---|---|---|
| **M1** | real 就绪标记缺失`__refBooted` 早于引擎/game 就绪被置driver 据它打空/flaky | **纳入·硬约束**新增 real 专属 `window.__refBootedReal`只在 `engineInit().then` game.init 完成后置driver real 分支改等它 | 步骤 4.3setupReal 末置 `__refBootedReal`+ 步骤 8adriver 等它+ §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 && rb>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.3M4 硬约束+ 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 realvs `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.shgit-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 已过 |