Files
youle_cocos/docs/superpowers/plans/2026-09-05-platform-ui-wiring.md

384 lines
25 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.
# Platform UI Wiring Core Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** 完成启动、登录、大厅、进房的无引擎 UI 接线核心,并用真实 PlatformRuntime 回放验证,不修改 Cocos 序列化资源。
**Architecture:** Runtime 保持唯一状态源;presentation 投影 ViewModel、转发命令并实现同步 ScenePort。实际节点绑定留给单独批准的 3B 编辑器批次;3A 使用记录型 ViewPort 验证。
**Tech Stack:** 现有 TypeScript、node:test、tsx;不新增依赖,不导入 cc。
**Spec:** `docs/superpowers/specs/2026-09-05-platform-ui-wiring-design.md`,并继承 `2026-09-04-framework-subgame-zero-coupling-migration-design.md`。
## Global Constraints
- 服务器包、route/rpc、roomtype 与 deskinfo 全部沿用已验证 contracts。不得为 UI 新增字段。
- 远程配置与原生读取/WVJB 不在本批修改。
- 任何 `.scene/.prefab/.anim/.meta` 变更均使 3A 验收失败。
- 3A 测试夹具只留在 framework-tests,不导入 assets,不声明生产 GameEntry。
- 不扩充 SDK 出口,不依赖旧 PlatformSession/RoomRPCBus,不引入下游兜底。
- 每个 Task 独立执行 RED → GREEN → 规格/实现审查 → 代码质量审查 → 修复复验 → scoped commit。审查不通过不得推进下一 Task。
- 执行使用新的隔离 worktree;先按 using-git-worktrees 核实,不重建已删除的旧工作树来冒充继续旧任务。本文件不授权编辑器资源写入。
## 执行环境与文件地图
所有代码路径在本计划中相对 `cocoscreator_projects/`;文档路径相对仓库根。所有命令在 `cocoscreator_projects` 执行,git 路径也据此指定。
先完整阅读仓库 AGENTS.md、上述两个设计及本计划。基线为 `8eae325`,执行时检查分支状态并记录新的 base SHA;保留无关修改。不需要重新执行已完成的 decoupled-runtime Tasks 1–11。
新增 `YouleNexus/assets/framework/presentation/`:`ui-contracts.ts` 管端口和模型;`page-model.ts` 管纯投影;`frame-renderer.ts` 管合帧与取消;`platform-ui-controller.ts` 管 ScenePort 和交互。不建第二个 Store 或 barrel SDK 出口。
修改 `platform/runtime.ts` 只增加只读观察及其终止清理;测试放在 `framework-tests/presentation/`,真实运行时联合回放追加到已有 `framework-tests/platform/runtime.test.ts`,复用其中 makeRuntime、RecordingWireClient、ManualClock、makeGameEntry、fixture,不把测试帮助函数搬进产品代码。
通用验证命令:
```powershell
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
node scripts/check-import-boundaries.mjs
node --test framework-tests/architecture/import-boundaries.test.mjs
git diff --check
```
npm 不可解析时使用 `C:\Users\Joywayer\AppData\Local\Author Software\nvm\.nodejs\npm.exe`。tsx 启动的 `uv_os_get_passwd` ENOMEM 属于待核实环境失败,不是有效 RED;申请在允许的环境运行同一只读测试命令,不改依赖、不写个人环境绕过脚本。不要运行含旧资源迁移生成器的全量 npm test:framework。
## Task 1: 给 Runtime 增加有明确错误通道的只读观察
**Files:** Modify `YouleNexus/assets/framework/platform/runtime.ts`; Test `framework-tests/platform/runtime.test.ts`。
**Interfaces:** Consumes `PlatformStore.subscribe(listener: (state: PlatformState, previous: PlatformState) => void): () => void` 和 Runtime 既有清理路径。Produces `PlatformRuntime.subscribeState(listener: (state: PlatformState) => void, onError: (error: unknown) => void): () => void`,不立即通知,初值仍取 state。
- [x] 在 runtime.test.ts 现有 helpers 下增加实际观察测试:
```ts
test('UI observes canonical commits and unsubscribe is idempotent', async () => {
const { runtime, wire } = makeRuntime();
const seen: unknown[] = [];
const off = runtime.subscribeState(s => seen.push(s), e => { throw e; });
assert.equal(seen.length, 0);
await runtime.start();
wire.emit({ type: 'open', server: 'ws://agent' });
assert.equal(seen[seen.length - 1], runtime.state);
off(); off();
const count = seen.length;
runtime.login(ACCOUNT);
wire.emit({ type: 'message', message: {
route: 'agent', rpc: 'player_login', data: fixture('player-login-success.json'),
} });
assert.equal(seen.length, count);
runtime.stop();
assert.throws(() => runtime.subscribeState(() => {}, () => {}), /stopped/);
});
```
- [x] Run `node --import tsx --test framework-tests/platform/runtime.test.ts`,确认因缺 subscribeState 失败。
- [x] 增加每观察者 active 标志、取消集合及终止清理;Store 回调交付提交值,不改为读取可能被重入更新的最新值。核心正常取消结构:
```ts
let active = true;
const off = (): void => {
if (!active) return;
active = false;
unsubscribeStore();
this.stateObservers.delete(off);
};
const unsubscribeStore = this.store.subscribe(state => {
if (!active) return;
try { listener(state); }
catch (error) {
off();
try { onError(error); }
catch (callbackError) {
this.showFatalOnce(this.failureError('UI observer', error, [callbackError]));
}
}
});
this.stateObservers.add(off);
return off;
```
`stateObservers` 是 Runtime 新增 `Set<() => void>`;现有 failureError 签名为 `(operation: string, primary: unknown, cleanupErrors: readonly unknown[]): Error`。入口拒绝 `kicked`、`fatal`、`stopped`,错误携带当前生命周期;不新增生命周期。stop 与 fatal 在外部清理回调前使观察者失效。不得将原始错误改成默认数据。
- [x] 追加 RED/GREEN:监听器抛错原对象只交付一次、其他监听继续;onError 抛错保留两错误;stop/fatal 后迟到 Store 回调无效;监听中 stop/取消其他监听/再订阅不会复活。测试使用现有 runtime helpers 和 Store 行为,不用私有字段强转写状态。
- [x] 重跑上述测试和通用验证。规格审查核对原引用、初值与取消语义,质量审查核对异常/重入清理。提交只含本 Task 两个文件,message `feat(platform): expose lifecycle-safe UI observation`。
## Task 2: 定义 UI 端口并实现唯一快照投影
**Files:** Create `YouleNexus/assets/framework/presentation/ui-contracts.ts`, `YouleNexus/assets/framework/presentation/page-model.ts`, `framework-tests/presentation/page-model.test.ts`。
**Interfaces:** Consumes Task 1 的 PlatformRuntime 只读观察和既有 `PlatformState`、`selectSelfPlayer`、`selectRoomPlayers`。Produces 下列完整端口与 `selectPageModel(page: PageId, state: PlatformState, ready: boolean): PageModel`:
```ts
import type { PlatformRuntime } from '../platform/runtime.ts';
import type { ServerDenial } from '../platform/runtime-session.ts';
export type UiRuntime = Pick<PlatformRuntime,
'state' | 'ready' | 'subscribeState' | 'login' | 'joinRoom' | 'prepare' | 'exitRoom'>;
export type PageId = 'loading' | 'login' | 'lobby' | 'room';
export interface PlayerModel {
readonly playerid: number; readonly nickname: string;
readonly avatar: string; readonly bean: number;
}
export type SeatModel =
| { readonly kind: 'empty'; readonly seat: number }
| { readonly kind: 'occupied'; readonly seat: number;
readonly player: PlayerModel; readonly ready: boolean; readonly offline: boolean };
export type PageModel =
| { readonly page: 'loading' }
| { readonly page: 'login'; readonly canLogin: boolean }
| { readonly page: 'lobby'; readonly self: PlayerModel }
| { readonly page: 'room'; readonly roomcode: string; readonly selfSeat: number;
readonly seats: readonly SeatModel[]; readonly canPrepare: boolean; readonly canExit: boolean };
export type OverlayModel =
| { readonly kind: 'none' }
| { readonly kind: 'reconnect' }
| { readonly kind: 'denial'; readonly outcome: ServerDenial }
| { readonly kind: 'kicked'; readonly data: unknown }
| { readonly kind: 'fatal'; readonly error: Error };
export interface FrameScheduler { request(callback: () => void): () => void; }
export interface PlatformViewPort {
render(model: PageModel): void;
activate(page: PageId): void;
showOverlay(model: OverlayModel): void;
}
```
- [x] 编写投影 RED;测试文件按相对路径 `../../YouleNexus/assets/framework/...` 导入类型/函数,使用 node:test 和 strict assert:
```ts
test('empty seats keep authoritative positions and count', () => {
for (const count of [2, 4, 10]) {
const state: PlatformState = {
app: { phase: 'logged-in' },
players: { selfPlayerId: 7, entities: {
7: { playerid: 7, nickname: 'self', avatar: '', bean: 0, sex: 0,
isprepare: 0, onstate: 0 },
} },
room: { kind: 'inside', roomcode: '000123', selfSeat: 0,
seatPlayerIds: [7, ...Array<null>(count - 1).fill(null)],
readySeats: [], offlineSeats: [], roomtype: [], stage: 0,
needprepare: 1, infinite: 0 },
};
const model = selectPageModel('room', state, true);
assert.equal(model.page, 'room');
if (model.page !== 'room') throw new Error('expected room');
assert.equal(model.roomcode, '000123');
assert.equal(model.seats.length, count);
assert.deepEqual(model.seats[1], { kind: 'empty', seat: 1 });
assert.equal(model.canPrepare, true);
assert.equal(model.canExit, true);
assert.ok(Object.isFrozen(model));
assert.ok(Object.isFrozen(model.seats));
}
});
```
- [x] Run `node --import tsx --test framework-tests/presentation/page-model.test.ts`,缺模块/函数为预期 RED。
- [x] 实现 switch(page),所有输出及嵌套模型 freeze;不直接泄露 PlatformPlayer 扩展字段。empty 来源仅允许 null。room occupied 的 onstate/isprepare 缺失抛带 seat/player 路径错误;selfSeat 必须指向 self。核心投影:
```ts
const playerModel = (player: PlatformPlayer): PlayerModel => Object.freeze({
playerid: player.playerid, nickname: player.nickname,
avatar: player.avatar, bean: player.bean,
});
const canExit = state.app.phase === 'logged-in'
&& (room.stage === 0 || room.infinite === 1);
```
PlatformPlayer 从既有 platform-types type-only 导入;room 是 inside 判别收窄后的 state.room。完整 canPrepare 条件按设计第 5 节,不扩展服务器规则。
- [x] 追加逐项 RED/GREEN:lobby 缺 self、occupied 缺实体、selfSeat 不匹配必报错;bean=0/空 avatar 不变;已有准备/离线状态;战斗普通房禁退出、infinite 房可退出;reconnecting 禁命令;输入 roomtype 完全不解析。
- [x] 重跑本测试和通用验证;规格审查查空位及服务器原值,质量审查查冻结/判别联合/职责。scoped commit 三文件,message `feat(presentation): project immutable public page models`。
## Task 3: 实现可取消、抗重入的合帧渲染
**Files:** Create `YouleNexus/assets/framework/presentation/frame-renderer.ts`, `framework-tests/presentation/frame-renderer.test.ts`。
**Interfaces:** Consumes Task 2 `FrameScheduler`。Produces `class FrameRenderer`,constructor `(scheduler: FrameScheduler, render: () => void, onFault: (error: unknown) => void)`;方法 `request(): void`, `flush(): void`, `dispose(): void`。
- [x] 写入可直接运行的 RED(导入 FrameRenderer、test、assert):
```ts
test('coalesces frames and invalidates callbacks even if scheduler delivers late', () => {
const callbacks: (() => void)[] = [];
let cancelled = 0;
let rendered = 0;
const renderer = new FrameRenderer({
request(cb) { callbacks.push(cb); return () => { cancelled++; }; },
}, () => { rendered++; }, e => { throw e; });
renderer.request(); renderer.request();
assert.equal(callbacks.length, 1);
renderer.flush();
assert.equal(rendered, 1);
callbacks[0]!();
assert.equal(rendered, 1);
renderer.request();
renderer.dispose(); renderer.dispose();
callbacks[1]!();
assert.equal(rendered, 1);
assert.equal(cancelled, 2);
});
```
- [x] Run `node --import tsx --test framework-tests/presentation/frame-renderer.test.ts`,确认缺类 RED。
- [x] 实现 pending cancel、generation、disposed。flush 先失效旧 token、取消旧任务再同步 render;同步异常直接抛出。request 的异步回调先释放 pending,再 try render,失败 dispose 并调用 onFault 原错误。dispose 先置终止位再调用外部 cancel;即使 cancel 抛错,迟到任务仍无效。核心失效顺序:
```ts
const cancel = this.pending;
this.pending = null;
this.generation += 1;
if (cancel !== null) cancel();
```
pending 初始为 null,generation 初始为 0。request/dispose 终止后的请求不排队;flush 终止后不渲染。取消错误和原 render 错误同时存在时用包含原对象的显式聚合错误,不吞掉任何一个。
- [x] 追加 RED/GREEN:render 内 request 只排下一帧;render 内 dispose 后不得重排;取消抛错仍失效;render 异步抛错只报一次;flush 同步抛错不吞。测试实现手动 scheduler,不用真实计时。
- [x] 重跑本测试与通用验证;规格审查查同帧最新/首帧,质量审查查 token 与回调重入。scoped commit 两文件,message `feat(presentation): add cancellable frame renderer`。
## Task 4: 实现 ScenePort Presenter 和原样命令入口
**Files:** Create `YouleNexus/assets/framework/presentation/platform-ui-controller.ts`, `framework-tests/presentation/platform-ui-controller.test.ts`。
**Interfaces:** Consumes Tasks 1–3。Produces `class PlatformUiController implements ScenePort`;constructor `(view: PlatformViewPort, scheduler: FrameScheduler, onFault: (error: unknown) => void)`;`connect(runtime: UiRuntime): void`;全部既有 ScenePort 方法;`login(account: LoginAccountIdentity): void`, `joinRoom(command: JoinRoomCommand): void`, `prepare(): void`, `exitRoom(): void`, `dismissDenial(): void`, `dispose(): void`。账号/命令/ScenePort 类型均 type-only 导入既有定义。
- [x] 先写纯端口 RED,测试 helper 也留在本测试文件:
```ts
test('login forwards the exact account only after the runtime opens login', () => {
const sent: unknown[] = [];
const runtime: UiRuntime = {
state: { app: { phase: 'connected' },
players: { selfPlayerId: null, entities: {} }, room: { kind: 'outside' } },
ready: true, subscribeState: () => () => {},
login: account => { sent.push(account); }, joinRoom: () => {},
prepare: () => {}, exitRoom: () => {},
};
const events: string[] = [];
const controller = new PlatformUiController({
render: model => { events.push(`render:${model.page}`); },
activate: page => { events.push(`activate:${page}`); }, showOverlay: () => {},
}, { request: () => () => {} }, error => { throw error; });
controller.connect(runtime);
const account = { openid: 'o1', nickname: 'name', avatar: '', sex: 0,
province: '', city: '', unionid: 'u1' };
assert.throws(() => controller.login(account), /login/);
controller.showLogin();
controller.login(account);
assert.equal(sent[0], account);
assert.deepEqual(events.slice(-2), ['render:login', 'activate:login']);
controller.dispose();
assert.throws(() => controller.login(account), /disposed/);
});
```
- [x] Run `node --import tsx --test framework-tests/presentation/platform-ui-controller.test.ts`,缺类为 RED。
- [x] 实现一次 connect;订阅之后读取 runtime.state。尚未收到 ScenePort 页面请求时不画业务页。切页保存 page、flush 最新模型、检查未被 render 重入终止后 activate;不调用 director/loadScene。核心模型读取:
```ts
private renderCurrent(): void {
if (this.page === null) return;
const runtime = this.requireRuntime();
this.view.render(selectPageModel(this.page, runtime.state, runtime.ready));
}
```
page 初始 null;requireRuntime 对未 connect/已 dispose 显式抛错。用 FrameRenderer 承担合帧,controller 保留 terminal/disposed 生命周期及当前 OverlayModel,不保留玩家/房间镜像。每个外部 View 调用后检查终止位再继续。connect 重复拒绝;dispose 幂等,取消 frame 和 subscription 均尝试执行。
- [x] 实现命令 guards,直接 `runtime.login(account)`/`runtime.joinRoom(command)`,不 spread 或加工业务输入。prepare/exit 使用最新 page model 决定 UI 可用性。恢复/denial/kick/fatal 规则严格按设计第 6 节:terminal 禁命令但仍允许第一次 fatal 画 overlay,fatal 优先于 kicked。
- [x] 每个行为先加 RED 再实现:join 原引用;ready=false 禁登录;每帧最新 state;showRoom 同步 render→activate;denial 保留 data 身份且重试成功;connected/logged-in 清 reconnect 但不清 denial;kick/fatal 后迟到回调不绘制;render/activate/showOverlay 中 dispose 不再调用后续 View;订阅/frame 错误调用 onFault,同步错误向 Runtime 传播。
- [x] 重跑三个 presentation 测试和通用验证;规格审查查导航唯一来源/拒绝重试,质量审查查 controller 未引入第二状态源、循环清理。scoped commit 两文件,message `feat(presentation): wire platform scenes and UI commands`。
## Task 5: 用真实 Runtime 验证整条接线并锁定边界
**Files:** Modify `framework-tests/platform/runtime.test.ts`; Create `framework-tests/architecture/presentation-boundaries.test.mjs`; Modify 本计划仅勾选已验证任务并记录结果。
**Interfaces:** Consumes `PlatformUiController`、真实 `PlatformRuntime` 及 runtime.test.ts 现有 helpers。Produces 无新增生产 API;只读集成回放与静态边界门禁。
- [x] 将下面集成 RED 加入 runtime.test.ts,导入 controller;使用现有 ACCOUNT、DEVICE、runtimeConfig、RecordingWireClient、ManualClock、fixture、makeGameEntry:
```ts
test('real runtime renders the room before restoring canonical deskinfo', async () => {
const events: string[] = [];
const wire = new RecordingWireClient();
const controller = new PlatformUiController({
render: model => { events.push(`render:${model.page}`); },
activate: page => { events.push(`activate:${page}`); },
showOverlay: model => { events.push(`overlay:${model.kind}`); },
}, { request: () => () => {} }, error => { throw error; });
const deskinfo = { fixture: 'opaque' };
const entry = makeGameEntry({ createModule: () => ({
attach() { events.push('attach'); },
handlePlatformEvent() {}, handleGameMessage() {}, dispose() {},
restore(value) { assert.equal(value, deskinfo); events.push('restore'); },
}) });
const runtime = new PlatformRuntime({
gameEntry: entry, resolveRuntimeConfig: async () => runtimeConfig(),
createWireClient: () => wire, scene: controller,
loadResources: async () => {}, waitForMinimumDisplay: async () => {},
getLoginDeviceSnapshot: () => DEVICE, clock: new ManualClock(),
});
controller.connect(runtime);
await runtime.start();
wire.emit({ type: 'open', server: 'ws://agent' });
controller.login(ACCOUNT);
wire.emit({ type: 'message', message: { route: 'agent', rpc: 'player_login',
data: fixture('player-login-success.json') } });
controller.joinRoom({ roomcode: '100001', location: null, ip: '127.0.0.1' });
wire.emit({ type: 'message', message: { route: 'agent', rpc: 'self_join_room',
data: { ...fixture('self-join-room.json'), deskinfo } } });
assert.ok(events.includes('render:lobby'));
assert.ok(events.indexOf('attach') < events.indexOf('render:room'));
assert.ok(events.indexOf('render:room') < events.indexOf('activate:room'));
assert.ok(events.indexOf('activate:room') < events.indexOf('restore'));
controller.dispose(); runtime.stop();
assert.equal(wire.stopCalls, 1);
});
```
- [x] Run `node --import tsx --test framework-tests/platform/runtime.test.ts`。联合测试若初次已绿,不制造伪失败;通过以下定向负例验证测试能抓回归:临时在 controller 中把 showRoom 首帧排到 scheduler,确认该用例因缺 room render/activate 失败,然后恢复正确代码。该受控变异只涉及本批 TS 并不得提交。
- [x] 追加真实 Runtime 回放:非零登录、非零进房保持原 Store root/原 data,重试能成功;断线重连 overlay 可恢复;kick/fatal 无后续 UI 命令;延迟 loadResources 时不得由 controller 登录;view 回调销毁 Runtime/Presenter 不得 restore;stop 后旧 scheduler 回调无效。夹具数据仅在测试中具备明确来源,不改服务器 fixture 文件来让错误行为通过。
- [x] 编写 architecture 测试,用 TypeScript AST 遍历 presentation 的 import/export/call expression,type-only 合同引用允许,值导入 protocol/net/cc 和动态导入同样禁止。扫描使用 `readFileSync/readdirSync`,不启动 Creator、不写资源。基础 RED 断言先覆盖禁止真实值导入:
```js
import ts from 'typescript';
import assert from 'node:assert/strict';
import { test } from 'node:test';
function forbiddenSpecifier(specifier) {
return specifier === 'cc' || /(?:^|\/)(?:net|protocol)(?:\/|$)/.test(specifier)
|| /(?:platform-session|room-rpc-bus)(?:\.|$)/.test(specifier);
}
test('presentation boundary recognizes forbidden production imports', () => {
assert.equal(forbiddenSpecifier('../net/wire-client.ts'), true);
assert.equal(forbiddenSpecifier('../protocol/contracts/index.ts'), true);
assert.equal(forbiddenSpecifier('../platform/runtime.ts'), false);
});
```
完成 AST 取值与 type-only 判别后,增加内联正反样本(普通/混合 type import、export from、import()、require()),再扫描真实目录;同文件核对 SDK index 仍仅导出 contracts。禁止整个 presentation 被添加到子游戏例外白名单。不要用单个字符串搜索冒充完整边界检查。
- [x] Run `node --test framework-tests/architecture/presentation-boundaries.test.mjs`;暂时注入禁止 import 到新增 presentation 文件验证真实扫描失败,立即恢复;GREEN 后运行所有纯 TS 测试:
```powershell
$uiCoreTests = @(rg --files framework-tests -g '*.test.ts')
if ($uiCoreTests.Count -eq 0) { throw 'No TypeScript tests selected' }
node --import tsx --test --test-concurrency=1 @uiCoreTests
node --test framework-tests/architecture/import-boundaries.test.mjs framework-tests/architecture/presentation-boundaries.test.mjs
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
node scripts/check-import-boundaries.mjs
git diff --check
git diff --name-only HEAD
git status --short
```
运行前检查所选 .test.ts 没有生成资源副作用;不混入 legacy-layer-migration 的 .mjs 生成器。核对从执行 base SHA 到 HEAD 的 committed diff 以及工作区/untracked,确认没有 `.scene/.prefab/.anim/.meta`;不仅检查最后一个 commit。
- [x] 规格审查把设计第 8 节每项映射到测试;质量审查核对 UI 生命周期重入和静态检查漏口。只提交本 Task 测试和计划记录,message `test(presentation): verify runtime UI wiring and boundaries`。
## 完成与后续门槛
### 3A 执行验证记录(2026-09-05)
- 执行分支 `codex/platform-ui-wiring-core`;实际 base `2d7655a5d7d2e56733d9622eac6d6e0fcb36254b`,Tasks 1–4 已经逐项规格与质量审查并提交,Task 4 head `d29a659541bcb0dcfe9037a6122ff03a25b9404f`。
- Task 5 实现后的独立验证:显式枚举 `.test.ts` 的回归 499/499;两个指定架构测试文件合计 49/49;上述命令退出码均为 0。`tsc --noEmit`、`check-import-boundaries.mjs`、`git diff --check` 通过。旧 EventBus/reactive/StoreBinder 异常路径测试有预期错误日志;新增测试输出干净。
- 真实 Runtime 63/63,新增回放验证原始 deskinfo 恢复顺序、非零拒绝原引用及重试、重连、终止、资源门槛及重入销毁。异步 showRoom 定向变异和真实禁止 import 变异均使对应测试失败,已立即恢复。
- 验证前扫描纯 TS 测试的资源写入风险;未运行生成器混入的全量 runner。分支 base 到当前提交及未提交/untracked 范围没有 `.scene/.prefab/.anim/.meta` 差异。执行早期误运行旧 runner 产生的两份 out prefab 变更已获用户明确授权、精确恢复并核验。
- UI 接线核心仅为 3A 无引擎回放;不是实际 Cocos 接管。Task 5 已完成规格与质量审查:SDK 本地导出漏检及精确声明数量问题经过两轮 RED/GREEN 修复和范围复审,全部关闭。最终整分支审查已通过,审查范围 `2d7655a..ec67fe6` 无 Critical/Important/代码类 Minor 问题;`ec67fe6` 上重新验证 TS 499/499、架构 49/49、类型与导入检查通过,工作区及整个提交范围无序列化资源差异。本次状态更新仅修改文档。
报告实际测试命令、数量、退出码、base/head SHA 与资源零变更检查;不沿用过去的 408/408 数字当本批结果。3A 完成可表述为“UI 接线核心已通过无引擎回放”。
不自动进入 3B。进入实际 Cocos 接管前,须另行确认允许修改的序列化文件、真实 GameEntry/目标宿主/账号来源,再通过 MCP 核实控件绑定并制定编辑器实施任务。主题扩展与真实子游戏继续各自独立计划。