Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
# game-server-engine-sessions — sessions + players (Wave B-2)
**Package**: `@suaveplan/game-server`
**Status**: PROPOSED
**Type**: feature (Wave B-2 migration slice)
- **Design**: see this file
- **Spec delta**: ./specs/game-server/spec.md
## Why
Wave A landed the foundations and Wave A interfaces (`ISessionManager`,
`IPlayerManager` et al). Wave B-1 shipped the engine top-level (GameEngine
+ GameLoop + PluginManager). Waves B-3 and B-4 shipped state + network.
What is missing: the session and player runtime that backs those Wave A
interfaces. Without it, the engine has nowhere to register a session or
track a connected player, and the `ISessionManager` /
`IPlayerManager` slots in `GameEngine` can only be filled by ad-hoc
stubs.
This slice (Wave B-2) ships five engine modules that round out the
runtime:
- `SessionLifecycle` — per-session FSM (`lobby → active → ended`) with
transition validation, audit history, and OTel dwell metrics.
- `SessionGraphNode` — internal mutable graph node implementing Wave A's
read-only `SessionNode` interface.
- `SessionTree` — hierarchical graph (`Map<id, node>` + `Set<rootId>`)
with O(1) lookups, Zod-validated boundaries, and typed event emission.
- `SessionStore` — `ISessionManager` implementation composing
`SessionTree` with an injectable `SessionPersistence` adapter for
durability.
- `PlayerManager` — `IPlayerManager` implementation with heartbeat-driven
liveness, reconnection grace window, and a timer-driven monitor that
routes through `TimersLike`.
This is a re-dispatch of an earlier B-2 attempt that branched from a
stale base and re-invented Wave A's interfaces. The new work builds on
top of the existing canonical interfaces and types.
## What changes
- ADDED: `SessionGraphNode` (`engine/session-node`) — internal mutable
graph node, structurally compatible with Wave A's `SessionNode`
interface; preserves timestamps on persistence round-trips.
- ADDED: `SessionTree` (`engine/session-tree`) — graph manager with
`createSession`/`destroySession`/`getStats`/`validateIntegrity` plus
the `attachRestored` seam used by `SessionStore.restore`.
- ADDED: `SessionStore` (`engine/session-store`) — `ISessionManager`
implementation with pluggable `SessionPersistence` (default in-memory).
- ADDED: `SessionLifecycle` (`engine/session-lifecycle`) — per-session
FSM with stable error codes for every rejection path.
- ADDED: `PlayerManager` (`engine/player-manager`) — `IPlayerManager`
implementation with injectable `Clock`/`TimersLike`/`EventBus`.
- ADDED: `GAME_SERVER.*` codes — `SESSION_NOT_FOUND`,
`SESSION_ALREADY_ENDED`, `INVALID_SESSION_TRANSITION`,
`DUPLICATE_PLAYER_IN_SESSION`, `INVALID_SESSION_DATA`,
`DUPLICATE_SESSION_ID`, `PARENT_SESSION_NOT_FOUND`,
`INVALID_SESSION_STORE_OPTIONS`, `INVALID_PLAYER_MANAGER_OPTIONS`,
`INVALID_HEARTBEAT_INTERVAL`, `PLAYER_DISCONNECTED`.
- ADDED: OTel instruments — `game_server.session.lifecycle_total`
counter, `game_server.session.dwell_ms` histogram,
`game_server.player.lifecycle_total` counter.
- ADDED: subpath exports `./engine/session-lifecycle`,
`./engine/session-node`, `./engine/session-store`,
`./engine/session-tree`, `./engine/player-manager`.
## Out of scope
- `GameEngine` re-wiring to consume `SessionStore` / `PlayerManager` at
construction (later orchestration slice; engine accepts them via DI).
- Hono / network plugin layer for session/player REST (later slice).
- The matchmaking / queue surface in upstream's `enhanced/` — out of
scope for B-2.
## Source mapping
| Requirement | Source lines | Notes |
|---|---|---|
| SessionNode graph helpers | `packages/universal/game-server/src/engine/SessionNode.ts` L8–L175 | Recast — `Date.now()` replaced by injectable `Clock`; idempotent `addChild`. |
| SessionTree O(1) graph | `packages/universal/game-server/src/engine/SessionTree.ts` L16–L441 | Recast — plain `Error` → `GameServerError`; Zod-validated boundary; persistence concern lifted into `SessionStore`. |
| SessionStore persistence | `packages/universal/game-server/src/engine/SessionStore.ts` L22–L132 | Recast — `AbstractStorage` byte API replaced by `SessionPersistence` adapter accepting structured records. |
| PlayerManager lifecycle | `packages/universal/game-server/src/engine/PlayerManager.ts` L13–L357 | Recast — `setInterval` replaced by `TimersLike`-driven self-rescheduling tick; reconnection grace window. |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `SessionLifecycle` FSM | Genesis bar — upstream `SessionLifecycle.ts` was a session+player coordinator, NOT an FSM. The B-2 dispatch explicitly requires a per-session `lobby → active → ended` FSM with transition validation + audit history. |
| OTel session lifecycle counter + dwell histogram | Genesis bar — upstream had no OTel. |
| OTel player lifecycle counter | Genesis bar — upstream had no OTel. |
| Zod-validated `SessionData` and option boundaries | Genesis bar. |
## Verification approach
- `src/engine/session-lifecycle/session-lifecycle.test.ts` — FSM
transition table, audit history, dwell sample emission, error paths.
- `src/engine/session-node/session-node.test.ts` — tree-walking helpers,
idempotent attach, persistence round-trip with preserved timestamps.
- `src/engine/session-tree/session-tree.test.ts` — Zod boundary, O(1)
lookups, bottom-up cascade, integrity audit, restore seam.
- `src/engine/session-store/session-store.test.ts` — `ISessionManager`
conformance harness, persist/restore round-trip, orphan handling.
- `src/engine/player-manager/player-manager.test.ts` —
`IPlayerManager` conformance harness, heartbeat monitor scheduling,
grace-window eviction, reconnection metadata merge.
## Impact
### Affected dependents
The package has no shipped dependents on genesis at the time of this
change. Additive surface only.
## Migration impact
None — additive only. Existing `./errors`, `./types`, `./core`, `./engine/*`
surfaces are unchanged. `GAME_SERVER_CODES` registry is **extended**;
all existing codes are preserved.

