From 0dd33690f74e1c0d1d038599ef253c19c7ae6d72 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 25 Feb 2026 11:29:41 +0100 Subject: [PATCH] docs(sprints): add Sprint 19: Persist briefings Server (7), client (5), CI (4) briefings plus joint integration plan. Save/load with D-085 per-game dirs, tier eviction/scope, test infra. Co-Authored-By: Claude Opus 4.6 --- docs/sprints/sprint-19/ci.md | 122 +++++++++++++++++++++++++ docs/sprints/sprint-19/client.md | 128 ++++++++++++++++++++++++++ docs/sprints/sprint-19/joint.md | 97 ++++++++++++++++++++ docs/sprints/sprint-19/server.md | 152 +++++++++++++++++++++++++++++++ 4 files changed, 499 insertions(+) create mode 100644 docs/sprints/sprint-19/ci.md create mode 100644 docs/sprints/sprint-19/client.md create mode 100644 docs/sprints/sprint-19/joint.md create mode 100644 docs/sprints/sprint-19/server.md diff --git a/docs/sprints/sprint-19/ci.md b/docs/sprints/sprint-19/ci.md new file mode 100644 index 000000000..0c9e79a9f --- /dev/null +++ b/docs/sprints/sprint-19/ci.md @@ -0,0 +1,122 @@ +# Sprint 19: Persist — CI Tasks + +**Goal:** The player can save and resume a game session with per-game directories; the simulation tier system gains eviction and scope pinning; and the first test infrastructure ships with information boundary validation and IPC hardening. + +**Branch:** `ci` +**Agents:** Hoshe (QA/CI), Oscar (networking) + +## Carry-over from Sprint 18 + +None. + +## New Tickets + +| # | Title | Blocked by | +|---|-------|------------| +| #270 | Test runner bash scripts | — | +| #556 | Protocol version handshake: client | #555 (server) | +| #342 | IPC round-trip timing benchmark | #555, #556 | +| #271 | IPC serialization fixture files | #270 | + +Use `db/connectors/ticket show ` for full details. + +## Key Decisions + +- `decisions/architecture.md` — D-020 (IPC architecture, MessagePack codec, SimBridge trait), D-030 (three-layer test architecture: fixture / mock-protocol / real-subprocess) + +## Notes + +### #270 — Test runner bash scripts + +The test infrastructure has no standardized entry points for CI or agents to invoke. This ticket ships the runner layer. + +What this ticket must deliver: +Six scripts at `tests/`: +1. `tests/run-rust` — runs `cargo test` in `server/`, exits 0/non-zero, JSON stdout summary +2. `tests/run-godot` — runs GUT headlessly (`godot --headless -s client/tests/run_gut.gd`), exits 0/non-zero +3. `tests/run-ipc-fixtures` — Layer 1: reads fixture files from `tests/fixtures/`, validates via Rust + GDScript, exits 0/non-zero +4. `tests/run-ipc-protocol` — Layer 2: runs mock subprocess protocol state machine tests +5. `tests/run-ipc-integration` — Layer 3: starts real server subprocess, runs full round-trip, kills it +6. `tests/run-all` — invokes all five in order, collects exit codes, reports JSON summary + +Script requirements per ticket description: exit code 0/non-zero, structured JSON stdout, accepts filter arguments (`--filter test_name`), no interactive input, whitelistable for Claude Code agents (no TTY prompts). + +JSON stdout format (consistent across all scripts): +```json +{"suite": "rust", "total": 42, "passed": 42, "failed": 0, "duration_ms": 1230} +``` + +These scripts are the entry points that `make ci-server`, `make ci-client`, and future CI pipelines call. Coordinate with Makefile targets in `docs/DEVOPS.md`. + +### #556 — Protocol version handshake: client + +Blocked by #555 (server must send `HandshakeMessage` first). + +What this ticket must deliver: +- `client/scripts/protocol/local_bridge.gd` (or `server_process.gd`): after starting the server subprocess, read the first framed message from the IPC channel +- Validate it is a `HandshakeMessage` with `protocol_version == Protocol.PROTOCOL_VERSION` (14) +- If mismatch: log error "Protocol version mismatch: server=%d, client=%d", emit a `handshake_failed` signal, shut down the server process gracefully +- If match: emit `handshake_complete`, begin normal tick loop +- Add a timeout: if no handshake message received within 5 seconds of process start, treat as mismatch + +Current state: `client/scripts/protocol/protocol.gd` already checks `version` in `decode_snapshot()` and logs a mismatch. That check is per-snapshot. The handshake is the startup-time equivalent — validate once at connection, not per tick. + +Files: `client/scripts/protocol/local_bridge.gd`, `client/scripts/protocol/server_process.gd`. + +### #342 — IPC round-trip timing benchmark + +Sprint exit criterion. Measures the complete latency path from server serialization to client scene update. + +What this ticket must deliver: +- A benchmark script `tests/run-ipc-benchmark` that: + 1. Starts the server subprocess + 2. Waits for handshake (#555/#556) + 3. Sends N `PlayerInput` messages (N = 100 by default) + 4. Measures from `rmp_serde::to_vec` (server) to scene update completion (client) + 5. Reports p50/p95/p99 latencies in milliseconds + 6. Flags if any percentile exceeds 5ms threshold +- Output JSON: `{"p50_ms": 1.2, "p95_ms": 2.8, "p99_ms": 4.1, "threshold_ms": 5, "passed": true}` +- The benchmark is run as part of `tests/run-ipc-integration` in Layer 3 + +Implementation approach: server-side timestamps in `ObserverSnapshot` (add `server_emit_tick_ms` field, stripped in production builds), client records receive timestamp via `Time.get_ticks_msec()`. Delta = client receive - server emit. + +Blocked by #555 and #556 — benchmark requires a working handshake before timing can start cleanly. + +### #271 — IPC serialization fixture files + +Layer 1 test data: pre-generated `.msgpack` fixture files that both Rust and GDScript can read to verify cross-language serialization compatibility. + +What this ticket must deliver: +- A Rust binary (or test in `server/src/`) that generates fixtures to `tests/fixtures/`: + - `snapshot_minimal.msgpack` — minimal valid `ObserverSnapshot` (version=14, tick=0, one entity) + - `snapshot_full.msgpack` — all optional fields populated (monologue, dialogue, inventory, POIs, KG dump) + - `player_input_move.msgpack` — `PlayerInput { tick: 1, action: MoveNorth }` + - `player_input_interact.msgpack` — `PlayerInput { tick: 2, action: Interact { target: 99, verb: "Talk" } }` + - `malformed.msgpack` — intentionally truncated bytes (tests error handling) +- A GDScript test `client/tests/test_ipc_fixtures.gd` that reads each `.msgpack` fixture file, decodes via `Protocol.decode_snapshot()` / `Protocol.decode_player_input()`, and asserts expected field values +- Cross-language verification: the same byte stream decoded by both Rust and GDScript must produce identical field values + +The fixture generator is run once (manually or in CI pre-step) to produce the committed `.msgpack` files. The files live at `tests/fixtures/` and are committed to the repo. + +Blocked by #270 — fixture tests are invoked by `tests/run-ipc-fixtures`. + +## Dependency Chain + +``` +#555 (server: protocol handshake) → #556 (ci: protocol handshake: client) + #555 + #556 → #342 (IPC benchmark: requires working handshake) + +#270 (test runner scripts) → #271 (fixture files: invoked by run-ipc-fixtures) + +Parallel starts: #270, #555 (server-side) — both unblocked week 1 +#556 starts after #555 is at review +#271 starts after #270 merges +#342 starts after #555 + #556 both land +``` + +## PR Workflow + +When ready to submit, create a PR with `tea` CLI. **All flags are required** to avoid TTY prompts (see CLAUDE.md "Gitea access" section): +```bash +tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(ci): description" --description "body" --base main --head ci +``` diff --git a/docs/sprints/sprint-19/client.md b/docs/sprints/sprint-19/client.md new file mode 100644 index 000000000..ad7bb34f4 --- /dev/null +++ b/docs/sprints/sprint-19/client.md @@ -0,0 +1,128 @@ +# Sprint 19: Persist — Client Tasks + +**Goal:** The player can save and resume a game session with per-game directories; the simulation tier system gains eviction and scope pinning; and the first test infrastructure ships with information boundary validation and IPC hardening. + +**Branch:** `client` +**Agents:** Stig (UI), Oscar (networking) + +## Carry-over from Sprint 18 + +None. Sprint 18 closed clean. + +## New Tickets + +| # | Title | Blocked by | +|---|-------|------------| +| #554 | Save/load: client UI | #553 (server) | +| #258 | Game session management | — | +| #205 | GDScript test framework setup | — | +| #206 | Scene testing utilities | #205 | +| #348 | Debug visualization overlay | — | + +Use `db/connectors/ticket show ` for full details. + +## Key Decisions + +- `decisions/architecture.md` — D-020 (IPC architecture, MessagePack), D-085 (per-game save directory structure) +- `decisions/questions.md` — Q-029 (save file format design — open, Sprint 19 uses MessagePack quick-and-dirty format) + +## Open Questions to Resolve Early + +- **Q-029: Save file format design** — Sprint 19 ships MessagePack quick-and-dirty format. Do not over-engineer the loading screen metadata. A readable directory name (`-/`) per D-085 is sufficient for v0.1. The full versioning/migration design is tracked in Q-029 for a later sprint. + +## Notes + +### #554 — Save/load: client UI + +Blocked by #553 (server must implement `SaveCommand`/`LoadCommand` IPC messages before client can wire F5/F6). + +What this ticket must deliver: +- F5 key mapped in `client/scripts/autoloads/input_mapper.gd` to send a `SaveGame` IPC action to the server with the active game directory path (`user://saves//quicksave.sav`) +- F6 key mapped to send `LoadGame` IPC action with the same path +- Server responds with `SaveComplete`/`LoadComplete` — client shows a brief HUD notification ("Saved" / "Loading...") +- Loading screen scene: reads `user://saves/` directory, lists subdirectories sorted by last-modified (most recent first), shows most recent save filename per game directory per D-085 +- F6 from the main menu opens the loading screen +- The active `game-id` is tracked in `GameState` autoload (add `current_game_id: String`) + +Integration points: `client/scripts/autoloads/input_mapper.gd` (key bindings), `client/scripts/autoloads/game_state.gd` (current_game_id field), `client/scripts/protocol/` (new IPC message encoding), `client/scripts/ui/` (loading screen scene). + +Save directory path per D-085: `user://saves/-/` where game-id is created on New Game (#258). F5 quicksave writes to `user://saves//quicksave.sav`. Loading screen lists directories sorted by `FileAccess.get_modified_time()`. + +Wireframe reference: `docs/design/wireframes/menus/v01-save-load.png`. + +### #258 — Game session management + +New Game creates the per-game save directory before any save occurs (D-085 requirement: "directory created on New Game — even before the first save, so the path exists for quicksave/autosave"). + +What this ticket must deliver: +- `GameState.current_game_id: String` — format `-` (e.g. `20260225-143022-a7b3f1`) +- On "New Game": generate game-id (timestamp + RNG hex suffix), create `user://saves//` directory via `DirAccess.make_dir_recursive()` +- On "Continue" / loading screen selection: set `current_game_id` from the selected directory name +- "Quit to menu" flow: prompt "Save before quitting?" — F5 save if confirmed +- Wire the game-id into the `SimBridge` startup: server subprocess launched with `--game-id ` argument (or equivalent) so server can log with the same ID + +Integration points: `client/scripts/autoloads/game_state.gd` (new fields), `client/scripts/protocol/server_process.gd` (subprocess launch args), `client/scripts/ui/` (main menu scene: New Game / Continue buttons). + +Note: `game_state.gd` is already the largest autoload with 300+ lines. Keep game session logic in a thin wrapper on `GameState` — do not add another 100-line block directly. Consider a `session_manager.gd` helper if the logic exceeds 40 lines. + +### #205 — GDScript test framework setup + +The project has no GDScript test infrastructure yet. The Godot client has no equivalent of `cargo test`. + +What this ticket must deliver: +- Install and configure **GUT (Godot Unit Test)** as the GDScript test framework — it has the best Godot 4 support and is actively maintained +- Create `client/tests/` as the test root directory +- `client/tests/run_gut.gd`: the GUT runner script that CI can invoke headlessly (`godot --headless -s client/tests/run_gut.gd`) +- Exit code 0 = all pass, non-zero = failures — required for CI integration (#270 test runner scripts) +- A single smoke test `client/tests/test_protocol.gd`: verifies `Protocol.decode_snapshot(bytes)` returns non-null for a minimal valid msgpack fixture + +GUT installation: add as a Godot addon. Check if there is already an `addons/` directory in `client/`. + +### #206 — Scene testing utilities + +Blocked by #205 (GUT must be installed first). + +What this ticket must deliver: +- `client/tests/util/scene_helper.gd`: loads a scene file by path, instantiates it into a temporary viewport, provides `assert_node_exists(path)`, `assert_signal_emitted(node, signal_name)`, and `get_node_at(path)` helpers +- `client/tests/test_game_state.gd`: tests for `GameState.apply_snapshot()` — verify that a snapshot dictionary with known fields updates the correct `GameState` fields +- `client/tests/test_protocol.gd` (extend from #205 smoke test): add roundtrip test for `Protocol.encode_player_input()` and `Protocol.decode_player_input()` + +These utilities are the scaffolding for all future client tests. Keep them minimal and dependency-free — do not require a running server. + +### #348 — Debug visualization overlay + +Dev tool (F3 toggle). The stub `client/scripts/ui/debug_overlay.gd` already exists. + +What this ticket must deliver: +- Extend `debug_overlay.gd` to draw on a `CanvasLayer` above the game world: + - **Pathfinding waypoints**: draw lines between waypoint positions from `GameState.visible_entities` (entities with kind `Npc` — estimate waypoints from position delta between ticks) + - **Line-of-sight rays**: draw lines from player position to each visible entity + - **Vision cone boundary**: draw the forward/peripheral arc boundary using `GameState.visibility_sectors` + - **Information state tags**: draw confidence label (Suspects/KnowsOf/KnowsDetails/Direct) above each visible NPC from `GameState.player_knowledge` + - **Tick timing graph**: small line chart in corner showing tick delta over the last 30 ticks +- F3 toggle: connected to `InputMapper` action `toggle_debug_overlay` +- Debug overlay is **dev-only**: compiled out in export builds via `OS.is_debug_build()` check + +Integration points: `client/scripts/autoloads/game_state.gd` (data source), `client/scripts/autoloads/input_mapper.gd` (F3 action), `client/scripts/ui/debug_overlay.gd` (extend existing stub). + +## Dependency Chain + +``` +#205 (GDScript test framework) → #206 (scene testing utilities) + +#258 (game session management) → #554 (save/load client UI: needs current_game_id) + #553 (server, ECS extraction) → #554 (save/load client UI: needs IPC commands) + +#348 (debug overlay) → standalone, parallel track +``` + +Parallel starts: #258, #205, #348 all unblocked week 1. +#554 starts after #553 (server) reaches review stage and #258 lands. +#206 starts after #205 merges. + +## PR Workflow + +When ready to submit, create a PR with `tea` CLI. **All flags are required** to avoid TTY prompts (see CLAUDE.md "Gitea access" section): +```bash +tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(client): description" --description "body" --base main --head client +``` diff --git a/docs/sprints/sprint-19/joint.md b/docs/sprints/sprint-19/joint.md new file mode 100644 index 000000000..d09c4664f --- /dev/null +++ b/docs/sprints/sprint-19/joint.md @@ -0,0 +1,97 @@ +# Sprint 19: Persist — Joint Briefing + +**Goal:** The player can save and resume a game session with per-game directories; the simulation tier system gains eviction and scope pinning; and the first test infrastructure ships with information boundary validation and IPC hardening. + +**Sprint:** 19 +**Status:** planning → active + +## Pre-Sprint + +Before implementation begins, no schema work is needed — `SaveStateV1` is already defined (#256, done). However, the following IPC protocol additions must be agreed between server and client **before either side implements**: + +| Item | Owner | Needed by | +|------|-------|-----------| +| `HandshakeMessage` wire format | server (#555) | client (#556) | +| `SaveCommand` / `LoadCommand` IPC message variants | server (#553) | client (#554) | +| `SaveComplete` / `LoadComplete` response format | server (#553) | client (#554) | +| Fixture file format and field names | ci (#271) | all teams | + +Server team: define these in `server/src/bridge/types.rs` first (as Rust structs + serde). CI team + client team: implement against the published definitions. Do not start #556 or #554 until #555 and #553 respectively reach review. + +## Team Allocation + +| Team | Tickets | Count | +|------|---------|-------| +| server | #553, #96, #97, #98, #200, #272, #555 | 7 | +| client | #554, #258, #205, #206, #348 | 5 | +| ci | #270, #556, #342, #271 | 4 | + +## Cross-Team Dependencies + +``` +server #555 (handshake: server) + → ci #556 (handshake: client) + → ci #342 (IPC benchmark) + +server #553 (ECS extraction) + → client #554 (save/load UI) + +server #200 (test module org) + → server #272 (info boundary tests) + +client #205 (GDScript test framework) + → client #206 (scene testing utilities) + → ci #271 (fixture files need GDScript reader) + +ci #270 (test runner scripts) + → ci #271 (fixture tests invoked by run-ipc-fixtures) +``` + +## Sprint Completion Proof + +When Sprint 19 is done, the following must all be observable: + +1. **Save/load round-trip**: Press F5 in-game → file appears at `user://saves//quicksave.sav` in MessagePack format. Press F6 → game state restored from file (tick, entities, player knowledge match pre-save state). + +2. **Per-game directory**: Starting a New Game creates `user://saves/-/` before any save occurs. The loading screen lists this directory. + +3. **Tier eviction**: Spawn 90+ NPCs (above Active cap of 80). `ActiveSim` count stabilizes at ≤80 with the excess evicted to `BackgroundSim`/`StateSaved`. Scope-tagged NPCs (KnownContact, Colleague) remain Active regardless. + +4. **Protocol handshake**: Starting the server subprocess: first IPC message is a `HandshakeMessage`. Version mismatch (force by temporarily changing server `PROTOCOL_VERSION`) produces an error and clean shutdown — no crash. + +5. **Test infrastructure**: `tests/run-all` exits 0 with all suites passing. `tests/run-ipc-fixtures` reads committed `.msgpack` files and validates both Rust and GDScript decode them identically. `cargo test` in `server/` includes information boundary negative tests that assert absence of leakage. + +6. **Debug overlay**: F3 in-game toggles the debug canvas showing vision cone arcs, entity LOS rays, NPC knowledge confidence labels, and tick timing graph. + +## Test Plan (D-030) + +| Layer | Runner | Tickets | When | +|-------|--------|---------|------| +| Layer 1: Fixture serialization | `tests/run-ipc-fixtures` | #271, #200 | Every edit | +| Layer 1: Unit tests (Rust) | `tests/run-rust` | #272, #96, #97, #98 | Every edit | +| Layer 1: Unit tests (GDScript) | `tests/run-godot` | #205, #206 | Every edit | +| Layer 2: Mock protocol | `tests/run-ipc-protocol` | #555, #556 | Every PR | +| Layer 3: Real subprocess | `tests/run-ipc-integration` | #342, #553/#554 | Daily/pre-merge | + +All layers must pass before any PR merges. `make ci` invokes `tests/run-all`. + +## Key Decisions Reference + +| Decision | Domain file | Relevant to | +|----------|------------|-------------| +| D-010: Determinism + info boundaries | architecture.md | #272, #96, #553 | +| D-020: IPC architecture, MessagePack | architecture.md | #553, #554, #555, #556, #342, #271 | +| D-026: Simulation tiers, timestamp eviction, scope tags | architecture.md | #96, #97, #98 | +| D-030: Three-layer test architecture | architecture.md | #200, #270, #271, #272, #342 | +| D-085: Per-game save directory structure | architecture.md | #554, #258, #553 | +| Q-029: Save file format (open) | questions.md | #553 (quick-and-dirty MessagePack for now) | + +## Risk Register + +| Risk | Probability | Impact | Mitigation | +|------|------------|--------|------------| +| ECS extraction misses components (#553) | Medium | High | #272 info boundary tests catch leakage; fixture roundtrip (#271) catches missing fields | +| IPC protocol mismatch between #555 and #556 | Low | High | Define wire types in Rust first, share definition doc before client implements | +| GUT framework incompatible with Godot 4.x version in use (#205) | Low | Medium | Verify GUT version before full installation; fallback to hand-rolled test runner | +| Save file bloat (SaveStateV1 larger than ~1-2 KB/NPC) | Low | Low | Q-029 tracks compression — deferred. Profile with #342 benchmark if flagged | +| Scope tag assignment races with eviction (#97/#98) | Low | Medium | Eviction runs after scope tag system in schedule order; schedule ordering test in #97 | diff --git a/docs/sprints/sprint-19/server.md b/docs/sprints/sprint-19/server.md new file mode 100644 index 000000000..3470cd932 --- /dev/null +++ b/docs/sprints/sprint-19/server.md @@ -0,0 +1,152 @@ +# Sprint 19: Persist — Server Tasks + +**Goal:** The player can save and resume a game session with per-game directories; the simulation tier system gains eviction and scope pinning; and the first test infrastructure ships with information boundary validation and IPC hardening. + +**Branch:** `server` +**Agents:** Dudley (simulation), Tyre (architecture), Hoshe (QA) + +## Carry-over from Sprint 18 + +None. Sprint 18 closed clean. + +## New Tickets + +| # | Title | Blocked by | +|---|-------|------------| +| #553 | Save/load: server ECS extraction | #256 (done) | +| #96 | State serialization system | — | +| #97 | Timestamp-based eviction | — | +| #98 | Scope tag system | — | +| #200 | Test module organization | — | +| #272 | Information boundary negative test suite | #200 | +| #555 | Protocol version handshake: server | — | + +Use `db/connectors/ticket show ` for full details. + +## Key Decisions + +- `decisions/architecture.md` — D-010 (determinism + info boundaries), D-020 (IPC architecture, MessagePack), D-026 (simulation tiers: Active/Background/State-saved/Ungenerated, timestamp eviction, scope tags), D-030 (three-layer test architecture), D-041 (knowledge graph data model, StableId) +- `decisions/questions.md` — Q-029 (save file format design — Sprint 19 ships quick-and-dirty, Q-029 tracks the thorough design pass for later) + +## Open Questions to Resolve Early + +- **Q-029: Save file format design** — Sprint 19 uses MessagePack from `SaveStateV1`. Resolution of full versioning/migration strategy is deferred. Do not block #553 on Q-029; proceed with MessagePack format as specified. + +## Notes + +### #553 — Save/load: server ECS extraction + +`server/src/simulation/save_state.rs` already defines `SaveStateV1` (done in #256). The data model is complete: tick, seed, RNG, `player_knowledge: KnowledgeGraph`, `relationship_graph: RelationshipGraph`, `npc_states: Vec`. Roundtrip tests pass. + +What this ticket must deliver: +- A `save_to_file(path: &Path, world: &World) -> Result<()>` function: queries ECS for all relevant components, builds a `SaveStateV1`, calls `state.to_bytes()`, writes to disk. Per-game directory path is provided by the client via a new `IpcCommand::SaveGame { path: String }` variant. +- A `load_from_file(path: &Path, world: &mut World) -> Result<()>` function: reads bytes, calls `SaveStateV1::from_bytes`, re-spawns entities, injects `KnowledgeGraph`, `RelationshipGraph`, and `SimulationTime` as resources, reseeds the RNG. +- A `SaveCommand` and `LoadCommand` IPC message pair wired through `server/src/bridge/` — server receives save/load triggers from the client, executes, sends `SaveComplete`/`LoadComplete` response. +- Format version check on load: reject files with `format_version != SAVE_FORMAT_VERSION` with a clear error. + +Integration points: `server/src/simulation/save_state.rs` (data model), `server/src/bridge/types.rs` (new IPC commands), `server/src/bridge/local.rs` or `tcp.rs` (command dispatch), `server/src/knowledge/graph.rs` (KG re-injection), `server/src/npc/relationships.rs` (RelationshipGraph re-injection). + +Gotcha: ECS entity IDs are generational — do not save bevy `Entity` handles. `SaveStateV1` already uses `StableId(u64)` throughout. On load, re-spawn entities and re-register `StableId -> Entity` in `EntityRegistry`. + +### #96 — State serialization system + +Complement to #553. Where #553 handles whole-game ECS extraction, #96 implements the per-NPC serialization primitive for tier transitions. + +What this ticket must deliver: +- A `serialize_npc_to_frozen(entity: Entity, world: &World) -> NpcSaveState` function producing the frozen struct (~1-2 KB per NPC per D-026) +- A `deserialize_npc_from_frozen(state: &NpcSaveState, commands: &mut Commands)` that re-spawns a full NPC entity with the correct component set +- Used by the tier system when evicting to `StateSaved`: instead of keeping ECS components live, serialize to `NpcSaveState` and despawn. On reactivation: deserialize and re-spawn. +- Unit tests: serialize + deserialize produces an entity with identical component values + +Existing shape: `NpcSaveState` in `save_state.rs` captures position, `SecretSeverity`, `Relationships`, stress, tolerance, contentment, and optional `KnowledgeGraph`. Verify this covers all components needed for Background/Active reconstruction. Flag any missing axis (D-024) in a code comment for follow-up. + +### #97 — Timestamp-based eviction + +`server/src/simulation/tier.rs` has the tier marker components (`ActiveSim`, `BackgroundSim`, `StateSaved`) and the distance-based `update_tier_markers` system. What is missing: the LRU eviction when sim-space fills up. + +What this ticket must deliver: +- A `LastInteractionTick(u64)` component on all NPCs, updated whenever the player interacts with or observes an NPC +- A `SimSpacePressure` resource tracking current `ActiveSim` count vs. capacity (cap: 80 per D-026) +- An `evict_excess_active` system: when `ActiveSim` count exceeds capacity, demote the N oldest-by-`LastInteractionTick` entities to `BackgroundSim` (or `StateSaved` if beyond background radius) +- Uses a priority queue (BinaryHeap keyed by `LastInteractionTick`) for O(log N) eviction selection + +Gotcha: eviction must not demote entities with active scope tags (see #98). The eviction system runs after #98's `ScopeTag` check. + +### #98 — Scope tag system + +Scope tags are the mechanism by which certain NPCs stay pinned to `ActiveSim` regardless of distance or LRU pressure (D-026: "neighborhood, active-quest, colleague, known-contact"). + +What this ticket must deliver: +- A `ScopeTag` component (or enum-tagged component) with variants: `Neighborhood`, `ActiveQuest`, `Colleague`, `KnownContact` +- A `ScopePinned` marker component: attached to any NPC carrying a `ScopeTag`, removed when no scope tags remain +- The eviction system (#97) skips entities with `ScopePinned` +- Scope tags are assigned by gameplay systems: `Neighborhood` from proximity at session start, `KnownContact` from `KnowledgeGraph` entries with confidence >= `KnowsOf`, `Colleague` from `RelationshipGraph` edges with `Friend` or `Colleague` kind, `ActiveQuest` reserved for future quest system + +Integration: `server/src/simulation/tier.rs` (eviction exclusion), `server/src/knowledge/graph.rs` (KnownContact assignment trigger), `server/src/npc/relationships.rs` (Colleague assignment trigger). + +### #200 — Test module organization + +`server/src/test_world/` already exists with `constants.rs`, `invariants.rs`, `mod.rs`, `reset.rs`, and `rooms/`. This is the foundation. + +What this ticket must deliver: +- Establish the external test module pattern for the server crate: `#[cfg(test)] mod tests` in each module, plus a top-level `tests/` directory alongside `src/` for integration tests that run against the full simulation +- Document the three-layer test architecture (D-030): Layer 1 = fixture-based serialization (fast), Layer 2 = mock subprocess protocol state machine (medium), Layer 3 = real subprocess integration (slow) +- Create `tests/integration/mod.rs` as the entry point for Layer 3 tests +- Ensure `cargo test` in `server/` runs all layers correctly +- No-ops are fine for Layer 2 and 3 stubs — the important deliverable is the directory structure and entry points + +#272 is blocked by this ticket — the information boundary tests land in the new structure. + +### #272 — Information boundary negative test suite + +The core asymmetric information claim of the game: entity X cannot see what entity Y knows, unless the observation system explicitly grants it. + +What this ticket must deliver: +- A suite of negative tests asserting that information does NOT cross boundaries: + 1. Player's `KnowledgeGraph` does not contain NPC data that was not observed (no passive leakage) + 2. `ObserverSnapshot` for the player does not include entities outside LOS (fog of perception holds) + 3. Background-tier NPC `KnowledgeGraph` is not updated by Active-tier systems (tier boundary holds) + 4. `SaveStateV1` for one NPC does not serialize another NPC's `KnowledgeGraph` +- Uses `test_world/` for scenario setup — reuse existing helpers +- These tests live in Layer 1 (pure unit) and Layer 2 (mock world) of D-030 + +Gotcha: "negative tests" means asserting absence. Use `assert!(kg.entities.get(&id).is_none())` patterns — not just "test passed because nothing happened." + +### #555 — Protocol version handshake: server + +`server/src/bridge/types.rs` defines `PROTOCOL_VERSION: u8 = 14`. The version is already included in `ObserverSnapshot` as `pub version: u8`. + +What this ticket must deliver: +- Verify the first `ObserverSnapshot` emitted after subprocess startup includes `version: PROTOCOL_VERSION` +- Add a handshake phase: before normal tick loop begins, server emits a minimal `HandshakeMessage { protocol_version: PROTOCOL_VERSION }` as the very first framed message on the IPC channel +- Client reads this message and validates before sending any `PlayerInput` +- If the server receives a `PlayerInput` before completing handshake, log a warning and process normally (forward-compatible) +- Integration point: `server/src/bridge/local.rs` (startup sequence), `server/src/bridge/framing.rs` (message framing) + +Coordinate with ci team (#556) — the client-side validation is their ticket. + +## Dependency Chain + +``` +#555 (protocol handshake: server) → #556 (ci: protocol handshake: client) + +#200 (test module organization) → #272 (information boundary tests) + +#98 (scope tag system) → feeds into #97 (eviction respects scope pins) + +#256 (done: save state data model) → #553 (server ECS extraction) + #553 (server ECS extraction) → #554 (client: save/load UI) + +#96 (state serialization) → feeds into #553 (used during ECS extraction) + +Parallel starts: #555, #97, #98, #96, #200 — all unblocked week 1 +#553 starts after #96 is at review stage (needs serialize_npc_to_frozen) +#272 starts after #200 merges +``` + +## PR Workflow + +When ready to submit, create a PR with `tea` CLI. **All flags are required** to avoid TTY prompts (see CLAUDE.md "Gitea access" section): +```bash +tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(simulation): description" --description "body" --base main --head server +```