Initial Commit
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user