Files
youle_cocos/docs/superpowers/plans/2026-09-04-platform-vertical-slice.md
T

1211 lines
56 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.
# YouleNexus Platform Vertical Slice 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 (`- [ ]`) syntax for tracking.
**Goal:** 建立可复用、可测试、与具体子游戏解耦的平台运行时,并跑通配置、登录、大厅、进房、准备和断线重连的第一条纵向链路。
**Architecture:** 平台核心通过单一 `PlatformRuntime` 装配配置、网络、协议、状态和场景端口;所有业务消息只经过一个 Router。子游戏通过版本化 `sdk/contracts` 接入,平台通过 `GameSessionHost` 调用游戏,游戏只能通过受限 `GameHost` 调用平台,不得导入任何平台内部实现。
**Tech Stack:** TypeScript 6、Node.js test runner、tsx、Cocos Creator 3.8+、原生 WebSocket、funplay-cocos MCP。
**Spec:** `docs/superpowers/specs/2026-09-04-platform-vertical-slice-design.md`
## Global Constraints
- 服务器零改动:信封固定为 `{app:"youle",route,rpc,data}`,route/rpc/字段名/类型/时序与 `docs/protocol/` 一致。
- 配置服务和原生 App 零改动:`gameserver → data.urlserver`、`window.settings` 名称和数据格式逐字兼容。
- 配置、RPC route、玩家实体和房间座位各自只有一个权威来源;下游不补默认值。
- 平台不解析 `roomtype` 和 `deskinfo`;`deskinfo` 严格按原工程 `if (deskinfo)` 的真值语义触发 `GameModule.restore`,不进入 PlatformState。
- `sdk/contracts` 不得依赖 platform/net/protocol/ui/core 内部模块、Cocos `cc` 或具体子游戏。
- 具体子游戏不得导入 framework 的 platform/net/protocol/ui/core 内部模块。
- 所有 `.scene`、`.prefab`、`.anim`、`.meta` 修改必须通过 funplay-cocos MCP,禁止文本编辑。
- 每个任务遵循测试先行;提交时只暂存本任务文件,不夹带工作区已有 UI 迁移改动。
- 测试命令均从 `cocoscreator_projects/` 目录执行。
---
## File Structure
本计划完成后的核心结构:
```text
cocoscreator_projects/YouleNexus/assets/framework/
├── config/
│ ├── runtime-config.ts
│ ├── bootstrap.ts
│ ├── remote-config.ts
│ └── sources/native-settings.ts
├── net/
│ ├── net-client.ts
│ └── ...
├── protocol/
│ ├── contracts/
│ │ ├── validation.ts
│ │ ├── login-contract.ts
│ │ ├── join-room-contract.ts
│ │ └── room-event-contracts.ts
│ ├── rpc-route.ts
│ ├── platform-handlers.ts
│ └── router.ts
├── platform/
│ ├── runtime.ts
│ ├── ready-gate.ts
│ ├── session.ts
│ ├── commands.ts
│ ├── game-host-adapter.ts
│ ├── scene-port.ts
│ └── stores/
│ ├── platform-store.ts
│ ├── platform-types.ts
│ ├── selectors.ts
│ └── types.ts # legacy types, removed after migration
├── sdk/
│ ├── contracts/
│ │ ├── api-version.ts
│ │ ├── game-descriptor.ts
│ │ ├── game-host.ts
│ │ ├── game-module.ts
│ │ ├── game-seat-mapper.ts
│ │ ├── platform-events.ts
│ │ ├── snapshots.ts
│ │ └── index.ts
│ ├── runtime/
│ │ ├── game-registry.ts
│ │ └── game-session-host.ts
│ └── testing/game-contract-harness.ts
└── ui/controllers/
├── connection-overlay-controller.ts
├── join-room-controller.ts
└── room-shell-controller.ts
```
具体子游戏后续统一放在 `YouleNexus/assets/games/<game-key>/`,本计划只创建测试游戏,不创建真实玩法。
---
### Task 1: 建立纯净、版本化的子游戏 SDK 契约
**Files:**
- Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/api-version.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-descriptor.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-host.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-module.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-seat-mapper.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/platform-events.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/snapshots.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/index.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/sdk/index.ts`
- Create: `cocoscreator_projects/framework-tests/sdk/contracts.test.ts`
- Create: `cocoscreator_projects/framework-tests/architecture/import-boundaries.test.mjs`
**Interfaces:**
- Produces: `PLATFORM_GAME_API_VERSION = 1`, `GameDescriptor`, `GameModule`, `GameHost`, `GameSeatMapper`, `PlatformToGameEvent`, `GameServerMessage`, `PlatformGameSnapshot`, `GameHostCommand`。
- Consumes: only TypeScript standard types.
- [ ] **Step 1: Write the failing public-contract test**
```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { PLATFORM_GAME_API_VERSION } from '../../YouleNexus/assets/framework/sdk/contracts/index.ts';
test('platform game API version is exact', () => {
assert.equal(PLATFORM_GAME_API_VERSION, 1);
});
```
- [ ] **Step 2: Write the failing import-boundary test**
The test recursively reads `assets/framework/sdk/contracts/**/*.ts` and rejects imports containing `/platform/`, `/net/`, `/protocol/`, `/ui/`, `/core/`, `from 'cc'`, or `from "cc"`.
```js
assert.equal(
forbidden,
undefined,
`${file} imports forbidden dependency: ${forbidden}`,
);
```
- [ ] **Step 3: Run the tests and verify failure**
Run:
```powershell
node --import tsx --test framework-tests/sdk/contracts.test.ts framework-tests/architecture/import-boundaries.test.mjs
```
Expected: FAIL because `sdk/contracts/index.ts` does not exist.
- [ ] **Step 4: Add the exact contract types**
```ts
export const PLATFORM_GAME_API_VERSION = 1 as const;
export type PlatformGameApiVersion = typeof PLATFORM_GAME_API_VERSION;
export interface GameDescriptor {
readonly apiVersion: PlatformGameApiVersion;
readonly gameId: string | number;
readonly route: string;
readonly key: string;
resolveSeatCount(roomtype: readonly unknown[]): number;
create(): GameModule;
}
export interface GameModule {
attach(host: GameHost): void;
handlePlatformEvent(event: PlatformToGameEvent): void;
handleGameMessage(message: GameServerMessage): void;
restore(deskinfo: unknown): void;
dispose(): void;
}
```
Define `GameHost` exactly as the approved spec: bound `seat: GameSeatMapper`, `getSnapshot`, immediate `subscribe`, route-bound `sendGameMessage`, and `execute` with only `room.prepare|room.exit`. `GameSeatMapper` exposes `toView(serverSeat)` and `toServer(viewSeat)`. Define snapshot DTOs locally in `sdk/contracts`; do not import Store types.
- [ ] **Step 5: Add the new contracts without breaking legacy callers**
```ts
export * from './contracts/index.ts';
```
Add this export to the existing `sdk/index.ts` while temporarily retaining its legacy interfaces. Only `sdk/contracts` is the new public boundary; Task 7 migrates callers and removes the legacy definitions in the same green commit.
- [ ] **Step 6: Run targeted tests and typecheck**
```powershell
node --import tsx --test framework-tests/sdk/contracts.test.ts framework-tests/architecture/import-boundaries.test.mjs
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
```
Expected: SDK tests PASS and typecheck reports zero errors.
- [ ] **Step 7: Commit**
```powershell
git add -- YouleNexus/assets/framework/sdk framework-tests/sdk/contracts.test.ts framework-tests/architecture/import-boundaries.test.mjs
git commit -m "feat(sdk): define decoupled game contracts"
```
---
### Task 2: 实现游戏注册表、会话生命周期与接入测试工具
**Files:**
- Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime/game-registry.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime/game-session-host.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts`
- Create: `cocoscreator_projects/framework-tests/sdk/game-registry.test.ts`
- Create: `cocoscreator_projects/framework-tests/sdk/game-session-host.test.ts`
- Create: `cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts`
**Interfaces:**
- Consumes: Task 1 `GameDescriptor`, `GameModule`, `GameHost`, event/message contracts.
- Produces: `GameRegistry.register/getByRoute/getByGameId`, `GameSessionHost.attach/enter/handleGameMessage/restore/dispose`, `assertGameContract`。
- [ ] **Step 1: Write registry failure tests**
```ts
test('registry rejects duplicate route and API mismatch', () => {
const registry = new GameRegistry();
registry.register(descriptor({ key: 'a', gameId: 1, route: 'g1' }));
assert.throws(() => registry.register(descriptor({ key: 'b', gameId: 2, route: 'g1' })), /route.*g1/);
assert.throws(() => registry.register({ ...descriptor({ key: 'c', gameId: 3, route: 'g3' }), apiVersion: 2 as never }), /apiVersion/);
});
```
Also reject a missing `resolveSeatCount` contract. The conformance harness calls it with fixture `roomtype` values and rejects zero, negative, fractional, or non-number results.
- [ ] **Step 2: Write lifecycle and generation tests**
Cover `detached → attached → entered → disposed`, illegal double attach, event before enter, dispose exactly once, and calls through an old Host after a new generation begins.
```ts
session.attach(host);
session.enter({ type: 'room-entered', roomtype: rawRoomtype });
session.dispose();
assert.throws(() => oldHost.sendGameMessage('play', {}), /disposed/);
```
- [ ] **Step 3: Run tests and verify failure**
```powershell
node --import tsx --test framework-tests/sdk/game-registry.test.ts framework-tests/sdk/game-session-host.test.ts
```
Expected: FAIL because runtime classes do not exist.
- [ ] **Step 4: Implement strict registry**
Use three maps keyed by `key`, normalized `String(gameId)`, and `route`. Reject empty values, duplicates, API mismatch, and factories that return the same object twice during conformance testing.
```ts
register(value: GameDescriptor): void {
assertDescriptor(value);
if (this.byRoute.has(value.route)) throw new Error(`duplicate game route: ${value.route}`);
this.byRoute.set(value.route, value);
this.byGameId.set(String(value.gameId), value);
this.byKey.set(value.key, value);
}
```
- [ ] **Step 5: Implement serialized session dispatch**
`GameSessionHost` owns one module and one Host. Every public method checks state; `fail(error)` records the first failure, disposes once, and rejects later dispatch. Store commit ordering remains the platform adapter's responsibility.
- [ ] **Step 6: Implement the reusable conformance harness**
`assertGameContract(descriptor, makeHost)` runs descriptor validation, seat-count resolution, fresh factory, lifecycle, route-bound send, snapshot immutability, subscription cleanup, deskinfo identity, 2/4/10-seat mapper round trips, and two-session isolation.
- [ ] **Step 7: Run tests and typecheck**
```powershell
node --import tsx --test framework-tests/sdk/game-registry.test.ts framework-tests/sdk/game-session-host.test.ts framework-tests/sdk/game-contract-harness.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
```
Expected: PASS.
- [ ] **Step 8: Commit**
```powershell
git add -- YouleNexus/assets/framework/sdk/runtime YouleNexus/assets/framework/sdk/testing framework-tests/sdk
git commit -m "feat(sdk): add game registry and isolated sessions"
```
---
### Task 3: 修正运行配置、原生身份与 urlserver 单一来源
**Files:**
- Create: `cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/config/sources/native-settings.ts`
- Modify: `cocoscreator_projects/framework-tests/config/bootstrap.test.ts`
- Modify: `cocoscreator_projects/framework-tests/config/remote-config.test.ts`
- Modify: `cocoscreator_projects/framework-tests/config/profiles.test.ts`
- Modify: `cocoscreator_projects/framework-tests/config/sources/native-settings.test.ts`
- Create: `cocoscreator_projects/framework-tests/config/runtime-config.test.ts`
**Interfaces:**
- Produces: `RuntimeConfig`, `resolveRuntimeConfig(options)`, `decodeNativeGameConfig(value)`, `parseUrlServers(config)`.
- Consumes: existing runtime mode and identity source abstractions.
- [ ] **Step 1: Add failing urlserver tests**
Before changing configuration or identity code, read `.agents/skills/native-bridge-contract/SKILL.md` completely and follow its referenced source map for `gameserver`, `gameconfig`, `urlserver`, and `window.settings`. Record the exact legacy source lines beside each fixture assertion; do not infer bridge names or transforms from current YouleNexus code.
```ts
test('remote config uses data.urlserver as the only WS source', () => {
assert.deepEqual(parseUrlServers({ data: { urlserver: ['10.0.0.1:3088', 'wss://b'] } }), [
'ws://10.0.0.1:3088',
'wss://b',
]);
assert.throws(() => parseUrlServers({ data: { player_server_tcp: 'wrong:1' } }), /data\.urlserver/);
});
```
- [ ] **Step 2: Add failing native branch tests**
```ts
assert.equal(decodeNativeGameConfig('host-path#8080-config'), 'http://host/path:8080/config.txt');
assert.throws(() => nativeSettingsSource(nonUAgentHostWithoutSettings)(), /getothername/);
assert.equal(nativeSettingsSource(uAgent3Host)().agentid, 'legacy-agent');
```
The uAgent_3 case must be selected by an explicit environment discriminator, not by catch-all exception handling.
- [ ] **Step 3: Run config tests and verify failure**
```powershell
node --import tsx --test framework-tests/config/*.test.ts framework-tests/config/sources/native-settings.test.ts
```
Expected: FAIL on `urlserver`, gameconfig decoding, and current fallback behavior.
- [ ] **Step 4: Implement strict remote config parsing**
```ts
export interface RemoteConfig {
data: { urlserver: string | readonly string[]; [key: string]: unknown };
}
export function parseUrlServers(input: unknown): string[] {
const value = requireRecord(input, '$').data;
const raw = requireRecord(value, '$.data').urlserver;
const entries = typeof raw === 'string' ? [raw] : requireStringArray(raw, '$.data.urlserver');
if (entries.length === 0) throw new ConfigParseError('$.data.urlserver must not be empty');
return entries.map(toWebSocketUrl);
}
```
Do not read `player_server_tcp`, `visitor_server_tcp`, or `game_server_tcp` for the first WebSocket path.
- [ ] **Step 5: Correct profiles and bootstrap**
Set the release default to the exact original source value from `Game_Surface_3/js/01_SubGame/00_SubGame_Config.js:11`. Keep explicit local/staging profiles, but remove `REPLACE_ME` data from production paths and prevent unknown profile names from silently selecting prod; unknown names throw in debug.
- [ ] **Step 6: Build immutable RuntimeConfig**
Validate channel identity and login identity before constructing NetClient. Return a frozen object and frozen server list. No controller may call `resolveBootstrap` directly after this task.
- [ ] **Step 7: Run tests and typecheck**
```powershell
node --import tsx --test framework-tests/config/*.test.ts framework-tests/config/sources/native-settings.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
```
Expected: PASS.
- [ ] **Step 8: Commit**
```powershell
git add -- YouleNexus/assets/framework/config framework-tests/config
git commit -m "fix(config): align runtime configuration with legacy contract"
```
---
### Task 4: 建立首批 RPC 的唯一 route 表、解析器与黄金样本
**Files:**
- Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/rpc-route.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/validation.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/login-contract.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/join-room-contract.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/room-event-contracts.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/index.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/protocol/routes.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/protocol/typed-send.ts`
- Replace: `cocoscreator_projects/YouleNexus/assets/framework/core/types/login.ts`
- Create: `cocoscreator_projects/framework-tests/fixtures/protocol/player-login-success.json`
- Create: `cocoscreator_projects/framework-tests/fixtures/protocol/player-login-room.json`
- Create: `cocoscreator_projects/framework-tests/fixtures/protocol/self-join-room.json`
- Create: `cocoscreator_projects/framework-tests/fixtures/protocol/room-events.json`
- Create: `cocoscreator_projects/framework-tests/protocol/contracts.test.ts`
- Modify: `cocoscreator_projects/framework-tests/protocol/routes.test.ts`
- Modify: `cocoscreator_projects/framework-tests/protocol/net-typed-send.test.ts`
- Modify: `cocoscreator_projects/framework-tests/protocol/login.test.ts`
**Interfaces:**
- Produces: exhaustive `RPC_ROUTE satisfies Record<RpcName, RouteName>`, request/response parsers for login, join, prepare, join/exit/online/offline, switch-server, kick.
- Consumes: `docs/protocol/01-05` and the contract inventory; no Store imports.
- [ ] **Step 1: Add failing exhaustive route test**
Before writing or changing a network contract, re-read `docs/protocol/01-网络通信与生命周期.md` through `05-游戏内协议与桥接.md` sections covering that RPC and verify every fixture field against the cited source. The contract inventory is an index, not a substitute for the protocol chapters.
```ts
for (const rpc of Object.values(Rpc)) {
assert.ok(RPC_ROUTE[rpc], `missing route for ${rpc}`);
}
assert.equal(RPC_ROUTE.self_join_room, Route.agent);
assert.equal(RPC_ROUTE.player_prepare, Route.room);
```
- [ ] **Step 2: Add golden fixture tests**
Copy the verified `player_login` response from `docs/protocol/04-数据结构.md`; give other fixtures only fields justified by protocol tables. Tests assert exact encoded envelope and parser output. Do not add values for undocumented fields.
```ts
const deskinfo = roomFixture.data.deskinfo;
const parsed = parseLoginResponse(roomFixture.data);
assert.equal(parsed.reconnect.present, true);
assert.equal(parsed.reconnect.value, deskinfo);
```
- [ ] **Step 3: Add failing negative tests**
Cover numeric `version`, missing `playerid`, missing room `seat`, invalid players array, invalid `urlserver`, and the critical cases where `isbattle` is set but `deskinfo` is absent or falsey.
```ts
const { deskinfo: _ignored, ...withoutDeskinfo } = room;
assert.equal(parseLoginResponse({ ...withoutDeskinfo, isbattle: 1 }).reconnect.present, false);
assert.equal(parseLoginResponse({ ...withoutDeskinfo, isbattle: 1, deskinfo: null }).reconnect.present, false);
assert.equal(parseLoginResponse({ ...withoutDeskinfo, isbattle: 1, deskinfo: { round: 2 } }).reconnect.present, true);
```
Use `Boolean(data.deskinfo)` for this trigger because the original source is exactly `if (_msg.data.deskinfo)`. Keep the truthy value opaque and preserve its identity; do not validate or interpret the snapshot structure in platform code.
- [ ] **Step 4: Run tests and verify failure**
```powershell
node --import tsx --test framework-tests/protocol/contracts.test.ts framework-tests/protocol/routes.test.ts framework-tests/protocol/net-typed-send.test.ts framework-tests/protocol/login.test.ts
```
Expected: FAIL because the production route table and strict parsers do not exist.
- [ ] **Step 5: Implement reusable validation helpers**
Provide `requireRecord`, `requireString`, `requireNumber`, `requireInteger`, `requireArray`, `optionalField`, and `hasOwn`. Every thrown message includes the JSON path.
- [ ] **Step 6: Implement exact parsers and request builders**
Request builders return fresh objects containing only protocol-defined fields. Response parsers preserve optional protocol values and keep `roomtype/deskinfo` opaque; the reconnect projection uses the original client's truthiness semantics.
- [ ] **Step 7: Replace caller-supplied route maps**
```ts
export function typedSend<T>(rpc: RpcName, data: T): TypedEnvelope<T> {
return { route: RPC_ROUTE[rpc], rpc, data };
}
```
Delete the `map` parameter so controllers and games cannot provide a second route source.
- [ ] **Step 8: Run protocol tests and typecheck**
```powershell
node --import tsx --test framework-tests/protocol/*.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
```
Expected: PASS.
- [ ] **Step 9: Commit**
```powershell
git add -- YouleNexus/assets/framework/protocol YouleNexus/assets/framework/core/types/login.ts framework-tests/protocol framework-tests/fixtures/protocol
git commit -m "feat(protocol): add strict vertical-slice contracts"
```
---
### Task 5: 用单一 PlatformStore 替换玩家与房间镜像状态
**Files:**
- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/selectors.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-types.ts`
- Create: `cocoscreator_projects/framework-tests/platform/platform-store.test.ts`
- Create: `cocoscreator_projects/framework-tests/platform/selectors.test.ts`
**Interfaces:**
- Produces: `PlatformStore`, `PlatformState`, `PlayerDirectoryState`, `RoomState`, atomic actions and read-only selectors.
- Consumes: Task 4 parsed DTOs; does not consume raw unknown server payloads.
- [ ] **Step 1: Write failing atomic-state tests**
```ts
const store = new PlatformStore();
let notifications = 0;
store.state.subscribe(() => notifications++);
store.applyLoginSuccess(parsedLogin);
assert.equal(notifications, 1);
assert.equal(store.state.value.players.selfPlayerId, parsedLogin.player.playerid);
```
Add a room fixture whose self player also appears in `players`; assert only one entity object exists and the room stores only player IDs.
- [ ] **Step 2: Write failing push/update tests**
Cover other join, exit, ready, offline, online, full reconnect replacement, and failure operations that leave the prior state reference unchanged.
```ts
const before = store.state.value;
assert.throws(() => store.playerReady({ seat: 99 }), /seat/);
assert.equal(store.state.value, before);
```
- [ ] **Step 3: Run tests and verify failure**
```powershell
node --import tsx --test framework-tests/platform/platform-store.test.ts framework-tests/platform/selectors.test.ts
```
Expected: FAIL because PlatformStore does not exist.
- [ ] **Step 4: Define internal state without sentinel identities**
```ts
export interface PlayerDirectoryState {
readonly selfPlayerId: number | null;
readonly entities: Readonly<Record<number, PlayerState>>;
}
export interface RoomState {
readonly inRoom: boolean;
readonly roomcode: string | null;
readonly selfSeat: number | null;
readonly seatPlayerIds: readonly (number | null)[];
readonly roomtype: readonly unknown[] | null;
readonly stage: number;
readonly needprepare: number;
readonly infinite: number;
}
```
There is no fake `playerid:0`, `seat:-1`, or empty server-derived player object. Keep the existing `stores/types.ts` unchanged until all legacy stores are removed in Task 12, so this task remains independently green.
- [ ] **Step 5: Implement one-signal atomic actions**
`PlatformStore` owns one `signal<PlatformState>`. Every action validates preconditions, builds all changed subtrees, then assigns `this.s.value` exactly once. It never mutates `this.s.value` or nested arrays in place. On room entry/recovery, recursively freeze the received JSON `roomtype` tree without interpreting positions, then keep that one canonical reference; tests assert nested mutation throws and no normalized copy is introduced.
- [ ] **Step 6: Implement read-only selectors**
Provide `selectSelfPlayer`, `selectSeat`, `selectRoomPlayers`, and `selectGameSnapshot`. Snapshot creation returns a frozen DTO and maps protocol `onstate/isprepare` to public booleans without changing the stored protocol values.
- [ ] **Step 7: Run tests and typecheck**
```powershell
node --import tsx --test framework-tests/platform/platform-store.test.ts framework-tests/platform/selectors.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
```
Expected: PASS while legacy stores remain temporarily available to old callers.
- [ ] **Step 8: Commit**
```powershell
git add -- YouleNexus/assets/framework/platform/stores framework-tests/platform/platform-store.test.ts framework-tests/platform/selectors.test.ts
git commit -m "feat(platform): add atomic platform state store"
```
---
### Task 6: 收紧 NetClient 前置条件和控制包行为
**Files:**
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/net/net-client.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/net/transport.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/net/cocos-transport.ts`
- Modify: `cocoscreator_projects/framework-tests/net/net-client.test.ts`
- Modify: `cocoscreator_projects/framework-tests/helpers/fake-transport.ts`
**Interfaces:**
- Produces: fail-fast `NetClient.start/send/stop`, one message stream, exact login gate, switch/kick events.
- Consumes: Task 3 `RuntimeConfig`, Task 4 request builders/envelope codec.
- [ ] **Step 1: Add failing precondition tests**
Before changing NetClient, re-read the lifecycle, control-packet, heartbeat, timeout, and reconnect sections in `docs/protocol/01-网络通信与生命周期.md` and `02-客户端发包.md`; record any discrepancy between current code and the documented original behavior in the test name.
```ts
assert.throws(() => clientWithoutIdentity.start(), /login identity/);
assert.throws(() => client.send('room', 'player_prepare', {}), /not connected/);
```
Also test transport `send` after close, duplicate `onerror/onclose`, and stop idempotency.
- [ ] **Step 2: Add control packet negative tests**
Missing `roomserver/agentserver` must emit a contract error or throw through the test boundary, not be ignored. `kick_server` must cancel login/reconnect/watchdog timers and close once.
- [ ] **Step 3: Run tests and verify failure**
```powershell
node --import tsx --test framework-tests/net/net-client.test.ts framework-tests/net/transport.test.ts
```
Expected: FAIL on silent `sendLogin`, optional transport send, and malformed switch packets.
- [ ] **Step 4: Implement explicit connection states**
Use an internal union `idle|connecting|open|stopped`. `start()` requires identity and at least one validated server. `send()` requires `open`. `stop()` transitions once and clears all timers before closing.
- [ ] **Step 5: Keep the verified legacy timing**
Retain four-second login guard, ten-second reconnect delay, receive timeout, TCP generation filtering, login-period packet gate, and server rotation. Update tests rather than changing these values.
- [ ] **Step 6: Run all net tests and typecheck**
```powershell
node --import tsx --test framework-tests/net/*.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
```
Expected: PASS.
- [ ] **Step 7: Commit**
```powershell
git add -- YouleNexus/assets/framework/net framework-tests/net framework-tests/helpers/fake-transport.ts
git commit -m "fix(net): fail fast on invalid client lifecycle"
```
---
### Task 7: 统一平台 Handler、Router 与 Session
**Files:**
- Create: `cocoscreator_projects/YouleNexus/assets/framework/protocol/platform-handlers.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/protocol/router.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/platform/session.ts`
- Modify: `cocoscreator_projects/YouleNexus/assets/framework/sdk/index.ts`
- Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/room-rpc-bus.ts`
- Delete: `cocoscreator_projects/YouleNexus/assets/framework/protocol/room-handlers.ts`
- Delete: `cocoscreator_projects/YouleNexus/assets/framework/protocol/active-game.ts`
- Modify: `cocoscreator_projects/framework-tests/protocol/router.test.ts`
- Create: `cocoscreator_projects/framework-tests/protocol/platform-handlers.test.ts`
- Replace: `cocoscreator_projects/framework-tests/platform/session.test.ts`
- Delete: `cocoscreator_projects/framework-tests/platform/room-rpc-bus.test.ts`
- Delete: `cocoscreator_projects/framework-tests/protocol/room-handlers.test.ts`
- Delete: `cocoscreator_projects/framework-tests/protocol/active-game.test.ts`
**Interfaces:**
- Consumes: strict parsers, `PlatformStore`, `GameSessionHost`, and an injected `GameHostFactory` port.
- Produces: one `Router.dispatch`, one `PlatformSession.handleLogin`, `GameHostFactory.create({ descriptor, selfSeat, seatCount })`, and the platform handler table for the first slice.
- [ ] **Step 1: Rewrite router tests around one dispatch owner**
```ts
router.dispatch({ route: 'room', rpc: 'player_prepare', data: { seat: 2 } });
assert.deepEqual(platformCalls, ['player_prepare']);
router.dispatch({ route: activeDescriptor.route, rpc: 'deal', data: { card: 1 } });
assert.deepEqual(gameMessages, [{ rpc: 'deal', data: { card: 1 } }]);
```
Assert an unknown platform RPC throws `UnsupportedPlatformRpcError`; a route different from the active session descriptor throws and never reaches either side.
- [ ] **Step 2: Write handler ordering tests**
For each room push, subscribe to PlatformStore and log GameModule events. Assert store notification occurs before the event and that the snapshot read inside the event contains the committed state.
- [ ] **Step 3: Write login/reconnect tests**
```ts
session.handleLogin(roomLogin);
assert.equal(store.state.value.room.inRoom, true);
assert.equal(fakeGame.restoreCalls[0], roomLogin.deskinfo);
assert.equal('deskinfo' in store.state.value.room, false);
```
Also assert `{ isbattle:1 }` with absent or `null` `deskinfo` does not call restore, while a truthy opaque object calls it exactly once.
- [ ] **Step 4: Run tests and verify failure**
```powershell
node --import tsx --test framework-tests/protocol/router.test.ts framework-tests/protocol/platform-handlers.test.ts framework-tests/platform/session.test.ts
```
Expected: FAIL against the current parallel Router/RoomRPCBus implementation.
- [ ] **Step 5: Implement PlatformHandlers**
Handlers accept parsed data only. Implement `self_join_room`, `other_join_room`, `self_exit_room`, `other_exit_room`, `player_prepare`, `other_offline`, and `other_online`. Each calls one PlatformStore action, then one GameSession event where defined.
- [ ] **Step 6: Replace ActiveGame with GameSessionHost in Router**
```ts
if (isPlatformRoute(message.route)) {
return this.platform.dispatch(message);
}
return this.gameSessions.dispatchGameMessage(message.route, {
rpc: message.rpc,
data: message.data,
});
```
No other module subscribes separately to `NetClient.message`.
- [ ] **Step 7: Make PlatformSession a parsed orchestration boundary**
`handleLogin(unknown)` parses once, rejects failure without partial Store writes, resolves the descriptor with `GameRegistry.getByGameId(RuntimeConfig.identity.gameid)`, obtains the seat count only through `descriptor.resolveSeatCount(roomtype)`, asks the injected `GameHostFactory` for a route/seat-bound Host, opens or replaces the correct game session, atomically replaces recovery state, then calls `restore` if and only if `Boolean(deskinfo)`. Task 7 tests use a strict fake factory; the production adapter arrives in Task 8, so there is no forward implementation dependency.
- [ ] **Step 8: Remove obsolete buses/stores from this path**
Delete the parallel bus/handler/active-game files and all imports. Replace `sdk/index.ts` with a pure re-export of `sdk/contracts/index.ts`. Do not leave forwarding shims that could become a second runtime path.
- [ ] **Step 9: Run platform/protocol tests and typecheck**
```powershell
node --import tsx --test framework-tests/protocol/*.test.ts framework-tests/platform/*.test.ts framework-tests/sdk/*.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
```
Expected: PASS.
- [ ] **Step 10: Commit**
```powershell
git add -A -- YouleNexus/assets/framework/protocol YouleNexus/assets/framework/platform YouleNexus/assets/framework/sdk/index.ts framework-tests/protocol framework-tests/platform
git commit -m "refactor(platform): unify message routing and session state"
```
---
### Task 8: 实现受限 GameHost 适配器与平台命令
**Files:**
- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/game-host-adapter.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/commands.ts`
- Create: `cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts`
- Create: `cocoscreator_projects/framework-tests/platform/commands.test.ts`
**Interfaces:**
- Consumes: GameHost contract, Task 7 `GameHostFactory` port, descriptor route, PlatformStore selectors, typed send, NetClient.
- Produces: the production `createGameHostAdapter(options)` implementation of `GameHostFactory` and `PlatformCommands.prepare/exitRoom`.
- [ ] **Step 1: Write failing route-binding and command tests**
```ts
host.sendGameMessage('play-card', { card: 7 });
assert.deepEqual(sent[0], { route: 'mock-game', rpc: 'play-card', data: { card: 7 } });
host.execute({ type: 'room.prepare' });
assert.equal(sent[1].rpc, 'player_prepare');
```
Assert no Host method accepts a route argument. Assert `room.exit` rejects when `room.stage !== 0`.
For seat mapping, cover 2-, 4-, and 10-seat sessions. Assert `host.seat.toView(selfSeat) === 0`, every server seat round-trips through `toView`/`toServer`, and an out-of-range or non-integer seat throws with the offending value.
- [ ] **Step 2: Write subscription/disposal tests**
The first callback is immediate. Reassigning internal state with no public snapshot change does not notify. Disposing removes all listeners and every subsequent Host method throws `GameSessionDisposedError`.
- [ ] **Step 3: Run tests and verify failure**
```powershell
node --import tsx --test framework-tests/platform/game-host-adapter.test.ts framework-tests/platform/commands.test.ts
```
Expected: FAIL because adapter and commands do not exist.
- [ ] **Step 4: Implement commands through typedSend**
```ts
prepare(): void {
this.net.sendTyped(typedSend(Rpc.player_prepare, {}));
}
exitRoom(): void {
if (this.store.state.value.room.stage !== 0) {
throw new UnsupportedGameHostCommandError('room.exit requires stage=0');
}
this.net.sendTyped(typedSend(Rpc.self_exit_room, {}));
}
```
If NetClient does not expose `sendTyped`, add a narrow method accepting `TypedEnvelope`; do not expose raw transport.
- [ ] **Step 5: Implement snapshot and seat-mapper isolation**
Build DTOs with selectors, freeze every newly created public object/array, and expose the Store's already-recursively-frozen canonical `roomtype` reference without interpreting its contents. Construct `GameSeatMapper` once from the current session's validated `selfSeat` and descriptor-derived `seatCount`; the adapter must not import the legacy `core/seat` implementation or read changing Store values inside the formulas. Do not expose `PlatformStore.state`.
- [ ] **Step 6: Run tests, conformance harness, and typecheck**
```powershell
node --import tsx --test framework-tests/platform/game-host-adapter.test.ts framework-tests/platform/commands.test.ts framework-tests/sdk/game-contract-harness.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
```
Expected: PASS.
- [ ] **Step 7: Commit**
```powershell
git add -- YouleNexus/assets/framework/platform/game-host-adapter.ts YouleNexus/assets/framework/platform/commands.ts YouleNexus/assets/framework/net/net-client.ts framework-tests/platform framework-tests/sdk
git commit -m "feat(platform): expose restricted game host adapter"
```
---
### Task 9: 建立 PlatformRuntime 和启动四门闩
**Files:**
- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/ready-gate.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/scene-port.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/platform/runtime.ts`
- Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/startup.ts`
- Create: `cocoscreator_projects/framework-tests/platform/ready-gate.test.ts`
- Create: `cocoscreator_projects/framework-tests/platform/runtime.test.ts`
- Replace: `cocoscreator_projects/framework-tests/platform/startup.test.ts`
**Interfaces:**
- Consumes: RuntimeConfig resolver, NetClient factory, Router, PlatformSession, PlatformStore, GameRegistry, ScenePort.
- Produces: single `PlatformRuntime.start/stop/joinRoom/prepare/exitRoom`, read-only state, four-gate readiness.
- [ ] **Step 1: Write all 16 readiness permutation tests**
For each subset of the four gates, assert ready is false unless all four are true. Assert each gate can be marked once and duplicate marks are harmless.
```ts
gate.mark('resources');
gate.mark('config');
gate.mark('socket');
assert.equal(gate.ready, false);
gate.mark('minimum-display');
assert.equal(gate.ready, true);
```
- [ ] **Step 2: Write runtime construction-order tests**
Assert config resolves before NetClient creation, identity is set before `start`, the application composition root registers descriptors explicitly, `RuntimeConfig.identity.gameid` resolves exactly one descriptor before a room session can open, Router subscribes before the first post-login business message, and stop disposes game session before stores/network.
- [ ] **Step 3: Write scene decision tests**
No room login calls `scene.showLobby()`. Room login calls `scene.showRoom()` before game restore. Kick calls `scene.showKicked()`. Reconnecting/slow calls `scene.showReconnect()` without clearing room state.
- [ ] **Step 4: Run tests and verify failure**
```powershell
node --import tsx --test framework-tests/platform/ready-gate.test.ts framework-tests/platform/runtime.test.ts
```
Expected: FAIL because runtime and gate do not exist.
- [ ] **Step 5: Implement Cocos-free runtime**
The runtime constructor accepts factories/ports; it never imports `cc`. `start()` launches resource/minimum-display promises plus config resolution, then creates and starts NetClient. All NetClient events are wired in one private method and unwired in `stop()`.
- [ ] **Step 6: Define narrow ScenePort**
```ts
export interface ScenePort {
showLoading(): void;
showLogin(): void;
showLobby(): void;
showRoom(): void;
showReconnect(): void;
showKicked(data: unknown): void;
showFatal(error: Error): void;
}
```
The port describes intent, not Cocos scene or node names.
- [ ] **Step 7: Delete StartupOrchestrator and adapt tests**
There must be only one startup owner. Remove all `RoomRPCBus` factory concepts and the old “login 后构造 NetClient” flow.
- [ ] **Step 8: Run tests and typecheck**
```powershell
node --import tsx --test framework-tests/platform/*.test.ts framework-tests/integration/login-flow.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
```
Expected: PASS after the integration test is adapted to PlatformRuntime.
- [ ] **Step 9: Commit**
```powershell
git add -A -- YouleNexus/assets/framework/platform framework-tests/platform framework-tests/integration/login-flow.test.ts
git commit -m "feat(platform): add single runtime and readiness gate"
```
---
### Task 10: 编写不依赖 Cocos 的首批 Layer 控制器
**Files:**
- Create: `cocoscreator_projects/YouleNexus/assets/framework/ui/controllers/connection-overlay-controller.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/ui/controllers/join-room-controller.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/framework/ui/controllers/room-shell-controller.ts`
- Create: `cocoscreator_projects/framework-tests/ui/connection-overlay-controller.test.ts`
- Create: `cocoscreator_projects/framework-tests/ui/join-room-controller.test.ts`
- Create: `cocoscreator_projects/framework-tests/ui/room-shell-controller.test.ts`
**Interfaces:**
- Consumes: narrow RuntimeCommands and read-only selector/subscription ports.
- Produces: presentation models and user-intent methods; no Cocos imports.
- [ ] **Step 1: Write JoinRoom input-state tests**
```ts
const controller = new JoinRoomController(commands, view);
for (const digit of [1, 2, 3, 4, 5, 6]) controller.inputDigit(digit);
assert.equal(view.last.roomcode, '123456');
controller.confirm();
assert.deepEqual(commands.calls, [{ type: 'join-room', roomcode: '123456' }]);
```
Cover 0-9 only, six-digit maximum, delete, clear, confirm before six digits, double confirm while pending, success close, and failure retaining input.
- [ ] **Step 2: Write connection overlay tests**
Map `reconnecting/slow` to Layer 615, pending commands to Layer 614, kicked to Layer 616, and normal states to all hidden. Kicked has precedence over reconnect/loading.
- [ ] **Step 3: Write room shell tests**
Feed snapshots and assert roomcode, seats, online/ready state, prepare visibility, and exit command. The controller stores no PlayerState objects after render.
- [ ] **Step 4: Run tests and verify failure**
```powershell
node --import tsx --test framework-tests/ui/connection-overlay-controller.test.ts framework-tests/ui/join-room-controller.test.ts framework-tests/ui/room-shell-controller.test.ts
```
Expected: FAIL because controllers do not exist.
- [ ] **Step 5: Implement view-port-driven controllers**
Each controller receives a view interface and commands. It owns only UI transient state such as roomcode digits and pending flag. All server state comes from snapshots/selectors.
- [ ] **Step 6: Run UI tests and typecheck**
```powershell
node --import tsx --test framework-tests/ui/connection-overlay-controller.test.ts framework-tests/ui/join-room-controller.test.ts framework-tests/ui/room-shell-controller.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
```
Expected: PASS.
- [ ] **Step 7: Commit**
```powershell
git add -- YouleNexus/assets/framework/ui/controllers framework-tests/ui
git commit -m "feat(ui): add vertical-slice presentation controllers"
```
---
### Task 11: 添加 Cocos 适配器并通过 MCP 挂接首批 prefab
**Files:**
- Create: `cocoscreator_projects/YouleNexus/assets/scripts/platform/PlatformRootComponent.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/scripts/platform/CocosScenePort.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/scripts/platform/JoinRoomLayerComponent.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/scripts/platform/RoomShellComponent.ts`
- Create: `cocoscreator_projects/YouleNexus/assets/scripts/platform/ConnectionOverlayComponent.ts`
- Replace: `cocoscreator_projects/YouleNexus/assets/scripts/LaunchFlow.ts`
- Replace: `cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts`
- Modify via funplay-cocos MCP: `cocoscreator_projects/YouleNexus/assets/scenes/Login.scene`
- Modify via funplay-cocos MCP: Layer 1、4、15、50、403、411、202、416、614、615、616 prefab resources
- Create: `cocoscreator_projects/framework-tests/cocos-adapters/no-demo-fallback.test.mjs`
**Interfaces:**
- Consumes: Task 9 PlatformRuntime, Task 10 controllers.
- Produces: thin Cocos Components and ScenePort adapter; no duplicated business logic.
- [ ] **Step 1: Write the static failure test first**
Scan production Cocos adapters for forbidden literals/patterns:
```js
const forbidden = [
'mock_openid',
"search: '?profile=local'",
'fallback to ws://127.0.0.1:3088',
'setIdentity({ agentid:',
];
```
Expected current `LoginFlow.ts` to fail.
- [ ] **Step 2: Run the static test and verify failure**
```powershell
node --test framework-tests/cocos-adapters/no-demo-fallback.test.mjs
```
Expected: FAIL on current demo code.
- [ ] **Step 3: Implement thin Cocos adapters**
`PlatformRootComponent` creates exactly one runtime and persists it across scene changes. `CocosScenePort` maps semantic scene intents to root Layer activation. Other components only translate Button/Label/Node events to/from the pure controllers.
- [ ] **Step 4: Remove demo networking from LaunchFlow/LoginFlow**
Delete local `WSTransport`, mock identity, bootstrap catch fallback, direct RoomRPCBus construction, and fixed one-second scene jump. Use `CocosTransport` and the root runtime.
- [ ] **Step 5: Use the cocos-mcp skill before editor changes**
Read `.agents/skills/cocos-mcp/SKILL.md`, verify the editor service is healthy, and use the injected `mcp__funplay_cocos__*` tools. If unavailable, stop this task and ask the user to start the editor service; never edit serialized files manually.
Before the first editor mutation, record `git status --short` for every listed scene/prefab and its `.meta`. They must already be tracked or separately checkpointed as the completed UI-migration baseline. If any is still an untracked pre-existing UI artifact, stop Task 11 and ask the user to checkpoint that UI work first; an untracked serialized file cannot separate earlier UI content from this task's bindings.
- [ ] **Step 6: Inspect exact prefab node paths via MCP**
For each Layer, record the queried node/component path in the task log before binding. Do not infer paths from prefab text. Verify which existing Sprite nodes are intended as buttons and which Labels display room/player state.
- [ ] **Step 7: Attach components and serialized references via MCP**
Attach only the five adapter components, wire Buttons/Labels/Nodes through editor properties, and keep business state out of prefab components. Save each prefab/scene through the editor.
- [ ] **Step 8: Preview the vertical UI states**
Using injected fake runtime states, capture or visually inspect: loading, login, lobby, six-digit join input, room with two seats, ready, reconnect, and kicked. Verify inactive overlays do not block input.
- [ ] **Step 9: Run static and framework checks**
```powershell
node --test framework-tests/cocos-adapters/no-demo-fallback.test.mjs
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
```
Expected: PASS. Also run the Cocos editor's script compilation check through MCP and require zero component/UUID errors.
- [ ] **Step 10: Commit**
```powershell
git add -- YouleNexus/assets/scripts/platform YouleNexus/assets/scripts/LaunchFlow.ts YouleNexus/assets/scripts/LoginFlow.ts framework-tests/cocos-adapters
git add -- YouleNexus/assets/scenes/Login.scene YouleNexus/assets/scenes/Login.scene.meta
git add -- YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab.meta
git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer15_JoinRoom.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer15_JoinRoom.prefab.meta
git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer50_MainScene.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer50_MainScene.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer403.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer403.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer411.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer411.prefab.meta
git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer202_PlayerHeadScore.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer202_PlayerHeadScore.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer416_PlayerInfo.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer416_PlayerInfo.prefab.meta
git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer615_Reconnect.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer615_Reconnect.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer616_Kick.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer616_Kick.prefab.meta
git commit -m "feat(cocos): connect platform runtime to migrated layers"
```
Before committing, inspect `git diff --cached --name-only` and unstage any prefab unrelated to the listed first-batch Layers.
---
### Task 12: 建立完整纵向回放测试并删除旧状态路径
**Files:**
- Create: `cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts`
- Replace: `cocoscreator_projects/framework-tests/integration/login-flow.test.ts`
- Modify: `cocoscreator_projects/framework-tests/architecture/import-boundaries.test.mjs`
- Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/app-store.ts`
- Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/player-store.ts`
- Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/room-store.ts`
- Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/stores/types.ts`
- Delete: `cocoscreator_projects/YouleNexus/assets/framework/platform/readonly.ts`
- Modify: `cocoscreator_projects/scripts/run-framework-tests.mjs`
- Delete or rewrite affected legacy tests under: `cocoscreator_projects/framework-tests/platform/`
**Interfaces:**
- Consumes: completed runtime and fake transport/clock/scene/game adapters.
- Produces: one deterministic end-to-end regression test and final dependency guard.
- [ ] **Step 1: Write the full replay test**
Replay this exact sequence with fixtures:
```text
runtime.start
→ config resolved
→ WS open / player_login sent
→ login success without room / lobby shown
→ joinRoom('123456') / self_join_room sent
→ self_join_room success / room shown / game session entered
→ other_join_room
→ player_prepare
→ socket close / reconnect overlay
→ reconnect open / player_login resent
→ login success with room + deskinfo / room state replaced / restore called once
```
At every arrow assert outbound envelope, PlatformState, ScenePort call, GameModule event, and notification count.
- [ ] **Step 2: Add two-game isolation to the architecture guard**
Register two descriptors, enter/leave both in sequence, and assert imports under `assets/games/<key>` cannot reach framework internals. Use temporary in-memory source strings in the test; do not create fake production games.
- [ ] **Step 3: Run the test and verify any remaining legacy path fails**
```powershell
node --import tsx --test framework-tests/integration/platform-vertical-slice.test.ts framework-tests/architecture/import-boundaries.test.mjs
```
Expected before cleanup: FAIL if any runtime still constructs old AppStore/PlayerStore/RoomStore or RoomRPCBus.
- [ ] **Step 4: Delete legacy Store facades and update remaining tests**
All production state callers must use PlatformStore/selectors. Do not leave deprecated aliases, because they would recreate a second source of truth.
Also add `--test-concurrency=1` to `run-framework-tests.mjs` so the complete suite is deterministic in the managed Windows environment:
```js
const r = spawnSync(
process.execPath,
['--import', 'tsx', '--test', '--test-concurrency=1', ...files],
{ stdio: 'inherit' },
);
```
- [ ] **Step 5: Run the complete framework suite**
```powershell
npm run typecheck:framework
npm run test:framework
```
Expected: every framework test PASS with zero skipped tests and zero TypeScript errors.
- [ ] **Step 6: Audit forbidden patterns**
```powershell
rg -n "RoomRPCBus|new AppStore|new PlayerStore|new RoomStore|mock_openid|fallback to ws://|transport\?\.send|if \(!this\.identity\) return|hasBattle.*isbattle" YouleNexus/assets
```
Expected: no production matches. Mentions in migration documentation are allowed outside `YouleNexus/assets`.
- [ ] **Step 7: Commit**
```powershell
git add -A -- YouleNexus/assets/framework/platform/stores/app-store.ts YouleNexus/assets/framework/platform/stores/player-store.ts YouleNexus/assets/framework/platform/stores/room-store.ts YouleNexus/assets/framework/platform/stores/types.ts YouleNexus/assets/framework/platform/readonly.ts
git add -- scripts/run-framework-tests.mjs framework-tests/integration/platform-vertical-slice.test.ts framework-tests/integration/login-flow.test.ts framework-tests/architecture/import-boundaries.test.mjs framework-tests/platform
git commit -m "test(platform): cover complete vertical slice replay"
```
---
### Task 13: 真服与编辑器验收
**Files:**
- Modify only if evidence changes: `docs/superpowers/specs/2026-09-04-platform-vertical-slice-contract-inventory.md`
- Create: `docs/superpowers/verification/2026-09-04-platform-vertical-slice.md`
**Interfaces:**
- Consumes: a user-authorized test account, the original configuration service, Cocos preview/device environment.
- Produces: sanitized verification record with packet shapes, state transitions, screenshots/observations, and remaining out-of-scope items.
- [ ] **Step 1: Run preflight verification**
```powershell
npm run typecheck:framework
npm run test:framework
```
Expected: PASS before opening a real connection.
- [ ] **Step 2: Select an explicit debug profile**
Put the authorized account only in the existing debug profile mechanism or injected runtime data. Do not commit openid, phone, token, machine ID, real IP, or location.
- [ ] **Step 3: Run the original and Cocos clients against the same test service**
Capture and sanitize these envelopes: first `player_login`, successful `self_join_room`, `player_prepare`, reconnect `player_login`, and one room push. Compare route/rpc, field presence, primitive types, and nested structure.
- [ ] **Step 4: Verify UI/state behavior through Cocos MCP preview**
Confirm lobby entry, join-room input, room player rendering, ready state, reconnect overlay, room recovery, and kicked overlay. Capture screenshots through MCP if the tool supports it.
- [ ] **Step 5: Record evidence and discrepancies**
The verification document must list each acceptance item as PASS or FAIL with the observed packet/state evidence. A discrepancy is a failed acceptance item; do not add a fallback to make the run continue.
- [ ] **Step 6: Re-run complete automated verification after any correction**
```powershell
npm run typecheck:framework
npm run test:framework
```
Expected: PASS.
- [ ] **Step 7: Commit the verification record**
```powershell
git add -- docs/superpowers/verification/2026-09-04-platform-vertical-slice.md
git commit -m "docs(verification): record platform vertical-slice results"
```
---
## Final Acceptance Checklist
- [ ] `sdk/contracts` has no framework implementation or Cocos dependencies.
- [ ] Adding a test game changes only its own directory and application registration.
- [ ] Game messages are route-bound and cannot impersonate platform RPCs.
- [ ] Game sessions release subscriptions and reject stale calls after disposal.
- [ ] Remote WebSocket addresses come only from `data.urlserver` or an explicit debug direct profile.
- [ ] All first-batch server payloads pass strict boundary parsers.
- [ ] There is one Router and one PlatformStore.
- [ ] Login, room recovery, join, prepare, offline/online, reconnect and kick match source behavior.
- [ ] `deskinfo` uses the original client's truthiness trigger, is never stored by the platform, and is passed unchanged to `restore`.
- [ ] Descriptor-owned seat-count resolution and the Host's 2/4/10-seat round-trip mapping tests pass without importing framework internals into a game.
- [ ] Layer adapters contain no server fallback, mock identity, network code, or writable business state.
- [ ] All serialized Cocos changes were made and verified through funplay-cocos MCP.
- [ ] `npm run typecheck:framework` passes.
- [ ] `npm run test:framework` passes with zero skipped tests.
- [ ] Real test-server packet comparison passes without server or native-side changes.