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

56 KiB
Raw Blame History

YouleNexus Platform Vertical Slice Implementation Plan

状态:已废止,不得执行。其运行时游戏注册和目录假设已被新的单游戏编译期组合架构替代。

权威设计:docs/superpowers/specs/2026-09-04-framework-subgame-zero-coupling-migration-design.md。用户审阅新设计后再生成替代实施计划。

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

本计划完成后的核心结构:

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

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".

assert.equal(
  forbidden,
  undefined,
  `${file} imports forbidden dependency: ${forbidden}`,
);
  • Step 3: Run the tests and verify failure

Run:

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
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
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
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
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

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.

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
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.

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
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
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.

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
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
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
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
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
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.

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.

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.

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
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
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
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
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

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.

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
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
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
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
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.

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
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
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
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

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
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
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
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
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
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

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
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
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
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
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.

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
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
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
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
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

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
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
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
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:

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
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
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
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:

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
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:

const r = spawnSync(
  process.execPath,
  ['--import', 'tsx', '--test', '--test-concurrency=1', ...files],
  { stdio: 'inherit' },
);
  • Step 5: Run the complete framework suite
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
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
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

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
npm run typecheck:framework
npm run test:framework

Expected: PASS.

  • Step 7: Commit the verification record
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.