View File

@@ -0,0 +1,254 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: game-server (game-server-engine-sessions)
**Package**: `@suaveplan/game-server`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## ADDED Requirements
### Requirement: SessionGraphNode internal mutable graph node
The package SHALL provide `SessionGraphNode` as the internal mutable
representation of a hierarchical session. The class SHALL be structurally
compatible with Wave A's read-only `SessionNode` interface (it
implements `SessionNode`). All wall-clock reads SHALL route through an
injected `Clock`; the class SHALL NOT call `Date.now()` directly. The
constructor SHALL accept an optional preserved-timestamps slot used by
`SessionStore.restore` to keep `createdAt` / `updatedAt` byte-equivalent
across persistence round-trips.
#### Scenario: constructs with clock-supplied timestamps by default
- **WHEN** a node is constructed without the `timestamps` arg
- **THEN** `createdAt` and `updatedAt` are both set from the injected `Clock`
#### Scenario: addChild is idempotent
- **WHEN** `addChild(c)` is called twice with the same child
- **THEN** the child appears exactly once in `children`
#### Scenario: tree-walking helpers respect depth and ancestry
- **WHEN** `getDepth`, `getDescendants` (DFS pre-order), `getAncestors`, and `getRoot` are called on a populated tree
- **THEN** each helper returns the documented walk; `isAncestorOf` / `isDescendantOf` exclude self (strict)
#### Scenario: toJSON serialises root with null parentId
- **WHEN** a root node is serialised
- **THEN** `parentId` is `null`; for a non-root the parent's id is preserved
**Verified by:** `src/engine/session-node/session-node.test.ts::"constructs with clock-supplied createdAt/updatedAt when no timestamps passed"` AND `src/engine/session-node/session-node.test.ts::"preserves timestamps when explicitly passed (restore path)"` AND `src/engine/session-node/session-node.test.ts::"addChild is idempotent — re-adding the same child is a no-op"` AND `src/engine/session-node/session-node.test.ts::"getDepth returns 0 for root, distance otherwise"` AND `src/engine/session-node/session-node.test.ts::"getDescendants returns DFS pre-order array"` AND `src/engine/session-node/session-node.test.ts::"isAncestorOf / isDescendantOf are strict (exclude self)"` AND `src/engine/session-node/session-node.test.ts::"toJSON serialises a node with null parentId for a root"`
---
### Requirement: SessionTree hierarchical graph
The package SHALL provide `SessionTree` owning a `Map<id, SessionGraphNode>`
plus a `Set<rootId>` and exposing the mutation surface used by
`SessionStore`. The `createSession` boundary SHALL parse `SessionData`
through Zod and reject failures with `INVALID_SESSION_DATA`. Duplicate
ids SHALL throw `DUPLICATE_SESSION_ID`; unknown parents SHALL throw
`PARENT_SESSION_NOT_FOUND`. `destroySession` SHALL cascade bottom-up
through every descendant.
#### Scenario: createSession registers a root and emits session:created
- **WHEN** `createSession(null, data)` is called with valid data
- **THEN** the node is registered as a root and `session:created` is emitted
#### Scenario: invalid SessionData rejects with INVALID_SESSION_DATA
- **WHEN** `createSession` is called with `id: ""` or any schema-failing input
- **THEN** a `GameServerError(GAME_SERVER.INVALID_SESSION_DATA)` is thrown
#### Scenario: duplicate id rejects with DUPLICATE_SESSION_ID
- **WHEN** a second `createSession` reuses an existing id
- **THEN** `GameServerError(GAME_SERVER.DUPLICATE_SESSION_ID)` is thrown
#### Scenario: unknown parent rejects with PARENT_SESSION_NOT_FOUND
- **WHEN** `createSession(parentId, ...)` references a missing id
- **THEN** `GameServerError(GAME_SERVER.PARENT_SESSION_NOT_FOUND)` is thrown
#### Scenario: destroySession cascades bottom-up
- **WHEN** `destroySession` is called on an ancestor with descendants
- **THEN** `session:destroyed` events fire in deepest-first order
#### Scenario: attachRestored re-attaches a deserialised node without firing session:created
- **WHEN** `attachRestored(node, parentId)` is called during `SessionStore.restore`
- **THEN** the node is inserted into the index, the `session:created` event is NOT emitted, and a duplicate id throws `DUPLICATE_SESSION_ID`
#### Scenario: validateIntegrity catches corrupted invariants
- **WHEN** the bidirectional parent/children invariant is forced into an inconsistent state
- **THEN** `validateIntegrity()` returns `{valid: false, errors: [...]}` with at least one diagnostic
**Verified by:** `src/engine/session-tree/session-tree.test.ts::"createSession registers a root and emits session:created"` AND `src/engine/session-tree/session-tree.test.ts::"createSession rejects invalid SessionData with INVALID_SESSION_DATA"` AND `src/engine/session-tree/session-tree.test.ts::"createSession rejects duplicate ids with DUPLICATE_SESSION_ID"` AND `src/engine/session-tree/session-tree.test.ts::"createSession rejects unknown parents with PARENT_SESSION_NOT_FOUND"` AND `src/engine/session-tree/session-tree.test.ts::"destroySession cascades through descendants bottom-up"` AND `src/engine/session-tree/session-tree.test.ts::"attachRestored attaches a pre-built node without firing session:created"` AND `src/engine/session-tree/session-tree.test.ts::"validateIntegrity flags a tampered parent pointer"`
---
### Requirement: SessionStore implements ISessionManager
The package SHALL provide `SessionStore` implementing Wave A's
`ISessionManager` interface (`createSession`, `getSession`,
`destroySession`, `getSessionsByType`, `getChildren`, `getParent`,
`updateMetadata`, `getAllSessions`, `persist`, `restore`). The store
SHALL accept an injectable `SessionPersistence` adapter and fall back to
an in-memory default when none is supplied. Construction SHALL
Zod-validate options and reject with `INVALID_SESSION_STORE_OPTIONS`.
`restore` SHALL handle arbitrary record orderings via a fixed-point walk
that attaches any record whose parent is already present.
#### Scenario: ISessionManager conformance harness passes
- **WHEN** `runISessionManagerConformance(() => new SessionStore(deps))` is invoked
- **THEN** every scenario in the harness reports `passed: true`
#### Scenario: invalid options reject with INVALID_SESSION_STORE_OPTIONS
- **WHEN** `new SessionStore(deps, { storageKey: "" })`
- **THEN** the constructor throws `GameServerError(GAME_SERVER.INVALID_SESSION_STORE_OPTIONS)`
#### Scenario: persist + restore round-trip preserves the tree
- **WHEN** a populated `SessionStore` is persisted, then cleared, then restored
- **THEN** the restored tree has the same node count and parent/child relationships
#### Scenario: restore handles permuted record orderings
- **WHEN** a persisted payload lists children before their parents
- **THEN** the fixed-point walk attaches every record and the final tree shape is identical to the pre-persist state
#### Scenario: restore logs orphan records and skips them
- **WHEN** a payload references a parent that doesn't exist in the same payload
- **THEN** the orphan id is logged via `logger.warn` and the rest of the records still come online
**Verified by:** `src/engine/session-store/session-store.test.ts::"ISessionManager conformance harness passes"` AND `src/engine/session-store/session-store.test.ts::"rejects invalid options with INVALID_SESSION_STORE_OPTIONS"` AND `src/engine/session-store/session-store.test.ts::"persist+restore round-trips with the in-memory default"` AND `src/engine/session-store/session-store.test.ts::"restore handles permuted record ordering via fixed-point walk"` AND `src/engine/session-store/session-store.test.ts::"restore logs orphan records as warnings and skips them"`
---
### Requirement: SessionLifecycle per-session FSM
The package SHALL provide `SessionLifecycle` implementing a per-session
finite-state machine with the closed three-state vocabulary
`lobby | active | ended`. The transition table SHALL be:
`lobby → active`, `lobby → ended`, `active → ended`. Every other pair
SHALL be rejected with `INVALID_SESSION_TRANSITION`. Transitions out of
`ended` SHALL throw `SESSION_ALREADY_ENDED`. Transitions on unknown ids
SHALL throw `SESSION_NOT_FOUND`. Every transition SHALL emit a
`session:transitioned` event and record both
`game_server.session.lifecycle_total` (counter) and
`game_server.session.dwell_ms` (histogram) OTel instruments.
#### Scenario: register places the session in lobby
- **WHEN** `register(id)` is called
- **THEN** the new entry is in state `lobby` with `enteredStateAt = clock()`
#### Scenario: transition validates against the allow-table
- **WHEN** `transition("s1", "active")` is called on a `lobby` entry
- **THEN** the FSM advances to `active` and emits `session:transitioned`
#### Scenario: backwards transition rejects with INVALID_SESSION_TRANSITION
- **WHEN** `transition("s1", "lobby")` is called on an `active` entry
- **THEN** `GameServerError(GAME_SERVER.INVALID_SESSION_TRANSITION)` is thrown
#### Scenario: transition out of ended rejects with SESSION_ALREADY_ENDED
- **WHEN** `transition("s1", "active")` is called on an `ended` entry
- **THEN** `GameServerError(GAME_SERVER.SESSION_ALREADY_ENDED)` is thrown
#### Scenario: unknown id rejects with SESSION_NOT_FOUND
- **WHEN** `transition("nope", "active")` is called
- **THEN** `GameServerError(GAME_SERVER.SESSION_NOT_FOUND)` is thrown
#### Scenario: dwell histogram receives a sample per transition
- **WHEN** the FSM transitions out of `lobby`
- **THEN** `game_server.session.dwell_ms` records one sample tagged `state: "lobby"`
#### Scenario: forget drops the entry but preserves audit history
- **WHEN** `forget(id)` is called after one or more transitions
- **THEN** `getState(id)` returns `null` while `getHistoryFor(id)` still returns the transitions
**Verified by:** `src/engine/session-lifecycle/session-lifecycle.test.ts::"register creates an entry in lobby state"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"lobby → active transition succeeds and emits event"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"active → lobby (backwards) is rejected with INVALID_SESSION_TRANSITION"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"transition out of ended is rejected with SESSION_ALREADY_ENDED"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"transition on unknown id is rejected with SESSION_NOT_FOUND"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"emits session.dwell_ms histogram sample for the state being left"` AND `src/engine/session-lifecycle/session-lifecycle.test.ts::"history preserves transitions across forget()"`
---
### Requirement: PlayerManager implements IPlayerManager
The package SHALL provide `PlayerManager` implementing Wave A's
`IPlayerManager` interface (11 methods). Construction SHALL Zod-validate
options and reject with `INVALID_PLAYER_MANAGER_OPTIONS`.
`startHeartbeatMonitor` SHALL Zod-validate the interval and reject
non-positive integers with `INVALID_HEARTBEAT_INTERVAL`. The heartbeat
monitor SHALL schedule via the injected `TimersLike`, NOT `setInterval`.
`checkTimeouts` SHALL transition connected players past
`timeoutThresholdMs` to `timeout` and evict disconnected entries past
`reconnectionGraceMs`. A `connect` against an existing disconnected entry
within the grace window SHALL walk the reconnection path
(preserving `connectedAt`).
#### Scenario: IPlayerManager conformance harness passes
- **WHEN** `runIPlayerManagerConformance(() => new PlayerManager(deps))` is invoked
- **THEN** every scenario in the harness reports `passed: true`
#### Scenario: invalid options reject with INVALID_PLAYER_MANAGER_OPTIONS
- **WHEN** `new PlayerManager(deps, { timeoutThresholdMs: 0 })`
- **THEN** the constructor throws `GameServerError(GAME_SERVER.INVALID_PLAYER_MANAGER_OPTIONS)`
#### Scenario: startHeartbeatMonitor rejects non-positive intervals
- **WHEN** `startHeartbeatMonitor(0)` or `startHeartbeatMonitor(1.5)` is called
- **THEN** the call throws `GameServerError(GAME_SERVER.INVALID_HEARTBEAT_INTERVAL)`
#### Scenario: heartbeat monitor schedules via TimersLike
- **WHEN** `startHeartbeatMonitor(50)` is called
- **THEN** `TimersLike.setTimer` is invoked with delay 50; firing a due timer triggers `checkTimeouts` and the monitor self-reschedules
#### Scenario: stopHeartbeatMonitor cancels the next scheduled tick
- **WHEN** `stopHeartbeatMonitor()` is called after `startHeartbeatMonitor`
- **THEN** the pending timer is cleared and the captured callback, if fired post-stop, does not re-schedule itself
#### Scenario: connect walks the reconnection path for disconnected entries
- **WHEN** `connect("p1", "s1", { region: "eu" })` is called after `disconnect("p1", "x")`
- **THEN** `connectedAt` is preserved from the original entry, metadata is shallow-merged, and `player:reconnected` is emitted
#### Scenario: checkTimeouts evicts disconnected entries past grace window
- **WHEN** `checkTimeouts` is called with `now - conn.disconnectedAt > reconnectionGraceMs`
- **THEN** the connection is deleted; a subsequent `getConnection` returns `null`
**Verified by:** `src/engine/player-manager/player-manager.test.ts::"IPlayerManager conformance harness passes"` AND `src/engine/player-manager/player-manager.test.ts::"rejects invalid options with INVALID_PLAYER_MANAGER_OPTIONS"` AND `src/engine/player-manager/player-manager.test.ts::"startHeartbeatMonitor rejects non-positive intervals"` AND `src/engine/player-manager/player-manager.test.ts::"startHeartbeatMonitor schedules ticks via TimersLike"` AND `src/engine/player-manager/player-manager.test.ts::"heartbeat tick stops re-scheduling if monitor was stopped mid-tick"` AND `src/engine/player-manager/player-manager.test.ts::"connect on a disconnected player walks the reconnection path"` AND `src/engine/player-manager/player-manager.test.ts::"checkTimeouts evicts disconnected entries past grace window"`
---
### Requirement: Subpath exports
The package SHALL expose each new feature directory via additive
`exports` map entries so consumers can tree-shake.
#### Scenario: each feature dir is independently importable
- **WHEN** a consumer imports from `@suaveplan/game-server/engine/session-lifecycle`, `@suaveplan/game-server/engine/session-node`, `@suaveplan/game-server/engine/session-store`, `@suaveplan/game-server/engine/session-tree`, or `@suaveplan/game-server/engine/player-manager`
- **THEN** the import resolves to the corresponding feature barrel
**Verified by:** `packages/gaming/games/game-server/package.json::"exports./engine/session-lifecycle"` AND `packages/gaming/games/game-server/package.json::"exports./engine/session-node"` AND `packages/gaming/games/game-server/package.json::"exports./engine/session-store"` AND `packages/gaming/games/game-server/package.json::"exports./engine/session-tree"` AND `packages/gaming/games/game-server/package.json::"exports./engine/player-manager"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,66 @@
# Tasks — game-server-engine-sessions (Wave B-2, re-dispatch)
## 0. Pre-flight (non-negotiable)
- [x] 0.1 `git status` clean in worktree, `git worktree list` accounted for.
- [x] 0.2 Worktree at `.claude/worktrees/agent-ae5779d2ba388beff` — isolated branch built on current genesis HEAD (`71a33ed`).
- [x] 0.3 Manifest count unchanged — no new packages added, only modules inside `@suaveplan/game-server`.
## 1. Spec authoring
- [x] 1.1 Read upstream `SessionNode.ts`, `SessionTree.ts`, `SessionStore.ts`, `PlayerManager.ts`, `SessionLifecycle.ts`.
- [x] 1.2 Read existing Wave A `core/interfaces/i-session-manager` and `i-player-manager`.
- [x] 1.3 Read existing `openspec/changes/game-server-engine-core` proposal as template.
- [x] 1.4 Drafted `proposal.md`.
- [x] 1.5 Filled `## Source mapping` table; FSM-shape requirement listed in the "no source line" table with justification.
- [x] 1.6 Drafted `specs/game-server/spec.md`.
- [x] 1.7 Every requirement carries `**Verified by:**` line.
## 2. Implementation
- [x] 2.1 Extend `GAME_SERVER_CODES` with session + player codes (11 new).
- [x] 2.2 Extend `observability/otel.ts` with `sessionLifecycleCounter`, `sessionDwellHistogram`, `playerLifecycleCounter`.
- [x] 2.3 Implement `src/engine/session-node/session-node.ts` — internal mutable graph node.
- [x] 2.4 Implement `src/engine/session-tree/session-tree.ts` — graph manager with Zod boundary.
- [x] 2.5 Implement `src/engine/session-store/session-store.ts` — `ISessionManager` impl with pluggable persistence.
- [x] 2.6 Implement `src/engine/session-lifecycle/session-lifecycle.ts` — per-session FSM.
- [x] 2.7 Implement `src/engine/player-manager/player-manager.ts` — `IPlayerManager` impl.
- [x] 2.8 Add subpath exports to `package.json` for all five new feature dirs.
## 3. Tests
- [x] 3.1 `session-node.test.ts` — 15 scenarios covering tree walks + persistence round-trip.
- [x] 3.2 `session-tree.test.ts` — 25 scenarios covering CRUD, integrity audit, restore seam.
- [x] 3.3 `session-store.test.ts` — `ISessionManager` conformance harness + persist/restore + orphan handling.
- [x] 3.4 `session-lifecycle.test.ts` — 15 scenarios covering FSM transitions, history, OTel emission.
- [x] 3.5 `player-manager.test.ts` — `IPlayerManager` conformance + heartbeat monitor + reconnection + grace eviction.
- [x] 3.6 Every `it(...)` carries `{ timeout: 5000 }`.
## 4. Documentation
- [x] 4.1 Sibling `.md` for every implementation `.ts` (6 docs: session-node, session-tree, session-store, session-lifecycle, player-manager, plus extended otel.md).
- [x] 4.2 `otel.md` updated with new instruments.
## 5. Pre-archive verification (the 5-gate triple)
- [x] 5.1 `bun run lint` — Biome 0 errors / 0 warnings.
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/game-server` — green (395 tests).
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/game-server` — 100/100/100/100 (1542/1542 stmts, 574/574 branches, 360/360 funcs, 1491/1491 lines).
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/game-server` — to be run by archive step.
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/game-server` — to be run by archive step (orchestrator merges before build run).
## 6. Pre-archive 7-row checklist (AGENTS.md §13)
- [x] 6.1 Generics threaded; conformance harnesses pass for both interfaces.
- [x] 6.2 Zod schemas wired at `SessionData`, `SessionStoreOptions`, `PlayerManagerOptions` boundaries.
- [x] 6.3 Every new error code thrown by at least one production path and asserted in a test.
- [x] 6.4 Lifecycle path tested both for `ok` and `error` outcomes.
- [x] 6.5 OTel instruments emitted from production paths and asserted with the in-memory `MeterProvider` polyfill.
- [x] 6.6 Reconnection grace window + heartbeat monitor self-rescheduling tested.
- [x] 6.7 No retroactive tick — every box reflects work done in this change.
## 7. Archive
- [ ] 7.1 Fold spec delta into `openspec/specs/game-server/spec.md` — handled by orchestrator post-merge.
- [ ] 7.2 Move folder to archive — handled by orchestrator post-merge.
- [ ] 7.3 Refresh `MANIFEST.md` if package set changed (it did not — additive modules only).