Files
suaveplan/openspec/changes/enterprise-game-server-core/specs/game-server/spec.md
2026-05-27 23:29:53 +01:00

6.6 KiB

Capability Spec Delta: game-server (enterprise-game-server-core)

Package: @suaveplan/game-server Status: PROPOSED

ADDED Requirements

Requirement: Typed error namespace

The package SHALL expose a single GameServerError class extending SuaveplanError<GameServerCode> and a GAME_SERVER_CODES registry produced by defineCodes("GAME_SERVER", [...]). The registry SHALL enumerate only codes thrown by an actual production code path.

Scenario: registry only contains codes thrown by production paths

  • WHEN the test suite inspects the registered codes
  • THEN the registry contains exactly SERVICE_NOT_REGISTERED, INVALID_GAME_CONFIG, INVALID_SESSION_METADATA, and INVALID_PLAYER_INFO, each fully-qualified as GAME_SERVER.<NAME>.

Scenario: errors carry code, message, cause, and context

  • WHEN a caller constructs new GameServerError(code, message, { cause, context })
  • THEN the resulting instance is an instanceof SuaveplanError, has .name === "GameServerError", exposes the typed .code, and forwards .cause and .context to the standard logging/OTel adapters.

Verified by: src/errors/game-server-error.test.ts::"registers all expected codes under GAME_SERVER namespace" AND src/errors/game-server-error.test.ts::"constructs an error with code, message, cause, and context" AND src/errors/game-server-error.test.ts::"constructs without optional cause/context" AND src/errors/game-server-error.test.ts::"supports discriminated access via the code union".


Requirement: Zod-validated boundary types

The package SHALL expose Zod schemas (GameConfigSchema, SessionMetadataSchema, PlayerInfoSchema, MoveValidationResultSchema, GameResultSchema, SessionTypeSchema) and throwing validators (parseGameConfig, parseSessionMetadata, parsePlayerInfo) plus non-throwing probes (tryParseMoveValidationResult, tryParseGameResult) that turn untrusted input into the corresponding typed value or raise a typed GameServerError with the matching GAME_SERVER.INVALID_* code.

Scenario: GameConfig enforces maxPlayers >= minPlayers

  • WHEN parseGameConfig is given { gameType: "x", maxPlayers: 1, minPlayers: 4, rules: {} }
  • THEN it throws GameServerError with code GAME_SERVER.INVALID_GAME_CONFIG and a cause set to the underlying ZodError.

Scenario: SessionMetadata accepts passthrough extras with non-negative timestamps

  • WHEN parseSessionMetadata is given a record with createdAt/updatedAt non-negative integers plus arbitrary extra keys
  • THEN the result preserves the extras and validates the timestamps.

Scenario: MoveValidationResult and GameResult reject extra keys

  • WHEN an off-spec object includes additional keys not declared in the strict schema
  • THEN the non-throwing probe returns null.

Verified by: src/types/common.test.ts::"rejects maxPlayers < minPlayers" AND src/types/common.test.ts::"throws GameServerError with INVALID_GAME_CONFIG" AND src/types/common.test.ts::"accepts a valid timestamp pair with passthrough extras" AND src/types/common.test.ts::"throws GameServerError with INVALID_SESSION_METADATA" AND src/types/common.test.ts::"throws GameServerError with INVALID_PLAYER_INFO on bad input" AND src/types/common.test.ts::"returns null on off-spec input" (twice).


Requirement: Dependency-injection container

The package SHALL expose a DIContainer class with register, resolve, has, unregister, clear, and getRegisteredServices methods. register SHALL support "singleton" (default) and "transient" lifecycles, return this for chaining, and discard any cached singleton instance when an identifier is re-registered. resolve SHALL throw GameServerError with code GAME_SERVER.SERVICE_NOT_REGISTERED for unknown identifiers.

Scenario: singleton factory invoked exactly once

  • WHEN an identifier registered as "singleton" is resolved repeatedly
  • THEN the factory runs once and every resolution returns the same instance.

Scenario: transient factory invoked on every resolve

  • WHEN an identifier registered as "transient" is resolved repeatedly
  • THEN the factory runs once per resolution and instances are not cached.

Scenario: unknown identifier throws typed error

  • WHEN resolve is called with an identifier that was never registered
  • THEN a GameServerError is thrown with code GAME_SERVER.SERVICE_NOT_REGISTERED, the message contains the stringified identifier, and .context.identifier matches.

Scenario: re-register invalidates cached singleton

  • WHEN an identifier is registered twice with different factories
  • THEN the next resolve returns an instance produced by the second factory, not the cached first instance.

Verified by: src/engine/di-container/di-container.test.ts::"registers and resolves a singleton (default lifecycle)" AND src/engine/di-container/di-container.test.ts::"registers and resolves a transient service" AND src/engine/di-container/di-container.test.ts::"throws GameServerError(SERVICE_NOT_REGISTERED) for unknown identifier (string)" AND src/engine/di-container/di-container.test.ts::"throws with the symbol stringified for unknown symbol identifier" AND src/engine/di-container/di-container.test.ts::"re-register invalidates the cached singleton" AND src/engine/di-container/di-container.test.ts::"unregister returns true for known, false for unknown, and clears cache" AND src/engine/di-container/di-container.test.ts::"clear() removes registrations and singleton cache" AND src/engine/di-container/di-container.test.ts::"getRegisteredServices() returns the registered identifiers" AND src/engine/di-container/di-container.test.ts::"supports symbol identifiers with typed generics" AND src/engine/di-container/di-container.test.ts::"supports fluent chained registration" AND src/engine/di-container/di-container.test.ts::"has() reflects registration state".

Deferred (2026-05-23): the broader core/{AbstractGameLogic, interfaces/*, events/*}, engine/{GameEngine, GameLoop, PlayerManager, PluginManager, SessionLifecycle, SessionNode, SessionStore, SessionTree}, state/*, matchmaking/*, performance/* → telemetry/*, network/*, plugins/*, enhanced/*, and integrations/* surfaces from the reference branch claude/review-game-engine-arch-Kaykr are tracked as follow-on changes per the deferral table in proposal.md. Six of those dependencies (controller, keys, network-webrtc, network-websockets, storage-browser, workflow) are not yet present on genesis and block the corresponding migration slices.