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 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 11:29:41 +01:00
co-authored by Claude Opus 4.6
parent 75e821ccd7
commit 0dd33690f7
4 changed files with 499 additions and 0 deletions
+122
View File
@@ -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 <id>` 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
```
+128
View File
@@ -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 <id>` 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 (`<timestamp>-<seed>/`) 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/<game-id>/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/<timestamp>-<seed>/` where game-id is created on New Game (#258). F5 quicksave writes to `user://saves/<game-id>/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 `<timestamp>-<seed>` (e.g. `20260225-143022-a7b3f1`)
- On "New Game": generate game-id (timestamp + RNG hex suffix), create `user://saves/<game-id>/` 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 <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
```
+97
View File
@@ -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/<game-id>/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/<timestamp>-<seed>/` 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 |
+152
View File
@@ -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 <id>` 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<NpcSaveState>`. 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
```