docs(workshops): complete Knowledge Graph & Information Boundaries workshop

Two-round workshop producing D-041 (Knowledge Graph Data Model):
- Round 1: independent analyses from Dudley, Gestalt, SI, Tyre, Paula
- Round 2: synthesis resolving debates + Gestalt mechanics validation

Key decisions:
- 4-level confidence hierarchy (Suspects < KnowsOf < KnowsDetails < Direct)
- BTreeMap for deterministic iteration (D-010 principle 4)
- Per-entity Component model, not centralized Resource
- StableEntityId + EntityRegistry for save/load stability (partial Q-019)
- Sprint 2 stub: structs + direct observation + basic decay (~6.5 dev-days)

Resolved Q-016 (knowledge hierarchy), raised Q-024/Q-025/Q-026.
Created tickets #361-#368 under epic #351, reconciled #49 children.
Updated sprint 2 briefings, agent briefings, and decision files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 23:24:37 +01:00
co-authored by Claude Opus 4.6
parent c065d98325
commit 93abd9b9d0
19 changed files with 6137 additions and 38 deletions
+19 -1
View File
@@ -114,6 +114,24 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **Raised by:** Tyre (technical proposal), Gestalt (day-phase design). Confirmed in Round 18 Gap Analysis Workshop with full team consensus.
- **Dissent:** None.
### D-041: Knowledge Graph Data Model
- **Date:** 2026-02-11
- **Decision:** The knowledge graph is a per-entity bevy_ecs Component with BTreeMap storage for deterministic iteration. Each entity that has knowledge (player character, Active-tier NPCs, Background-tier NPCs) gets a `KnowledgeGraph` component containing: (1) entity knowledge map: `BTreeMap<StableId, EntityKnowledge>`, (2) fact knowledge map: `BTreeMap<FactId, FactKnowledge>`. Knowledge confidence uses a 4-level hierarchy: `Suspects < KnowsOf < KnowsDetails < Direct`. Knowledge state tracks temporal/logical status: `Active` (believed true), `Contradicted` (conflicting information exists), `Stale` (aged beyond threshold). Knowledge source provides provenance per entry: `DirectObservation`, `Heard`, `ToldBy`, `Inferred`, `Background`. Stable entity IDs (`StableId(u64)`) replace bevy_ecs Entity handles in knowledge references, mapped via `EntityRegistry` resource for bidirectional `StableId <-> Entity` lookup. Knowledge updates flow through event-driven architecture: perception systems emit `KnowledgeEvent` to `KnowledgeEventQueue` resource, knowledge update system drains queue and writes to `KnowledgeGraph` components. Basic decay runs once per game-minute (every 10 ticks per D-031), downgrading confidence levels based on `last_observed_tick` age against configurable `DecayThresholds`.
- **Sprint 2 scope:** Full data structures + direct observation flow + basic decay + observer snapshot integration (#112). Deferred to Sprint 3+: NPC-to-NPC gossip, `ToldBy`/`Inferred` source generation, `Contradicted` state detection, `Stale` state logic, knowledge-driven dialogue filtering, monologue triggering, misinformation.
- **Canonical reference:** Full Rust struct definitions at `docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.md` Part 3 (lines 320-752). All implementation must conform to those types.
- **Key design choices:**
- **BTreeMap over HashMap:** D-010 principle 4 (deterministic simulation) requires stable iteration order. BTreeMap provides O(log N) lookups (~6-8 comparisons at N=50-200), deterministic serialization, and predictable replay behavior. HashMap iteration is non-deterministic and incompatible with deterministic replay (D-030 sub-decision #7).
- **Per-entity Component, not centralized Resource:** Enables `Changed<KnowledgeGraph>` dirty tracking, per-entity serialization for D-026 tier transitions, no shared mutable state, natural ECS query patterns.
- **4-level confidence hierarchy:** Resolves Q-016. `Suspects` = "something's off", gates initial investigation. `KnowsOf` = "X is involved in Y", gates topic-specific dialogue and peer-tier access (D-028). `KnowsDetails` = actionable detail, gates confrontation and secret-tier dialogue. `Direct` = currently in LOS, provides live position data and maximum rendering fidelity. Maps to D-028 access tiers: `surface` available at any level, `real` at KnowsOf+, `secret` at KnowsDetails+.
- **KnowledgeState for contradiction detection:** THE FRIEND arc (D-034, D-039 wow moment #3) requires detecting when a `ToldBy` entry conflicts with a `DirectObservation` entry. Both entries receive `Contradicted` state, triggering monologue event and relationship state shift (PersonOfInterest). Sprint 2 only uses `Active` state; contradiction detection ships Sprint 3.
- **StableId for knowledge references:** Partially resolves Q-019 for server-side and knowledge graph purposes. Knowledge graphs reference `StableId(u64)` that persists across save/load cycles, not bevy_ecs `Entity` (generational index). `EntityRegistry` maintains bidirectional mapping. Assigned once at entity creation, never changes. Client-side mapping (Godot StableId -> scene node) remains open.
- **Event-driven updates:** Phase 2 (perception) emits events. Phase 3 (knowledge) consumes events and writes graphs. Phase 4 (snapshot) reads graphs. Prevents mutable borrow conflicts in bevy_ecs.
- **Performance budget:** ~14 KB per NPC knowledge graph (50 entities + 20 facts). Active tier (80 NPCs) = ~1.1 MB. Background tier (2,000 NPCs, 10 entries each) = ~5 MB. Total live memory: ~6 MB. Knowledge lookups are O(log N) at N=50 (~100ns per query). Not on critical path (shadowcasting/spatial queries consume 10-20ms per tick, knowledge operations <3ms).
- **Resolves:** Q-016 (knowledge hierarchy). Partially resolves Q-019 (entity ID stability, server-side).
- **Blocks:** #352 (Observer Snapshot Pipeline Workshop)
- **Raised by:** Tyre (architecture synthesis), Dudley (implementation analysis), Gestalt (mechanics validation), Paula (narrative requirements). Workshop participants: Tyre, Dudley, Gestalt, Paula, SI. Source: Knowledge Graph & Information Boundaries Workshop (Epic #351), 2026-02-11.
- **Dissent:** None. Gestalt initially proposed dual-axis model (confidence + understanding) but validated that single-axis confidence with future content-driven understanding progression is architecturally sufficient. Dudley proposed HashMap; synthesis chose BTreeMap per D-010 principle 4 with Dudley's explicit acknowledgment ("iteration order is a determinism time bomb").
---
*8 decisions. Last updated: 2026-02-11*
*9 decisions. Last updated: 2026-02-11*
+33 -10
View File
@@ -77,10 +77,10 @@ Tracked questions awaiting discussion or resolution.
- **Source:** Content Gap Analysis Workshop (Ozzie R2, Gestalt R2)
### Q-016: Knowledge hierarchy for monologue prerequisites
- **Status:** Open
- **Question:** Monologue prerequisites currently use binary flags (`knows: smuggling_operation`). Need a hierarchy: `suspects` < `knows_of` < `knows_details`. How granular?
- **Assigned to:** Gestalt, Paula
- **Source:** Content Gap Analysis Workshop (Gestalt R2)
- **Status:** Resolved → [D-041](architecture.md#d-041-knowledge-graph-data-model)
- **Resolution:** 4-level hierarchy: `Suspects < KnowsOf < KnowsDetails < Direct`. Suspects = "something's off", gates initial investigation and vague monologue. KnowsOf = "X is involved in Y", gates topic-specific dialogue and peer-tier access. KnowsDetails = actionable detail, gates confrontation and secret-tier dialogue. Direct = currently in LOS, provides live position data. Maps to D-028 access tiers and D-035 prerequisite tags.
- **Date resolved:** 2026-02-11
- **Source:** Knowledge Graph & Information Boundaries Workshop
### Q-017: Triangle pressure threshold
- **Status:** Open
@@ -96,11 +96,12 @@ Tracked questions awaiting discussion or resolution.
- **Source:** Architecture Review Audit 2026-02-11
### Q-019: Entity ID stability strategy
- **Status:** Open
- **Question:** How are stable `entity_id: u64` values generated from bevy_ecs `Entity` handles? Must IDs be stable across save/load cycles? How does the client map entity IDs to scene nodes for entity lifecycle management (creation, updates, despawning)?
- **Context:** `VisibleEntity.entity_id` is a `u64` in the protocol (server/src/bridge/types.rs). bevy_ecs `Entity` is a generational index that may not be stable. Architecture review flagged this as MEDIUM severity gap (audit section 2.2).
- **Assigned to:** Tyre, Dudley
- **Source:** Architecture Review Audit 2026-02-11
- **Status:** Partially resolved → [D-041](architecture.md#d-041-knowledge-graph-data-model)
- **Resolution:** Server-side: `StableEntityId` component + `EntityRegistry` resource provides bidirectional `StableId(u64) <-> Entity` mapping. StableId assigned once at entity spawn, never changes, persists across save/load. Knowledge graphs reference StableId, not bevy Entity. Client-side mapping (Godot StableId -> scene node lifecycle) remains open.
- **Remaining:** Client-side entity lifecycle management, scene node mapping strategy.
- **Date partially resolved:** 2026-02-11
- **Assigned to:** Tyre, Dudley (client-side portion)
- **Source:** Knowledge Graph & Information Boundaries Workshop
### Q-020: Multi-entity collision resolution
- **Status:** Open
@@ -130,6 +131,28 @@ Tracked questions awaiting discussion or resolution.
- **Assigned to:** Tyre, Stig
- **Source:** Architecture Review Audit 2026-02-11
### Q-024: Gossip propagation timing
- **Status:** Open (preferred direction: queued)
- **Question:** When does NPC-to-NPC knowledge transfer occur? Two options: (1) Immediate during conversation — knowledge transfers instantly when two NPCs talk, more reactive but harder to predict. (2) Queued for routine intersection — knowledge transfers at scheduled meeting points (bar visits, shift changes), more predictable and player can exploit timing windows.
- **Preferred direction:** Queued approach aligns better with core gameplay. If gossip propagates at routine intersections, the player can observe NPCs meeting and predict information flow, time actions between intersection points to exploit windows of ignorance, and strategically attend/skip routine events to control what they overhear. Creates the "I need to be at the bar before shift change to see who talks to whom" mechanic. Immediate propagation makes gossip invisible and unpredictable; queued propagation makes it observable and exploitable, which is the design goal (D-010 principle 2, D-028 Layer 4).
- **Context:** Deferred to Sprint 3. Making this decision now without implementation experience would be premature. Gossip propagation is not in Sprint 2 scope (D-041).
- **Assigned to:** Gestalt, Tyre
- **Source:** Knowledge Graph & Information Boundaries Workshop (Gestalt Round 1)
### Q-025: Knowledge graph cap and eviction strategy
- **Status:** Open
- **Question:** At what point does an NPC's knowledge graph need entry eviction? What is the eviction policy? Options: MAX_ENTITY_KNOWLEDGE constant (e.g., 100 entities), LRU by `last_observed_tick`, lowest confidence first, hybrid approach. How are evicted entries handled — complete removal, or archival to "forgotten" state that can be refreshed?
- **Context:** Memory budget analysis shows ~14 KB per NPC with 50 entities + 20 facts. At 80 Active NPCs this is ~1.1 MB, well within budget. However, long-running sessions or NPCs with high interaction rates could accumulate entries. Eviction policy affects gameplay: forgetting low-confidence rumors creates natural information decay; forgetting old observations simulates memory limits.
- **Assigned to:** Tyre, Dudley
- **Source:** Knowledge Graph & Information Boundaries Workshop (Dudley Round 1, section 8.3)
### Q-026: Contradiction detection algorithm
- **Status:** Open
- **Question:** How exactly does the system detect that two knowledge entries contradict each other? Options: (1) Content-authored contradiction pairs (explicit markup in content files: "fact A contradicts fact B"), (2) Automatic same-subject different-value detection (algorithmic comparison of EntityKnowledge fields), (3) Hybrid (author-marked contradictions plus automatic location/state conflicts). How granular should contradiction detection be? Entity location only, or also attributes, relationships, facts?
- **Context:** THE FRIEND arc (D-034, D-039 wow moment #3) requires contradiction detection for emotional impact. The canonical example: Sera tells detective "Kael was at the dock during second shift" (ToldBy source), then detective observes Kael in corridor B-7 at that time (DirectObservation source). System must detect location contradiction, set both entries to Contradicted state, emit monologue event, and shift relationship to PersonOfInterest.
- **Assigned to:** Gestalt, Paula, Dudley
- **Source:** Knowledge Graph & Information Boundaries Workshop (Gestalt/Paula Round 1)
---
*23 questions (3 resolved, 2 partially resolved, 18 open). Last updated: 2026-02-11*
*26 questions (3 resolved, 2 partially resolved, 21 open). Last updated: 2026-02-11*
+3 -2
View File
@@ -2,7 +2,7 @@
Last updated: 2026-02-11
## Current Project State
The Settled Reach: top-down immersive sim, single-character perspective, asymmetric information core mechanic, Rimworld-style storyteller. 40 confirmed decisions, 17 discussion rounds + 1 workshop complete. Engine selected (D-020: Rust/bevy_ecs server). Vertical slice defined (D-027). v0.1 Content Gap Analysis Workshop complete.
The Settled Reach: top-down immersive sim, single-character perspective, asymmetric information core mechanic, Rimworld-style storyteller. 41 confirmed decisions, 17 discussion rounds + 2 workshops complete. Engine selected (D-020: Rust/bevy_ecs server). Vertical slice defined (D-027). v0.1 Content Gap Analysis Workshop complete. Knowledge Graph & Information Boundaries Workshop complete (D-041).
## Status
STANDBY. This briefing will be populated when backend/engine implementation begins.
@@ -27,10 +27,11 @@ See `docs/DEVOPS.md` for full procedures. Your key targets:
- Server code lives in `server/` — unit tests inline with `#[cfg(test)]`, integration tests in `server/tests/`
- Cross-boundary IPC fixtures in `tests/`
### Workshop Decisions (Content Gap Analysis — relevant to server)
### Workshop Decisions (relevant to server implementation)
- **D-034: THE FRIEND ECS spec** — 14 ECS components defined for production-level NPCs (NpcIdentity, Want, Secret, Relationships[3], ToleranceThreshold, DailyRoutine, InformationInventory, Contentment, PersonalityTraits, TellState, SkillSet, AccessTierMap, TriangleMembership, MoodState)
- **D-035: Converged tag taxonomy (6+3)** — dialogue selection pipeline: access (hard filter) → situation (context filter) → trust (hard filter) → mood+topic (weighted selection). Server implements pipeline.
- **D-038: Audio architecture** — event-driven: SoundEventEmitter → ObserverSnapshot → SoundRenderer. Server emits sound events.
- **D-041: Knowledge Graph Data Model** — Per-entity `KnowledgeGraph` Component with BTreeMap storage (deterministic). 4-level confidence hierarchy (Suspects/KnowsOf/KnowsDetails/Direct). StableEntityId + EntityRegistry for stable entity references. Event-driven updates via KnowledgeEventQueue. Sprint 2 scope: data structures + direct observation + basic decay + observer snapshot integration (#112). Canonical Rust structs at `docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.md` Part 3.
## Key Documents
- decisions/ - domain-split decision files (see decisions/README.md for index)
+6 -3
View File
@@ -2,7 +2,7 @@
Last updated: 2026-02-11
## Current Project State
The Settled Reach: top-down immersive sim, single-character perspective, asymmetric information core mechanic, Rimworld-style storyteller. 40 confirmed decisions, 17 discussion rounds + 1 workshop complete. Engine selected (D-020). Vertical slice defined (D-027). v0.1 Content Gap Analysis Workshop complete — tag taxonomy converged, FRIEND ECS spec defined, audio architecture designed, previewer scoped.
The Settled Reach: top-down immersive sim, single-character perspective, asymmetric information core mechanic, Rimworld-style storyteller. 41 confirmed decisions, 17 discussion rounds + 2 workshops complete. Engine selected (D-020). Vertical slice defined (D-027). v0.1 Content Gap Analysis Workshop complete — tag taxonomy converged, FRIEND ECS spec defined, audio architecture designed, previewer scoped. Knowledge Graph & Information Boundaries Workshop complete (D-041) — Q-016 resolved.
## Status
**ACTIVE.** Systems specification work unblocked. Tag taxonomy needs formalization as standalone document. Knowledge state vocabulary blocks Mellanie's monologue authoring. 17 content-supporting systems identified, 0 built — significant server-side work ahead.
@@ -34,6 +34,7 @@ The Settled Reach: top-down immersive sim, single-character perspective, asymmet
- **D-033: Entity color** — client-side derivation from RelationshipState + knowledge state. Not an NPC property.
- **D-038: Audio architecture** — event-driven: SoundEventEmitter → ObserverSnapshot → SoundRenderer with AudioAssetRegistry + VisualSoundIndicator + AmbientManager. Events first, assets second.
- **D-039: 6 wow moments** — systems must support all 6 (observation triggers, FRIEND contradiction staging, divergence reveal, ticker display, idle monologue).
- **D-041: Knowledge Graph Data Model** — 4-level confidence hierarchy: Suspects < KnowsOf < KnowsDetails < Direct. Resolves Q-016. Maps to D-028 access tiers and D-035 prerequisite tags. Per-entity KnowledgeGraph Component with BTreeMap storage. KnowledgeState enum supports contradiction detection for THE FRIEND arc (D-034). Sprint 2 scope: data structures + direct observation + basic decay.
## Open Questions Assigned to You
- Q-002: v0.1 scope definition (co-lead)
@@ -44,14 +45,16 @@ The Settled Reach: top-down immersive sim, single-character perspective, asymmet
- Q-013: Line previewer temporal progression (co-lead with Dudley)
- Q-014: Audio timing with monologue chime (co-lead with Ozzie)
- Q-015: Generation expansion for FRIEND content (co-lead with Mellanie)
- Q-016: Knowledge hierarchy for prerequisites (co-lead with Paula)
- Q-016: Knowledge hierarchy for prerequisites **RESOLVED** (D-041)
- Q-017: Triangle pressure threshold (co-lead with Paula)
- Q-024: Gossip propagation timing (co-lead with Tyre) — preferred direction: queued
- Q-026: Contradiction detection algorithm (co-lead with Paula, Dudley)
## Current Priorities
**Systems specification → server implementation support → previewer**
1. **Lock tag taxonomy + line pool format** — formalize Round 2 converged schema as standalone spec document. Reference for all content authors.
2. **Knowledge state vocabulary**define v0.1 knowledge flags (e.g., `knows: smuggling_operation`, `met: kael_davan`). Blocks Mellanie's monologue prerequisite authoring. Co-lead with Dudley.
2. **Knowledge state vocabulary****UNBLOCKED** by D-041. Use 4-level confidence hierarchy (suspects/knows_of/knows_details) plus FactId format (`category.topic`). See D-041 Appendix A for v0.1 vocabulary and prerequisite format.
3. **Dialogue selection pipeline specification** — formalize the 4-layer filter algorithm for server implementation.
4. **Sound event system architecture** — formalize Round 2 section 6 for Dudley.
5. **Line previewer (#193) MVP scope** — 7 must-have features. Build as first implementation of dialogue pipeline (shares 80% code). Rust CLI.
+5 -2
View File
@@ -2,7 +2,7 @@
Last updated: 2026-02-11
## Current Project State
The Settled Reach: top-down immersive sim, single-character perspective, asymmetric information core mechanic, Rimworld-style storyteller. 40 confirmed decisions, 17 discussion rounds + 1 workshop complete. Engine selected (D-020). Vertical slice defined (D-027). v0.1 Content Gap Analysis Workshop complete — NPC web reconciled, THE FRIEND designed, dependency chain established.
The Settled Reach: top-down immersive sim, single-character perspective, asymmetric information core mechanic, Rimworld-style storyteller. 41 confirmed decisions, 17 discussion rounds + 2 workshops complete. Engine selected (D-020). Vertical slice defined (D-027). v0.1 Content Gap Analysis Workshop complete — NPC web reconciled, THE FRIEND designed, dependency chain established. Knowledge Graph & Information Boundaries Workshop complete (D-041) — knowledge vocabulary unblocked for content authoring (#309).
## Status
**ACTIVE.** Critical path owner. The Dual Lens Authoring Guide (#261) is the single biggest blocker in the project — everything downstream depends on it.
@@ -27,6 +27,7 @@ The Settled Reach: top-down immersive sim, single-character perspective, asymmet
- **D-036: Sova Transit District** — 15 NPCs with Krenn System names (your Round 2 web is canonical)
- **D-037: Contraband = unlicensed lattice components** — smuggling access, not weapons (moral ambiguity confirmed)
- **D-039: All 6 wow moments in scope** — THE FRIEND contradiction is wow moment #3
- **D-041: Knowledge Graph Data Model** — 4-level confidence hierarchy resolves Q-016. Knowledge vocabulary for content authoring (#309) defined at synthesis Appendix A. Prerequisite format: `knowledge: [{subject, flag, level}]` where level = suspects/knows_of/knows_details. FactId format: `category.topic`. Supports THE FRIEND contradiction detection (Contradicted state, monologue trigger, relationship shift to PersonOfInterest).
## Your Relationship Web (Canonical — Round 2)
15 NPCs, 5 triangles:
@@ -40,8 +41,9 @@ Distribution: 3 flat (Pael, Ren, Tev), 7 mundane triangle, 4-5 entangled.
## Open Questions Assigned to You
- Q-011: Character selection roster for prototype (co-lead with Miri)
- Q-016: Knowledge hierarchy for monologue prerequisites (co-lead with Gestalt)
- Q-016: Knowledge hierarchy for monologue prerequisites **RESOLVED** (D-041)
- Q-017: Triangle pressure threshold (co-lead with Gestalt)
- Q-026: Contradiction detection algorithm (co-lead with Gestalt, Dudley)
## Current Priorities
**Critical path: Dual Lens Guide → Voice Patterns → THE FRIEND profiles → NPC profiles → Review**
@@ -49,6 +51,7 @@ Distribution: 3 flat (Pael, Ren, Tev), 7 mundane triangle, 4-5 entangled.
1. **Dual Lens Authoring Guide (#261) — full draft** (blocks everything downstream)
- Refined outline from workshop: 6 parts including THE FRIEND section
- Must include: emotional divergence moments, access tier shift paths, authoring checklist with display context tags
- **Knowledge prerequisite format** now available: see D-041 Appendix A for v0.1 vocabulary and prerequisite syntax
2. **THE FRIEND: Kael Davan — full Tier 1 profile** (needs DLG)
3. **THE FRIEND: Sera Venn — full Tier 1 profile** (needs DLG)
4. **Character voice speech patterns** (co-deliver with Mellanie)
+11 -3
View File
@@ -2,7 +2,7 @@
Last updated: 2026-02-11
## Current Project State
The Settled Reach: top-down immersive sim, single-character perspective, asymmetric information core mechanic, Rimworld-style storyteller. 40 confirmed decisions, 17 discussion rounds + 1 workshop complete. Engine selected (D-020). Vertical slice defined (D-027). v0.1 Content Gap Analysis Workshop complete — ~50 tickets identified across 7 categories.
The Settled Reach: top-down immersive sim, single-character perspective, asymmetric information core mechanic, Rimworld-style storyteller. 41 confirmed decisions, 17 discussion rounds + 2 workshops complete. Engine selected (D-020). Vertical slice defined (D-027). v0.1 Content Gap Analysis Workshop complete — ~50 tickets identified across 7 categories. Knowledge Graph & Information Boundaries Workshop complete (D-041) — Sprint 2 ticket impact requires reconciliation with existing #49 epic children.
## Decisions Relevant to Your Role
All decisions across all domains. Read `decisions/README.md` for the index. Query decisions DB for sprint planning:
@@ -16,16 +16,24 @@ make decisions-active # List all active decisions
All open questions - tracks as tickets for assignment and progress monitoring.
## Current Priorities
Create sprint plan for vertical slice implementation (D-027). Break D-024 through D-040 into tickets. Workshop output (`docs/workshops/content-gap-analysis_v0_1/SUMMARY.md`) contains ~50 deduplicated ticket proposals across 7 categories. Track dependencies:
Create sprint plan for vertical slice implementation (D-027). Break D-024 through D-041 into tickets. Workshop output (`docs/workshops/content-gap-analysis_v0_1/SUMMARY.md`) contains ~50 deduplicated ticket proposals across 7 categories. Track dependencies:
- Paula's Dual Lens Authoring Guide (#261) — critical path, blocks all downstream content
- Miri's contraband spec + Sova texture appendix (blocks Mellanie)
- Gestalt's tag taxonomy spec + knowledge state vocabulary (blocks content authoring)
- Gestalt's tag taxonomy spec + knowledge state vocabulary **UNBLOCKED by D-041** (Appendix A provides v0.1 vocabulary)
- Hoshe's test plans (must precede implementation)
- NPC ECS model (blocks template instantiation)
- Template system (blocks dialogue system)
- **Knowledge graph system (D-041)** — Sprint 2 priority, blocks #112, #358, #356. Requires ticket reconciliation with #49 epic.
Vertical slice implementation + content authoring are now parallel workstreams — coordinate with Team Leader on sequencing and resourcing.
### Knowledge Graph Workshop Ticket Impact (D-041)
See synthesis Part 8 for full details. Key actions:
1. **Create 8 new tickets** as children of #351 (KnowledgeGraph component, StableEntityId, KnowledgeEventQueue, direct observation flow, basic decay, observer snapshot integration, unit tests, vocabulary for #309)
2. **Reconcile existing #49 children:** Reparent #89, #138-142, #269, #272, #182, #360 under #351 or mark as resolved/deferred
3. **Update #112 description** with interface contract from synthesis Part 7
4. **Update sprint 2 dependencies:** KnowledgeGraph blocks #112, StableEntityId blocks #360, observer snapshot integration blocks #358/#356
## Development Workflow
See `docs/DEVOPS.md` for full procedures. Key awareness for sprint planning:
- `make ci` must pass before any PR merges — definition of done includes CI green
+12 -6
View File
@@ -2,7 +2,7 @@
Last updated: 2026-02-11
## Current Project State
The Settled Reach: top-down immersive sim, single-character perspective, asymmetric information core mechanic, Rimworld-style storyteller. 40 confirmed decisions, 17 discussion rounds + 1 workshop complete. Engine selected (D-020: Godot + Rust/bevy_ecs subprocess). Vertical slice defined (D-027). v0.1 Content Gap Analysis Workshop complete.
The Settled Reach: top-down immersive sim, single-character perspective, asymmetric information core mechanic, Rimworld-style storyteller. 41 confirmed decisions, 17 discussion rounds + 2 workshops complete. Engine selected (D-020: Godot + Rust/bevy_ecs subprocess). Vertical slice defined (D-027). v0.1 Content Gap Analysis Workshop complete. Knowledge Graph & Information Boundaries Workshop complete (D-041) — architectural foundation for information boundaries system defined.
## Decisions Relevant to Your Role
Read `decisions/architecture.md` (primary) and `decisions/perception.md` (secondary). Key decisions:
@@ -11,21 +11,27 @@ Read `decisions/architecture.md` (primary) and `decisions/perception.md` (second
- **D-026:** Simulation tier budgets (Active 30-80 NPCs, Background 500-2K, State-saved 10K+)
- **D-030:** Testability architecture (hybrid test org, gdUnit4, CauseChain, three-layer IPC testing)
- **D-031:** Time system (10 ticks = 1 game-minute, 4 day phases)
- **D-041:** Knowledge Graph Data Model — **BTreeMap mandate** (not HashMap) per D-010 principle 4. Per-entity KnowledgeGraph Component. 4-level confidence hierarchy. StableEntityId + EntityRegistry for stable entity references. Event-driven updates. Performance budget: ~6 MB live memory for 80 Active + 2K Background NPCs. Canonical Rust structs at `docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.md` Part 3. Sprint 2 deliverable: ~6.5 developer-days.
## Open Questions Assigned to You
- ~~Q-001: Engine selection~~ — **Resolved** as D-020 (Godot 4 + Rust/bevy_ecs via subprocess/IPC)
- Q-002: v0.1 scope definition (co-lead)
- Q-005: Prototype scale (co-lead with Gestalt, Miri)
- Q-007: Target platforms
- Q-021: Tick budget overflow policy (co-lead with Dudley)
- Q-022: NPC pathfinding cache eviction (co-lead with Dudley)
- Q-024: Gossip propagation timing (co-lead with Gestalt) — preferred direction: queued
- Q-025: Knowledge graph cap and eviction strategy (co-lead with Dudley)
## Current Priorities
Vertical slice technical implementation:
1. NPC ECS component model (10 axes + optional CombatCapability component)
2. Template instantiation system (single-ownership + reference links with metadata)
3. Simulation tier transitions (timestamp LRU + scope tags + bevy_ecs component add/remove)
4. Access tier system (content filtering by tier 0-3)
5. Basic cargo/goods system (for smuggling ring mechanics)
6. Line previewer CLI (weekend build — YAML templates → trait-modified line pools)
2. **Knowledge Graph Data Model (D-041)** — Sprint 2 priority. Implement canonical Rust structs from workshop synthesis. BTreeMap for deterministic iteration (non-negotiable). StableEntityId + EntityRegistry. KnowledgeEventQueue. Direct observation flow. Basic decay. Observer snapshot integration (#112). ~6.5 developer-days. Blocks #352 (Observer Snapshot Pipeline Workshop).
3. Template instantiation system (single-ownership + reference links with metadata)
4. Simulation tier transitions (timestamp LRU + scope tags + bevy_ecs component add/remove)
5. Access tier system (content filtering by tier 0-3)
6. Basic cargo/goods system (for smuggling ring mechanics)
7. Line previewer CLI (weekend build — YAML templates → trait-modified line pools)
**Future flag:** Base builder DLC requires data model stub now: `modifications: Vec<Modification>` on relevant entities. Add to schema early.
+2 -1
View File
@@ -21,4 +21,5 @@ Historical discussion rounds from the Commonwealth game design process.
| 15 | README / Steam Page Placeholder | - | [round-15](round-15-readme-steam-page.md) |
| 16 | Factions — Building a Fuller Narrative Picture | AWAITING INPUT | [complete](round-16-factions-complete.md), [lore](round-16-factions.md), [mechanics](round-16-faction-mechanics.md), [notes](round-16-session-notes.md) |
| 17 | Content Architecture | D-023, D-024, D-025, D-026, D-027, D-028, D-029 | [round-17](round-17-content-architecture.md) |
| 18 | v0.1 Gap Analysis Workshop | D-030, D-031 | [round-18](round-18-v01-gap-analysis-workshop.md) |
| 18 | v0.1 Gap Analysis Workshop | D-030, D-031, D-032, D-033, D-034, D-035, D-036, D-037, D-038, D-039, D-040 | [round-18](round-18-v01-gap-analysis-workshop.md) |
| 19 | Knowledge Graph & Information Boundaries Workshop | D-041, Q-016 resolved, Q-019 partially resolved, Q-024, Q-025, Q-026 | [workshop brief](../workshops/knowledge-graph-information-boundaries/workshop-brief.md), [synthesis](../workshops/knowledge-graph-information-boundaries/round2-synthesis.md) |
+3 -3
View File
@@ -11,7 +11,7 @@
|---|-------|------------|
| #116 | Camera lock to character | — |
| #129 | Tile rendering engine | — |
| #130 | Entity sprite management | |
| #130 | Entity sprite management | #360 (entity ID stability, partially resolved by #362) |
| #131 | Fog overlay rendering | #129 (needs tile layer to overlay) |
| #113 | Fog rendering - client | #131 (rendering layer), server #112 (fog data in snapshot) |
@@ -20,14 +20,14 @@ Use `db/connectors/ticket show <id>` for full details.
## Key Decisions
- `decisions/perception.md` — D-015 (camera locked, no panning), D-011 (fog of perception), D-019 (top-down camera)
- `decisions/architecture.md` — D-020 (ObserverSnapshot drives all rendering)
- `decisions/architecture.md` — D-020 (ObserverSnapshot drives all rendering), D-041 (Knowledge Graph — entity color from relationship state)
- `decisions/scope.md` — D-014 (v0.1 map spec: ~150x150, 2-3 z-levels, fog + LOS)
## Notes
- **#116:** Camera follows player character position. No panning, no rotation (v0.1). The `GameState.player_position` already tracks position — camera just needs to center on it smoothly. Use `Camera2D` with smoothing. Existing `main.gd` handles input; camera attaches to the player entity's position.
- **#129:** Multi-layer tilemap rendering from snapshot data. Currently the client renders entities as `ColorRect` placeholders (`client/scripts/rendering/entity_renderer.gd`). This ticket adds a `TileMapLayer` (or multiple layers) for floor/walls/objects. Tiles come from the `ObserverSnapshot` — the client draws what the server says is visible. Placeholder art: colored rectangles with labels (D-014: "functional boxes with labels").
- **#130:** Upgrade entity rendering from `ColorRect` to proper sprite management. Animation states (idle, walk). Entity color based on relationship state (D-033: unknown=teal, known=green, POI=amber, hostile=red). Facing direction indicator. Builds on existing `entity_renderer.gd`.
- **#130:** Upgrade entity rendering from `ColorRect` to proper sprite management. Animation states (idle, walk). Entity color based on relationship state from knowledge graph (D-033: unknown=teal, known=green, POI=amber, hostile=red; D-041: KnowledgeGraph drives relationship color). Facing direction indicator. Remembered (not-visible) entities render as ghosts. Builds on existing `entity_renderer.gd`.
- **#131:** Fog overlay on the tile layer. Three visibility states: visible (clear), fog-edge (dimmed), hidden (dark/black). Driven by `visible_tiles` data from `ObserverSnapshot`. This is the rendering half of the fog system — the server (#112) determines what's visible, the client draws the fog.
- **#113:** Integration of server fog data into the client fog renderer. The existing `fog_renderer.gd` is a stub. This ticket connects it to actual visibility data from the snapshot. Fog returns when you leave an area (time-based decay, tracked client-side from last-seen tick).
- **WorldRenderer:** The existing `world_renderer.gd` orchestrates entity + fog rendering. It already calls `entity_renderer.update_entities()` and `fog_renderer.update_fog()` — the stubs just need real implementations.
+19 -3
View File
@@ -9,22 +9,34 @@
| # | Title | Owner | Blocks |
|---|-------|-------|--------|
| #359 | Resolve Q-018: shadowcasting algorithm selection | server (Tyre, Dudley) | #110 |
| #360 | Resolve Q-019: entity ID stability | joint (Tyre, Dudley) | #130 |
| #360 | Resolve Q-019: entity ID stability | joint (Tyre, Dudley) | #130 (partially resolved by #362) |
| #358 | Design ObserverSnapshot v2 schema | joint (Tyre) | #112, #113, #25 |
## Knowledge Graph Integration Tickets (from Workshop #351, D-041)
| # | Title | Priority | Est. | Blocks |
|---|-------|----------|------|--------|
| #366 | Observer snapshot knowledge integration | critical | 1d | #356 |
| #368 | Knowledge vocabulary for v0.1 content | high | 0.5d | #309 |
**#366** integrates KnowledgeGraph queries into ObserverSnapshot: entity color from relationship state + remembered entities as ghosts. Assign to Oscar to avoid blocking Dudley on server critical path.
**#368** provides the content authoring vocabulary from D-041 Appendix A, unblocking Mellanie on #309.
## Integration Tickets
| # | Title | Blocked by |
|---|-------|------------|
| #356 | Fog data through bridge | #112, #113 |
| #356 | Fog data through bridge | #112, #113, #366 |
| #357 | Sprint 2 proof: fog of perception | #356, #116, #25 |
Use `db/connectors/ticket show <id>` for full details.
## Key Decisions
- `decisions/architecture.md` — D-020 (IPC protocol, ObserverSnapshot), D-010 (information boundaries)
- `decisions/architecture.md` — D-020 (IPC protocol, ObserverSnapshot), D-010 (information boundaries), D-041 (Knowledge Graph Data Model)
- `decisions/perception.md` — D-011 (fog non-negotiable), D-015 (vision cone)
- `docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.md` — canonical reference for D-041
## Sprint Completion Proof
@@ -37,6 +49,10 @@ Use `db/connectors/ticket show <id>` for full details.
5. Fog covers areas outside the vision cone
6. Walking behind a wall hides what's on the other side
7. Walking around a corner reveals what's there
8. Entity color reflects relationship state from knowledge graph (#361, #366)
9. Remembered (not visible) entities appear as ghosts with faded color (#361, #366)
**Knowledge graph proof:** Observe an NPC, walk away, return. The NPC appears as a ghost at last-known position while not in LOS. Color shifts based on relationship state (green = known, amber = flagged).
This is the first moment the game *feels* like an immersive sim — you can't see behind walls, and that constraint IS the game.
+23 -4
View File
@@ -18,15 +18,29 @@
|---|-------|------------|
| #110 | Shadowcasting algorithm - server | #359 (algorithm decision) |
| #111 | Vision cone implementation | #110 |
| #112 | Observer visibility query | #111, #358 (snapshot schema) |
| #112 | Observer visibility query | #111, #358, #361, #362 |
| #25 | Game clock and day-phase system | #358 (snapshot schema) |
## Knowledge Graph Tickets (from Workshop #351, D-041)
| # | Title | Priority | Est. | Blocked by |
|---|-------|----------|------|------------|
| #361 | Implement KnowledgeGraph component + types | critical | 1d | — |
| #362 | StableEntityId + EntityRegistry resource | critical | 1d | — |
| #363 | KnowledgeEventQueue + processing system | high | 0.5d | — |
| #364 | Direct observation knowledge flow | critical | 0.5d | — |
| #365 | Basic knowledge decay system | high | 0.5d | — |
| #367 | Knowledge graph unit test suite | high | 1d | — |
**Start #361 and #362 on day 1.** Both block #112 (observer visibility query). See `decisions/architecture.md` D-041 for canonical struct definitions.
Use `db/connectors/ticket show <id>` for full details.
## Key Decisions
- `decisions/perception.md` — D-011 (fog non-negotiable), D-015 (locked camera + vision cone), D-018 (three-range sound model)
- `decisions/architecture.md` — D-010 (information boundaries), D-020 (ObserverSnapshot), D-031 (time system)
- `decisions/architecture.md` — D-010 (information boundaries), D-020 (ObserverSnapshot), D-031 (time system), D-041 (Knowledge Graph Data Model)
- `docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.md` — canonical reference for knowledge graph structs and Sprint 2 scope
## Open Questions to Resolve Early
@@ -46,9 +60,14 @@ Use `db/connectors/ticket show <id>` for full details.
## Dependency Chain
```
#359 (Q-018) → #110 (shadowcast) → #111 (vision cone) → #112 (observer query)
#358 (snapshot v2) → #112, #113, #25
#359 (Q-018) → #110 (shadowcast) → #111 (vision cone) ──┐
#361 (KnowledgeGraph) ─┬─→ #112 (observer query) → ... │
#362 (StableEntityId) ─┘ │
#358 (snapshot v2) → #112, #113, #25 ────────────────────┘
#25 (time) → parallel track, blocked by #358
Knowledge graph parallel track:
#361, #362 (day 1-3, no blockers) → #363, #364 (day 3-4) → #365, #367 (parallel)
```
## PR Workflow
@@ -0,0 +1,841 @@
# Knowledge Graph & Information Boundaries — Round 1 Implementation Analysis
**Author:** Dudley (Server Implementation Specialist)
**Date:** 2026-02-11
**Workshop:** Knowledge Graph & Information Boundaries
**Focus:** Implementation practicality, ECS integration, performance, buildability
---
## Executive Summary
The simulation guarantees deterministic knowledge state, but the current `InformationInventory { known_facts: Vec<String> }` is a placeholder that cannot support the design requirements. I need to verify the data structure before committing to an implementation path.
**Key findings:**
1. **ECS Integration:** Knowledge graph should be a per-entity Component, not a centralized Resource — fits bevy_ecs model and supports tier serialization
2. **Stable entity references:** Wire-format `u64` entity IDs solve the save/load stability problem — already in the protocol
3. **Performance at scale:** 80 Active NPCs × 50 known entities = 4,000 knowledge entries — feasible with proper indexing
4. **Complexity landmine:** Knowledge decay implementation has hidden state explosion — needs batching
5. **Simplest buildable path:** Start with HashMap-based graph per entity, add spatial queries later
**Critical dependencies:**
- Q-019 (entity ID stability) must be resolved before knowledge references work
- Q-016 (knowledge hierarchy) blocks the KnowledgeConfidence enum design
- Spatial partitioning (for observer queries) is a prerequisite
---
## 1. ECS Integration (Questions 1-4)
### 1.1 Component vs Resource: Knowledge Graph Placement
**Recommendation: Per-entity Component.**
```rust
#[derive(Component, Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeGraph {
/// What this entity knows about other entities
/// Key: wire-format entity_id (u64), not bevy Entity
pub entity_knowledge: HashMap<u64, EntityKnowledge>,
/// Non-entity facts (location observations, overheard events)
pub world_facts: Vec<WorldFact>,
/// Last decay pass (for batch processing)
pub last_decay_tick: u64,
}
```
**Why Component, not Resource?**
- Each entity has its own knowledge state — natural fit for Component model
- Tier serialization (D-026) requires per-entity serialization — Components serialize with their entity bundle
- bevy_ecs `Changed<KnowledgeGraph>` queries enable efficient snapshot generation (only entities with knowledge updates)
- No lock contention — each entity's knowledge is independent
- Background tier NPCs can have simplified knowledge (fewer entries) without affecting Active tier
**Why not Resource?**
- Centralized `HashMap<Entity, KnowledgeGraph>` requires lock for every knowledge update
- Harder to serialize for tier transitions (must extract per-entity subgraphs)
- Loses bevy_ecs change detection benefits
### 1.2 Migration Path from InformationInventory
Current state (server/src/npc/mod.rs line 47):
```rust
#[derive(Component, Debug, Clone, Serialize, Deserialize)]
pub struct InformationInventory {
pub known_facts: Vec<String>,
}
```
**Migration strategy:**
1. Keep `InformationInventory` as deprecated component for v0.1 compatibility
2. Add `KnowledgeGraph` component to new NPCs
3. Add migration system that runs once at spawn:
```rust
fn migrate_information_inventory(
mut commands: Commands,
query: Query<(Entity, &InformationInventory), Without<KnowledgeGraph>>,
) {
for (entity, inventory) in query.iter() {
// Parse string facts into structured knowledge
let graph = KnowledgeGraph::from_string_facts(&inventory.known_facts);
commands.entity(entity).insert(graph);
}
}
```
4. Remove `InformationInventory` after migration complete
**v0.1 scope:** Only `KnowledgeGraph`. No migration — fresh world generation.
### 1.3 Concrete Rust Struct Proposal
```rust
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
/// Knowledge this entity has about another entity
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityKnowledge {
/// Wire-format ID of the known entity (stable across save/load)
pub subject_id: u64,
/// Last observed position (if known)
pub last_seen_position: Option<(f32, f32, i32)>,
/// Tick when last observed
pub last_seen_tick: u64,
/// Confidence/hierarchy level (Q-016)
pub confidence: KnowledgeConfidence,
/// How this knowledge was acquired
pub source: KnowledgeSource,
/// Known facts about this entity's state
pub facts: Vec<KnownFact>,
}
/// Knowledge confidence hierarchy (resolves Q-016)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum KnowledgeConfidence {
/// "I think someone might be involved" (lowest)
Suspects = 0,
/// "I know this person exists and their basic role"
KnowsOf = 1,
/// "I know specific details about their activities"
KnowsDetails = 2,
/// "I directly observed this" (highest)
DirectObservation = 3,
}
/// How knowledge was acquired (affects trust/decay)
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum KnowledgeSource {
DirectObservation,
Told { by_entity_id: u64, trust: f32 },
Inferred { confidence: f32 },
Overheard,
}
/// A specific fact about an entity
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnownFact {
pub fact_type: FactType,
pub learned_tick: u64,
pub confidence: f32, // 0.0-1.0
}
/// Types of facts an entity can know
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FactType {
/// Entity's current activity
CurrentActivity(String),
/// Entity's routine
UsualRoutine(String),
/// Entity's relationship to another
Relationship { with: u64, kind: String },
/// Entity's involvement in something
Involvement(String),
/// Entity's location habits
FrequentsLocation { x: i32, y: i32, z: i32 },
}
/// Knowledge about locations/events (not tied to entities)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorldFact {
pub description: String,
pub location: Option<(i32, i32, i32)>,
pub learned_tick: u64,
pub confidence: f32,
}
```
**Key design choices:**
- **`subject_id: u64`** uses wire-format IDs (stable), not bevy `Entity` (unstable across save/load)
- **`KnowledgeConfidence` as enum with `Ord`** enables hierarchy queries (`>= KnowsOf`)
- **`KnowledgeSource` carries context** (who told me? how much do I trust them?)
- **`confidence: f32`** enables gradual decay (not binary)
- **`FactType` enum** is extensible without breaking serialization
### 1.4 Entity ID Stability Problem (Q-019)
**The problem:** bevy_ecs `Entity` is a generational index (e.g., `Entity { index: 42, generation: 3 }`). Across save/load, entity IDs can change. Knowledge graph references would break.
**Solution (already in protocol):** Wire-format `u64` entity IDs in `VisibleEntity` (server/src/bridge/types.rs line 26). The simulation needs a bidirectional mapping:
```rust
#[derive(Resource, Debug, Default)]
pub struct EntityIdMap {
/// bevy Entity -> stable u64 ID
to_wire: HashMap<Entity, u64>,
/// stable u64 ID -> bevy Entity
from_wire: HashMap<u64, Entity>,
/// Next ID to assign
next_id: u64,
}
impl EntityIdMap {
pub fn register(&mut self, entity: Entity) -> u64 {
if let Some(id) = self.to_wire.get(&entity) {
return *id;
}
let id = self.next_id;
self.next_id += 1;
self.to_wire.insert(entity, id);
self.from_wire.insert(id, entity);
id
}
pub fn get_wire_id(&self, entity: Entity) -> Option<u64> {
self.to_wire.get(&entity).copied()
}
pub fn get_entity(&self, wire_id: u64) -> Option<Entity> {
self.from_wire.get(&wire_id).copied()
}
}
```
**Save/Load strategy:**
- Save: `EntityIdMap.next_id` + `to_wire` mapping serialize with World
- Load: Restore mapping, bevy Entities get new indices but same wire IDs
- Knowledge graph references stay valid (use wire IDs, not Entity handles)
**This resolves Q-019.** Stable IDs = monotonic counter, mapped to bevy entities at runtime.
---
## 2. Implementation Practicalities
### 2.1 Data Structure Choice
**Options evaluated:**
| Structure | Lookup | Memory | Use Case |
|-----------|--------|--------|----------|
| `HashMap<u64, EntityKnowledge>` | O(1) | ~24 bytes overhead per entry | Best for sparse graphs (not every entity knows every other) |
| Sparse matrix (Vec<Vec<Option<T>>>) | O(1) | ~8 bytes per cell (mostly None) | Best for dense graphs (every entity knows most others) |
| Adjacency list (Vec<(u64, EntityKnowledge)>) | O(n) scan | Minimal | Best for very small graphs (<10 entries) |
**At 80 Active NPCs, ~50 known entities each:**
- HashMap: 80 × 50 × (~200 bytes per EntityKnowledge + 24 overhead) = ~900KB
- Sparse matrix: 80 × 80 × 8 bytes = ~51KB, but needs 6,400 slots (wasteful if only 4,000 entries)
**Recommendation: HashMap per entity.** Knowledge graphs are sparse (an NPC doesn't know about entities they've never encountered), and HashMap lookup is O(1) for observer snapshot generation.
### 2.2 Knowledge Decay Implementation
D-011 requirement: "Fog returns when you leave." Knowledge decays over time.
**Naive approach (WRONG):**
```rust
fn decay_knowledge(
mut query: Query<&mut KnowledgeGraph>,
time: Res<SimulationTime>,
) {
for mut graph in query.iter_mut() {
for entry in graph.entity_knowledge.values_mut() {
let ticks_since = time.tick - entry.last_seen_tick;
if ticks_since > DECAY_THRESHOLD {
entry.confidence *= DECAY_RATE;
}
}
}
}
```
**Problem:** At 80 NPCs × 50 entries = 4,000 decay calculations per tick × 10 tps = 40,000 ops/sec. Unnecessary when knowledge doesn't change every tick.
**Batch decay approach (CORRECT):**
```rust
fn decay_knowledge_batch(
mut query: Query<&mut KnowledgeGraph>,
time: Res<SimulationTime>,
) {
// Only decay once per game-minute (600 ticks at 10 tps)
if time.tick % 600 != 0 {
return;
}
for mut graph in query.iter_mut() {
// Only process if knowledge changed since last decay
if graph.last_decay_tick + 600 > time.tick {
continue;
}
graph.last_decay_tick = time.tick;
graph.entity_knowledge.retain(|_, entry| {
let ticks_since = time.tick - entry.last_seen_tick;
entry.confidence *= DECAY_RATE.powf((ticks_since / 600) as f32);
entry.confidence > 0.1 // Drop very low confidence knowledge
});
}
}
```
**Benefits:**
- Runs 1/600th as often (once per game-minute instead of every tick)
- Skips entities whose knowledge hasn't changed
- `retain()` removes decayed entries in-place (no Vec reallocation)
**Complexity landmine avoided:** Per-tick decay has hidden O(N×M) cost.
### 2.3 Observer Snapshot Query Efficiency
**The query:** "What does entity A know about all entities in this spatial region?"
```rust
fn generate_observer_snapshot(
observer: Entity,
observer_knowledge: &KnowledgeGraph,
spatial_partition: &SpatialPartition, // Prerequisite system
entity_id_map: &EntityIdMap,
world: &World,
) -> ObserverSnapshot {
// 1. Get entities in observer's perception radius
let nearby_entities = spatial_partition.query_radius(
observer_pos,
PERCEPTION_RADIUS
);
// 2. Filter by LOS (shadowcasting, Q-018)
let visible_entities = nearby_entities
.into_iter()
.filter(|e| has_line_of_sight(observer_pos, e.pos))
.collect::<Vec<_>>();
// 3. Build VisibleEntity list with knowledge filtering
let entities = visible_entities
.into_iter()
.filter_map(|e| {
let wire_id = entity_id_map.get_wire_id(e.entity)?;
let knowledge = observer_knowledge.entity_knowledge.get(&wire_id);
// What the observer knows determines what detail is revealed
Some(VisibleEntity {
entity_id: wire_id,
x: e.x,
y: e.y,
z: e.z,
kind: classify_entity(e.entity, knowledge, world),
})
})
.collect();
ObserverSnapshot {
tick: world.resource::<SimulationTime>().tick,
entities,
}
}
```
**Performance profile:**
- Spatial query: O(log N) with grid partitioning
- LOS checks: O(visible entities × ray length) — typically 10-30 entities
- Knowledge lookups: O(1) per entity (HashMap)
- **Total: ~1-2ms per observer at 80 NPCs** (fits within 100ms tick budget for 30-40 observers)
**Critical dependency:** Spatial partitioning must exist before observer queries work. Currently missing (architecture audit section 3.1).
---
## 3. Performance (Questions 9-11)
### 3.1 Memory Budget
**Active tier (80 NPCs, 50 known entities each):**
```
EntityKnowledge struct size: ~200 bytes
- subject_id: 8 bytes
- last_seen_position: 13 bytes (Option<(f32, f32, i32)>)
- last_seen_tick: 8 bytes
- confidence: 1 byte (enum)
- source: 16 bytes (enum with variants)
- facts: Vec (24 bytes ptr + ~50 bytes per fact × 3 facts avg) = ~174 bytes
HashMap overhead: ~24 bytes per entry
Per NPC: 50 entries × (200 + 24) = ~11KB
80 NPCs: 80 × 11KB = ~880KB
WorldFact storage: ~100 facts per NPC × ~80 bytes = ~8KB per NPC = 640KB
Total: ~1.5MB for Active tier knowledge graphs
```
**Background tier (500-2,000 NPCs):**
Simplified representation — only high-confidence entries:
```rust
#[derive(Component, Debug, Clone, Serialize, Deserialize)]
pub struct BackgroundKnowledge {
/// Only high-confidence (>0.7) knowledge
pub known_entities: Vec<u64>, // Just IDs, no detail
}
```
Per NPC: 10 known entities × 8 bytes = 80 bytes
2,000 NPCs: 2,000 × 80 = ~160KB
**State-saved tier:**
Full serialization via serde, stored as blob in save file. Not in memory.
**Total memory budget: ~2MB** (Active + Background) — negligible on modern hardware.
### 3.2 Query Patterns and Indices
**Primary queries:**
1. **Snapshot generation:** "What does observer A know about entities in region R?"
- Requires: Spatial partition (grid-based, ~200 lines)
- Frequency: Once per tick per observer (10-30 observers)
- Cost: O(visible entities) × O(1) knowledge lookup = ~1-2ms
2. **Knowledge update:** "Entity A observed entity B at tick T"
- Direct HashMap insert/update
- Cost: O(1)
3. **Gossip propagation:** "Entity A tells entity B about entity C"
- Lookup A's knowledge of C, insert into B's graph with source=Told
- Cost: O(1) + O(1) = O(1)
4. **Decay pass:** "Remove low-confidence knowledge"
- Batch operation, once per game-minute
- Cost: O(entities × knowledge entries) but amortized
**No complex indices needed.** HashMap per entity + spatial partition for perception = sufficient.
### 3.3 Batch Knowledge Updates
**Observation events can be queued:**
```rust
#[derive(Debug, Clone)]
pub struct KnowledgeEvent {
pub observer: Entity,
pub subject: Entity,
pub event_type: KnowledgeEventType,
pub tick: u64,
}
#[derive(Debug, Clone)]
pub enum KnowledgeEventType {
DirectObservation { position: (f32, f32, i32) },
Overheard { content: String },
ToldBy { source: Entity },
}
#[derive(Resource, Default)]
pub struct KnowledgeEventQueue {
pub events: Vec<KnowledgeEvent>,
}
```
System processes queue in batch:
```rust
fn process_knowledge_events(
mut queue: ResMut<KnowledgeEventQueue>,
mut query: Query<&mut KnowledgeGraph>,
entity_id_map: Res<EntityIdMap>,
time: Res<SimulationTime>,
) {
for event in queue.events.drain(..) {
if let Ok(mut graph) = query.get_mut(event.observer) {
let subject_id = entity_id_map.get_wire_id(event.subject).unwrap();
match event.event_type {
KnowledgeEventType::DirectObservation { position } => {
graph.entity_knowledge
.entry(subject_id)
.and_modify(|e| {
e.last_seen_position = Some(position);
e.last_seen_tick = time.tick;
e.confidence = KnowledgeConfidence::DirectObservation;
})
.or_insert_with(|| EntityKnowledge {
subject_id,
last_seen_position: Some(position),
last_seen_tick: time.tick,
confidence: KnowledgeConfidence::DirectObservation,
source: KnowledgeSource::DirectObservation,
facts: vec![],
});
}
// Handle other event types...
}
}
}
}
```
**Benefits:**
- Perception system emits events without blocking
- Knowledge updates happen in dedicated system phase
- Batching reduces query overhead
---
## 4. System Architecture
### 4.1 bevy_ecs Systems Needed
```rust
pub struct KnowledgePlugin;
impl Plugin for KnowledgePlugin {
fn build(&self, app: &mut App) {
app
.init_resource::<EntityIdMap>()
.init_resource::<KnowledgeEventQueue>()
.add_systems(Update, (
process_knowledge_events
.after(perception_system) // After perception emits events
.before(generate_snapshot), // Before snapshot reads knowledge
decay_knowledge_batch
.after(advance_tick),
));
}
}
```
**System ordering dependencies:**
```
advance_tick (time system)
perception_system (emits KnowledgeEvents)
process_knowledge_events (consumes queue, updates KnowledgeGraph)
decay_knowledge_batch (runs periodically)
generate_snapshot (reads KnowledgeGraph for filtering)
```
### 4.2 Error Handling: Stale Entity References
**Problem:** Entity A knows about entity B (wire ID 42). Entity B despawns. Knowledge graph still references ID 42.
**Solution 1: Tombstone entities**
```rust
#[derive(Component)]
pub struct Despawned {
pub despawn_tick: u64,
}
// When entity despawns, mark instead of removing
fn mark_despawned(mut commands: Commands, query: Query<Entity, With<DespawnRequested>>) {
for entity in query.iter() {
commands.entity(entity)
.remove::<DespawnRequested>()
.insert(Despawned { despawn_tick: /* current tick */ });
}
}
// Cleanup tombstones after sufficient time (e.g., 1 game-hour)
fn cleanup_tombstones(
mut commands: Commands,
query: Query<(Entity, &Despawned)>,
time: Res<SimulationTime>,
) {
for (entity, despawned) in query.iter() {
if time.tick - despawned.despawn_tick > 36000 { // 1 hour at 10 tps
commands.entity(entity).despawn();
}
}
}
```
**Solution 2: Clean references on despawn**
```rust
fn clean_knowledge_references(
mut commands: Commands,
despawned: Query<Entity, With<Despawned>>,
mut all_knowledge: Query<&mut KnowledgeGraph>,
entity_id_map: Res<EntityIdMap>,
) {
for entity in despawned.iter() {
let wire_id = entity_id_map.get_wire_id(entity).unwrap();
// Remove this entity from all knowledge graphs
for mut graph in all_knowledge.iter_mut() {
graph.entity_knowledge.remove(&wire_id);
}
// Now safe to despawn
commands.entity(entity).despawn();
}
}
```
**Recommendation: Solution 1 (tombstones).** Preserves NPC memory ("I knew someone who disappeared") — narratively interesting. Solution 2 causes knowledge to vanish mysteriously.
---
## 5. Tier Serialization (D-026)
### 5.1 Active Tier: Full Serialization
```rust
// KnowledgeGraph derives Serialize/Deserialize
// Serializes with entity bundle automatically
#[derive(Bundle)]
pub struct NpcBundle {
pub npc: Npc,
pub knowledge: KnowledgeGraph,
pub tier: SimulationTier,
// ... other components
}
```
When entity saves: entire `KnowledgeGraph` serializes via serde.
**Serialized size estimate:**
- 50 EntityKnowledge entries × ~200 bytes = ~10KB per NPC
- 80 Active NPCs = ~800KB in save file
- Compressed (bincode): ~400-500KB
### 5.2 Background Tier: Compressed Knowledge
**Transition from Active → Background:**
```rust
fn downgrade_to_background(
mut commands: Commands,
query: Query<(Entity, &KnowledgeGraph), With<SimulationTier>>,
) {
for (entity, knowledge) in query.iter() {
// Compress: keep only high-confidence entries
let compressed = BackgroundKnowledge {
known_entities: knowledge.entity_knowledge
.iter()
.filter(|(_, entry)| entry.confidence >= KnowledgeConfidence::KnowsOf as u8)
.map(|(id, _)| *id)
.collect(),
};
commands.entity(entity)
.remove::<KnowledgeGraph>()
.insert(compressed)
.insert(SimulationTier::Background);
}
}
```
**Transition from Background → Active:**
```rust
fn upgrade_to_active(
mut commands: Commands,
query: Query<(Entity, &BackgroundKnowledge)>,
) {
for (entity, bg_knowledge) in query.iter() {
// Restore minimal knowledge graph
let mut entity_knowledge = HashMap::new();
for id in &bg_knowledge.known_entities {
entity_knowledge.insert(*id, EntityKnowledge {
subject_id: *id,
last_seen_position: None,
last_seen_tick: 0, // Unknown
confidence: KnowledgeConfidence::KnowsOf,
source: KnowledgeSource::Inferred { confidence: 0.7 },
facts: vec![],
});
}
commands.entity(entity)
.remove::<BackgroundKnowledge>()
.insert(KnowledgeGraph {
entity_knowledge,
world_facts: vec![],
last_decay_tick: 0,
})
.insert(SimulationTier::Active);
}
}
```
**Memory savings:** 11KB per NPC → 80 bytes per NPC = ~99% reduction for Background tier.
### 5.3 State-Saved Tier: Blob Serialization
```rust
#[derive(Component, Serialize, Deserialize)]
pub struct SerializedState {
pub blob: Vec<u8>, // bincode-serialized entity bundle
}
fn save_to_state_saved(
mut commands: Commands,
query: Query<Entity, With<StateSavedTransition>>,
world: &World,
) {
for entity in query.iter() {
// Serialize entire entity bundle
let bundle = extract_bundle(world, entity);
let blob = bincode::serialize(&bundle).unwrap();
commands.entity(entity)
.insert(SerializedState { blob })
.insert(SimulationTier::StateSaved)
// Remove all other components
.remove::<KnowledgeGraph>()
.remove::<Position>()
// ...
}
}
```
**Restoration:** Deserialize blob, reconstruct entity.
---
## 6. Critical Gaps and Dependencies
### 6.1 Blocking Issues
**Must be resolved before knowledge system works:**
1. **Q-019 (Entity ID stability):** Wire-format IDs must be generated/mapped. Proposed `EntityIdMap` above solves this.
2. **Spatial partitioning:** Observer queries need spatial index. Architecture audit identified this as CRITICAL (section 3.1).
3. **Q-018 (Shadowcasting algorithm):** LOS checks required for perception → knowledge updates.
### 6.2 Design Blockers
**Must be decided before implementation:**
1. **Q-016 (Knowledge hierarchy):** `KnowledgeConfidence` enum needs concrete levels. Proposed: `Suspects < KnowsOf < KnowsDetails < DirectObservation`.
2. **Q-017 (Triangle pressure):** How does knowledge of triangles affect behavior? Knowledge system needs to expose triangle state for AI queries.
### 6.3 Nice-to-Have but Deferrable
- Knowledge graph visualization tool (debugging)
- Knowledge diff between two entities (for testing information asymmetry)
- Knowledge graph serialization format (JSON for save files?)
---
## 7. Simplest Buildable Implementation
**Phase 1: Minimal Knowledge Graph (Sprint 2)**
1. Replace `InformationInventory` with `KnowledgeGraph` component
2. Implement `EntityIdMap` resource for stable IDs
3. Add `KnowledgeEvent` queue and processing system
4. Direct observation only (no gossip, no inference)
5. No decay (constant knowledge)
**Deliverable:** NPC A observes NPC B → knowledge graph entry created → observer snapshot filters based on knowledge.
**Phase 2: Knowledge Flow (Sprint 3)**
1. Add gossip (entity A tells entity B about C)
2. Add inference (entity A saw entity B enter building → knows B is inside)
3. Add knowledge decay (batch system, once per game-minute)
**Phase 3: Advanced Features (Sprint 4+)**
1. Knowledge confidence hierarchy (Q-016 resolved)
2. Misinformation (wrong facts, discoverable)
3. Triangle pressure integration (Q-017 resolved)
---
## 8. Implementation Warnings
### 8.1 HashMap Iteration Order is a Determinism Time Bomb
Current `Cargo.toml` has `rand = "0.9"` but no explicit HashMap replacement. **Standard Rust `HashMap` has non-deterministic iteration order** (uses randomized SipHash).
**Critical fix:**
```toml
[dependencies]
indexmap = "2" # Or BTreeMap for smaller maps
```
Replace `HashMap<u64, EntityKnowledge>` with `IndexMap<u64, EntityKnowledge>` for deterministic iteration.
**This affects D-010 principle 4 (deterministic simulation).** Non-deterministic iteration breaks replay.
### 8.2 Serialization Version Compatibility
`KnowledgeGraph` will evolve (new fields, new FactType variants). Save file compatibility requires:
```rust
#[derive(Serialize, Deserialize)]
pub struct KnowledgeGraph {
#[serde(default)] // New fields get default values
pub entity_knowledge: HashMap<u64, EntityKnowledge>,
#[serde(default)]
pub world_facts: Vec<WorldFact>,
// Version tag for future migrations
#[serde(default)]
pub version: u32,
}
```
### 8.3 Knowledge Graph Size Can Explode
Without limits, an NPC could accumulate unbounded knowledge. **Add a cap:**
```rust
const MAX_ENTITY_KNOWLEDGE: usize = 100; // Per NPC
const MAX_WORLD_FACTS: usize = 50;
// In update system
if graph.entity_knowledge.len() > MAX_ENTITY_KNOWLEDGE {
// Evict lowest-confidence entries
let mut entries: Vec<_> = graph.entity_knowledge.iter().collect();
entries.sort_by_key(|(_, e)| e.confidence);
let to_remove: Vec<_> = entries.into_iter().take(10).map(|(id, _)| *id).collect();
for id in to_remove {
graph.entity_knowledge.remove(&id);
}
}
```
---
## 9. Recommended Next Steps
1. **Workshop Round 2:** Resolve Q-016 (knowledge hierarchy) with concrete enum values
2. **Architecture spike:** Implement `EntityIdMap` + basic `KnowledgeGraph` in a test harness (~2-3 days)
3. **Ticket breakdown:**
- #351-A: Entity ID stability (EntityIdMap resource)
- #351-B: KnowledgeGraph component (core structs)
- #351-C: KnowledgeEvent queue + processing system
- #351-D: Direct observation flow (perception → knowledge)
- #351-E: Observer snapshot filtering (knowledge → visibility)
4. **Performance validation:** Benchmark at 80 Active NPCs × 50 known entities (target: <5ms for all knowledge updates per tick)
---
## 10. Summary
**The simulation guarantees deterministic knowledge state.** The proposed architecture:
- Uses per-entity `KnowledgeGraph` Component (fits bevy_ecs, supports tier serialization)
- Wire-format `u64` entity IDs solve save/load stability (resolves Q-019)
- HashMap-based storage is O(1) lookup, ~2MB memory for full Active tier
- Batch decay prevents hidden performance explosion
- Knowledge event queue decouples perception from updates
- Tier transitions compress Background knowledge (99% size reduction)
**Critical dependencies:** Spatial partitioning, Q-016 resolution, Q-019 resolution.
**Simplest path:** Start with direct observation only, add gossip/inference/decay in phases.
**Buildable in Sprint 2.** No architectural blockers.
---
**Files referenced:**
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/server/src/npc/mod.rs` (lines 47-49: InformationInventory)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/server/src/bridge/types.rs` (line 26: entity_id)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/server/src/simulation/tier.rs` (tier system)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/architecture.md` (D-010, D-020, D-026, D-030)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/perception.md` (D-011, D-017)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/questions.md` (Q-016, Q-019)
@@ -0,0 +1,734 @@
# Knowledge Graph & Information Boundaries — Round 1 (Gestalt)
**Role:** Mechanics Specialist
**Focus:** How knowledge creates gameplay decisions, system interactions, and emergent fun
---
## Executive Summary
The knowledge graph is the ENGINE that makes asymmetric information playable rather than bookkeeping. Here's what actually matters mechanically:
**What makes this fun:**
- Knowledge GATES player actions (can't ask about what you don't know exists)
- Knowledge CREATES opportunities (exploit what NPCs don't know)
- Knowledge DEGRADES (fog returns, what you learned becomes outdated)
- Knowledge PROPAGATES through NPCs (gossip you can observe and exploit)
**The aha moments:**
1. Realizing an NPC just lied to you (you have contradictory knowledge)
2. Discovering two NPCs both know something you just learned (they've been talking)
3. Exploiting an NPC's ignorance (they don't know you know)
4. Getting caught in the web of your own misinformation
**The failure modes:**
1. Knowledge as inventory management (collect facts like Pokemon)
2. Perfect memory (no decay or challenge)
3. NPC knowledge invisible (can't exploit information asymmetry)
4. Binary knowledge (either you know or you don't — no gradient of confidence)
---
## 1. Knowledge as Gameplay Mechanic (Questions 5-8)
### 1.1 How Knowledge Enters the Graph
Knowledge should come from **four mechanically distinct sources**, each with different gameplay implications:
| Source | How You Get It | Trust Level | Mechanical Consequence |
|--------|---------------|-------------|----------------------|
| **Direct Observation** | You see it happen in your LOS | HIGH (but interpretation can be wrong) | Certain knowledge, adds to your graph immediately. Can trigger monologue. |
| **NPC Tells You** | Dialogue, unprompted disclosure | VARIABLE (depends on NPC trust + mood) | Confidence tied to relationship. Can be lies. Gossip chains visible. |
| **Physical Evidence** | Documents, terminals, manifest logs | MEDIUM-HIGH (can be forged/outdated) | Bypasses social access tiers. Can contradict NPC statements. |
| **Inference/Analysis** | Player connects two facts, or character's insert flags pattern | LOW-MEDIUM (you might be wrong) | Generates `suspects` level knowledge. Needs confirmation. |
**Key mechanic:** The SOURCE must be tracked per knowledge node. This creates:
- "I saw Kael in the corridor" (direct) vs "Sera told me Kael was in the corridor" (told) — mechanically different
- If Sera lied, the lie is IN YOUR KNOWLEDGE GRAPH until contradicted
- Monologue can flag contradictions: "Wait, Sera said Kael was at the dock, but I just saw him here. She lied."
**Implementation proposal:**
```rust
pub enum KnowledgeSource {
DirectObservation { tick: u64, location: TileCoord },
ToldByNPC { npc_id: u64, tick: u64, trust_at_time: f32 },
PhysicalEvidence { evidence_id: String, location: TileCoord },
Inference { based_on: Vec<KnowledgeNodeId> },
}
```
### 1.2 Can Knowledge Be Wrong? (Misinformation Mechanic)
**YES. This is load-bearing.**
The graph must support:
1. **Lies from NPCs** — THE FRIEND tells you they were somewhere, but you later observe contradictory evidence
2. **Outdated information** — "Kael works at the Terminal" (true yesterday, false today after reassignment)
3. **Partial information** — "Two people in that room" but you couldn't see who
4. **False inferences** — Player assumes X based on incomplete data
**Mechanical consequence:** Knowledge needs a `KnowledgeState` enum:
```rust
pub enum KnowledgeState {
Active, // Currently believed true
Contradicted, // You have conflicting information (monologue fires)
Outdated, // Information age threshold passed
Superseded(KnowledgeNodeId), // Replaced by newer knowledge
}
```
When you discover contradictory knowledge:
- Both nodes remain in the graph
- Internal monologue fires: "Sera told me Kael was at the dock, but I just saw him in the corridor. Why did she lie?"
- THE FRIEND's color shifts (green → amber) — this IS the emotional gut-punch
This is emergent detective gameplay. The graph structure CREATES the story beat.
### 1.3 How NPCs Share Knowledge (Gossip System)
**NPCs must have their own knowledge graphs.** D-010 principle 2 is non-negotiable here.
**Gossip propagation rules:**
1. **NPCs share knowledge during conversations** (visible or invisible to player)
2. **What gets shared depends on:**
- Relationship trust level
- Secret/vulnerability axis (some facts are suppressed)
- Tolerance threshold (stressed NPCs overshare)
- Triangle pressure (gossip escalates triangle conflicts)
**Observable mechanic:**
- Player sees two NPCs talking
- Later, NPC B mentions something only NPC A would know
- Player realizes: "They talked. Information propagates. I need to control what people know."
**Gameplay loop:**
```
1. Player learns secret about Kael
2. Player tells Sera (dialogue choice)
3. Sera gossips to Hael (background simulation)
4. Hael confronts Kael (triangle pressure threshold crossed)
5. Player observes the consequence of information propagation
```
**Anti-pattern:** NPCs all share knowledge instantly (breaks mystery).
**Correct pattern:** Gossip propagates at routine intersections + trust thresholds. Visible if you're watching. Invisible if you're not.
---
## 2. Q-016: Knowledge Hierarchy (Suspects < Knows_Of < Knows_Details)
Let me break down what this actually means mechanically.
### 2.1 The Three Levels
| Level | What It Means | How You Get It | What It Gates |
|-------|--------------|---------------|--------------|
| **Suspects** | "Something's weird about X" | Observation triggers (tell fired, routine deviation, monologue hint) | Can investigate. Can't ask direct questions. Monologue says "seems off." |
| **Knows_Of** | "X is involved in Y" | Confirmed connection (saw them at location, told by trusted source, found evidence) | Can ask surface-level questions. NPC deflects if trust < threshold. Access tier: insider/peer. |
| **Knows_Details** | "X is doing Y because Z" | Deep evidence or full disclosure | Can confront. Can exploit. Can make informed choices. Access tier: real/secret. |
### 2.2 Mechanical Consequences
**Monologue prerequisites** (resolves Q-016):
```yaml
# Smuggler entering The Terminal
- id: terminal_m_001
text: "Another day at the office. Keep your head down, Kael."
character: smuggler
trigger: enter_location
prerequisite: null # Always fires
- id: terminal_m_023
text: "Sera's avoiding Torek again. What's she hiding?"
character: smuggler
trigger: observe_npc
prerequisite:
knows_of: ["sera_avoiding_torek"]
- id: terminal_m_047
text: "She's protecting Hael. The manifest discrepancies. It all connects."
character: smuggler
trigger: discover_evidence
prerequisite:
knows_details: ["sera_manifest_cover", "hael_kael_partnership"]
```
**Dialogue access tiers** (D-028 Layer 1):
```yaml
# Talking to Sera
- id: sera_d_012
text: "Busy day at the scanners?"
access: [public]
trust: surface
prerequisite: null
- id: sera_d_089
text: "I noticed you've been avoiding Torek. Everything okay?"
access: [peer]
trust: real
prerequisite:
knows_of: ["sera_avoiding_torek"]
- id: sera_d_134
text: "You're covering for Hael, aren't you? The manifest logs."
access: [insider]
trust: secret
prerequisite:
knows_details: ["sera_manifest_cover"]
```
**Key insight:** The hierarchy creates a PROGRESSION SYSTEM for investigation. You're not grinding XP — you're grinding knowledge confidence.
### 2.3 How You Advance Through The Hierarchy
This is the core gameplay loop:
```
SUSPECTS → KNOWS_OF:
- Observe NPC in wrong place (direct observation)
- Multiple tells fire (pattern emerges)
- Trusted NPC mentions connection (told by high-trust source)
- Find physical evidence (document, terminal log)
KNOWS_OF → KNOWS_DETAILS:
- Confront NPC and they confess (relationship trust + pressure)
- Gather 2+ pieces of corroborating evidence
- Witness the action directly (catch them in the act)
- Trusted source gives full disclosure (unprompted at high trust)
```
**Mechanical weight:** Advancing a knowledge node should require PLAYER ACTION. You don't automatically go from suspects → knows_of just by waiting. You have to investigate.
---
## 3. Game Mechanics Integration (Questions 12-14)
### 3.1 Knowledge Graph → Monologue Pipeline (D-019, Tickets #119-122)
**How this works:**
1. **Simulation detects trigger condition** (enter location, observe NPC, time idle)
2. **Query knowledge graph:** What does the player character know about this location/NPC?
3. **Filter monologue pool by:**
- Character (hard partition per D-032)
- Trigger type
- Knowledge prerequisites (suspects/knows_of/knows_details)
- Situation tags (first_visit, return_visit, after_confrontation, etc.)
4. **Select weighted by mood + recent topics**
5. **Emit MonologueEvent to client**
**Example system logic:**
```rust
fn trigger_observation_monologue(
query: Query<(&PlayerCharacter, &Position, &KnowledgeGraph)>,
observed: Query<(Entity, &Position), With<Npc>>,
monologue_pool: Res<MonologuePool>,
) {
for (character, player_pos, knowledge) in &query {
for (npc_entity, npc_pos) in &observed {
if in_line_of_sight(player_pos, npc_pos) {
// Check knowledge state
let knowledge_level = knowledge.get_confidence(npc_entity);
// Query monologue pool with filters
let candidates = monologue_pool.query(MonologueQuery {
character: character.archetype, // smuggler/detective
trigger: TriggerType::ObserveNpc,
npc_role: npc_role_mapping(npc_entity),
prerequisite: knowledge_level,
});
if let Some(line) = weighted_select(candidates, character.mood) {
events.send(MonologueEvent { line });
}
}
}
}
}
```
**Key insight:** Knowledge prerequisites make monologue REACTIVE to investigation progress. Early game monologue is vague ("Something's off"). Late game monologue is specific ("She's lying to protect Hael").
### 3.2 Knowledge Graph → Dialogue Access (D-028 Layer 1)
**The hard gate mechanic:**
You literally cannot SELECT a dialogue line if you don't meet the knowledge prerequisite.
```
Player clicks NPC → Open dialogue UI
Query available lines:
- Filter by access tier (player's relationship to NPC)
- Filter by knowledge prerequisites
- Weight by mood + situation
Present 3-5 dialogue options to player
Player selects one → conversation continues
```
**Example interaction:**
**Early game (knows_of: null):**
- "Hi Sera, how's it going?" [public]
- "Seen anything unusual today?" [public]
- "Where's Kael?" [peer, if relationship ≥ 0.3]
**Mid game (knows_of: ["sera_avoiding_torek"]):**
- "Hi Sera, how's it going?" [public]
- "I noticed you've been avoiding Torek. Want to talk?" [peer]
- "Where's Kael?" [peer]
**Late game (knows_details: ["sera_manifest_cover"]):**
- "Hi Sera, how's it going?" [public]
- "You're covering for Hael, aren't you?" [insider, trust: secret]
- "I found the manifest logs, Sera." [insider, trust: secret]
**This is emergent gating.** The player doesn't hit a script flag. The knowledge graph unlocks dialogue naturally.
### 3.3 Knowledge + Social Triangles (D-024, Q-017)
**Triangle pressure as knowledge-driven mechanic:**
Social triangles have a `pressure` value that increases based on:
- Knowledge propagation (A learns about B's secret via gossip)
- Player action (player tells C about A-B conflict)
- Time + mood (tolerance threshold erosion)
**When pressure crosses threshold:**
```rust
if triangle.pressure > triangle.confrontation_threshold {
// Activate confrontation situation
mark_npcs_as_in_conflict(&triangle.members);
// Dialogue lines tagged situation:confrontation become available
// Monologue observing these NPCs fires urgent variant
// If player knows_details about the triangle, they get context monologue
}
```
**Q-017 proposal:** Triangle pressure threshold should be ~60-80 (out of 100 scale).
**Events that increase pressure:**
- NPC discovers contradiction (+15)
- Gossip reaches third party (+10)
- Player confronts one member (+20)
- Routine conflict (wants collide) (+5 per day)
**Player knowledge interaction:**
- If player knows_details about triangle, monologue explains what's happening
- If player only suspects, monologue is vague: "Tension between these two."
- Player can INTENTIONALLY escalate by sharing information
**This is SYSTEMS DESIGN.** Knowledge graph + gossip propagation + triangle pressure = emergent drama without scripts.
---
## 4. Data Structure Design (Questions 1-4)
Let me map that to mechanics.
### 4.1 What "Entity A Knows About Entity B" Looks Like
```rust
pub struct KnowledgeGraph {
/// All knowledge nodes this entity has
nodes: HashMap<KnowledgeNodeId, KnowledgeNode>,
/// Quick lookup: what do I know about entity X?
entity_knowledge: HashMap<u64, Vec<KnowledgeNodeId>>,
/// Quick lookup: what facts do I know about location X?
location_knowledge: HashMap<TileCoord, Vec<KnowledgeNodeId>>,
/// Gossip propagation queue (for background NPCs)
pending_gossip: Vec<GossipEvent>,
}
pub struct KnowledgeNode {
pub id: KnowledgeNodeId,
pub content: KnowledgeContent,
pub source: KnowledgeSource,
pub confidence: KnowledgeConfidence,
pub state: KnowledgeState,
pub timestamp: u64, // When learned
pub last_confirmed: u64, // When last verified (decay mechanic)
}
pub enum KnowledgeContent {
EntityAtLocation { entity_id: u64, location: TileCoord },
EntityRelationship { entity_a: u64, entity_b: u64, relationship: String },
EntityProperty { entity_id: u64, property: String, value: String },
WorldFact { fact_id: String, data: String },
TriangleMembership { triangle_id: String, members: Vec<u64> },
}
pub enum KnowledgeConfidence {
Suspects, // ~20-40% confidence
KnowsOf, // ~60-75% confidence
KnowsDetails, // ~85-95% confidence
}
```
### 4.2 Knowledge Decay (D-011: "Fog Returns When You Leave")
**Mechanical implementation:**
```rust
pub fn update_knowledge_decay(
time: Res<SimulationTime>,
mut query: Query<(&mut KnowledgeGraph, &Position)>,
) {
for (mut knowledge, pos) in &mut query {
for (id, node) in &mut knowledge.nodes {
let age = time.current_tick - node.last_confirmed;
// Decay rules based on content type
let decay_threshold = match node.content {
KnowledgeContent::EntityAtLocation { .. } => 600, // 60 game-minutes
KnowledgeContent::EntityRelationship { .. } => 14400, // 24 game-hours
KnowledgeContent::EntityProperty { .. } => u64::MAX, // Never decays
_ => 3600, // 6 game-hours
};
if age > decay_threshold {
node.state = KnowledgeState::Outdated;
// Trigger monologue if player returns
if player_at_location(pos, node.content.location()) {
emit_monologue_event("I wonder if X is still here...");
}
}
}
}
}
```
**Why this is fun:**
- Knowledge isn't perfect memory
- Revisiting locations creates "is this still true?" tension
- NPCs move, situations change
- Player has to MAINTAIN their knowledge through repeated observation
**Anti-pattern:** Knowledge never decays (player becomes omniscient over time).
### 4.3 Non-Entity Facts
**Handled via `KnowledgeContent::WorldFact`:**
```rust
// Examples
KnowledgeContent::WorldFact {
fact_id: "smuggling_ring_exists",
data: "terminal_district",
}
KnowledgeContent::WorldFact {
fact_id: "manifest_discrepancy",
data: "cargo_batch_47_alpha",
}
KnowledgeContent::WorldFact {
fact_id: "code_word",
data: "nightingale",
}
```
**These gate:**
- Monologue ("I need to find out more about nightingale")
- Dialogue options ("Have you heard the word 'nightingale'?")
- Access to restricted areas (code word at door)
---
## 5. Performance (Questions 9-11)
Let me reality-check this against D-026 simulation tiers.
### 5.1 Memory Budget at 80 Active NPCs
**Per NPC knowledge graph estimate:**
```
Average knowledge nodes per NPC: 50-100
Average node size: ~200 bytes (with enums, IDs, metadata)
Per-NPC graph size: 10-20 KB
80 Active NPCs = 800 KB - 1.6 MB total
+ 500 Background NPCs (smaller graphs) = +2-5 MB
Total: ~3-7 MB for all knowledge graphs
```
**This is CHEAP.** For context, a single high-res texture is 4-16 MB. Knowledge graphs are not the bottleneck.
### 5.2 Observer Snapshot Query Efficiency
**The critical query:**
> "What does Player A know about the entities currently in their LOS?"
```rust
fn generate_observer_snapshot(
player: &Player,
knowledge: &KnowledgeGraph,
visible_entities: &[u64],
) -> ObserverSnapshot {
let mut snapshot = ObserverSnapshot::new();
for entity_id in visible_entities {
// O(1) lookup via entity_knowledge HashMap
if let Some(node_ids) = knowledge.entity_knowledge.get(entity_id) {
let confidence = highest_confidence(node_ids, knowledge);
let last_seen = most_recent_observation(node_ids, knowledge);
snapshot.add_entity(VisibleEntity {
entity_id: *entity_id,
known_confidence: confidence,
relationship_color: derive_color(entity_id, knowledge),
});
} else {
// Unknown entity - teal color, minimal info
snapshot.add_entity(VisibleEntity {
entity_id: *entity_id,
known_confidence: None,
relationship_color: Color::UNKNOWN_TEAL,
});
}
}
snapshot
}
```
**Performance:** ~10-50 entities in LOS × O(1) lookup = negligible overhead.
### 5.3 Spatial Partitioning for Knowledge Updates
**Do we need it?** NO, not for v0.1.
**Why:**
- Active tier is only 80 NPCs
- Knowledge updates happen on discrete events (observation, conversation, evidence discovery)
- Not every tick — only when something HAPPENS
**Future optimization:** If we hit 500+ active NPCs, add spatial partitioning so gossip propagation only checks nearby entities. But that's a milestone 3+ problem.
---
## 6. The Fun Question (Critical Evaluation)
### 6.1 What Makes This Interesting vs Bookkeeping?
**Interesting mechanics (keep these):**
1. **Knowledge gates meaningful choices** — "Should I confront Sera now, or gather more evidence?" depends on confidence level
2. **Asymmetry creates exploitation** — "Kael doesn't know I know" enables social manipulation
3. **Contradictions create story beats** — Discovering THE FRIEND lied IS the emotional punch
4. **Gossip propagates observably** — You see NPCs talking, you see consequences later
5. **Knowledge decays** — Revisiting locations has tension ("Is this still true?")
**Bookkeeping mechanics (avoid these):**
1. **Binary knowledge** — Either you know or you don't (no gradient, no stakes)
2. **Perfect memory** — Once you know, you always know (no decay, no challenge)
3. **Hidden NPC knowledge** — Can't see what NPCs know (can't exploit information asymmetry)
4. **No propagation** — Knowledge is static (no emergent social dynamics)
5. **No consequences** — Knowing something doesn't change options (why track it?)
**Our design scores:**
- Knowledge gates choices: YES (dialogue access, monologue prerequisites)
- Asymmetry exploitable: YES (NPC knowledge graphs separate from player)
- Contradictions visible: YES (knowledge state tracking, monologue triggers)
- Gossip observable: YES (routine intersections, visible conversations)
- Knowledge decays: YES (timestamp + decay thresholds)
**Verdict:** This is mechanically sound. The knowledge graph creates DECISIONS, not just RECORDS.
### 6.2 Where Are The Aha Moments?
**Moment 1: The Contradiction**
```
You: "Where were you last night?"
Sera: "Working late at the scanners."
[Later, you check terminal logs]
Monologue: "Sera wasn't at the scanners. She lied to me. Why?"
[Sera's color shifts: green → amber]
```
**This works because:**
- Two knowledge nodes with conflicting sources
- System detects contradiction
- Monologue fires
- Visual feedback (color change)
- Player realizes: "Information is unreliable. I need to verify."
**Moment 2: The Gossip Chain**
```
[You tell Voss about Kael's smuggling]
[Hours later, you observe Voss and Maret talking]
[Next day, Maret confronts Kael]
Monologue: "Voss told Maret. Information spreads. I started this."
```
**This works because:**
- Gossip propagation system
- Observable routine intersections
- Triangle pressure increases
- Player sees CONSEQUENCE of their choices
**Moment 3: The Exploitation**
```
[You know Kael is in corridor B, but Torek doesn't]
Torek: "Have you seen Kael?"
You: [Lie] "I think he's at the dock."
[Torek leaves, you slip into corridor B unobserved]
```
**This works because:**
- Player knowledge ≠ NPC knowledge
- Dialogue options gated by what you know
- Can deliberately misinform
- Consequences are mechanical (NPC goes wrong place)
### 6.3 Failure Modes to Avoid
**Failure Mode 1: Pokemon Knowledge**
> "Collect all 47 facts about the smuggling ring!"
**How we avoid it:**
- Knowledge isn't completionist (no "you've discovered 23/47 facts" UI)
- Confidence gradients mean there's no "done" state
- Multiple valid investigation paths (not checklist)
**Failure Mode 2: Information Overload**
> Journal has 300 entries, player drowns in text.
**How we avoid it:**
- Knowledge graph is BACKEND (no giant journal UI)
- Player experiences knowledge through GATES (dialogue options, monologue context)
- "What do I know?" expressed through what actions are available
**Failure Mode 3: NPC Knowledge Invisible**
> "I have no idea what anyone else knows."
**How we avoid it:**
- Observable gossip (NPCs talk, you see it)
- Tell system (NPCs react to what they know)
- Confrontation situations (knowledge triggers visible behavior change)
---
## 7. Mechanical Recommendations
### 7.1 Knowledge Graph Core Features (Must Have)
1. **Per-entity knowledge graphs** — Player + all Active/Background NPCs
2. **Knowledge confidence hierarchy** — Suspects < KnowsOf < KnowsDetails
3. **Knowledge source tracking** — Direct/Told/Evidence/Inference
4. **Knowledge state** — Active/Contradicted/Outdated/Superseded
5. **Timestamp + decay** — Old knowledge becomes unreliable
6. **Entity knowledge lookup** — O(1) "what do I know about X?"
7. **Gossip propagation** — NPCs share knowledge at routine intersections
### 7.2 Integration Points
**Monologue system (#119-122):**
- Query knowledge graph for prerequisites
- Filter pool by confidence level
- Fire contradicted state as urgent monologue
**Dialogue system (D-028):**
- Gate access by knowledge prerequisites
- "Ask about X" only appears if you know_of X
- Confrontation lines require knows_details
**Social triangles (D-024):**
- Knowledge propagation increases triangle pressure
- Pressure threshold triggers confrontation situation
- Player knowledge gates context monologue
**Entity color (D-033):**
- Derive relationship color from knowledge + relationship state
- Color shifts when knowledge state changes (contradicted → amber)
### 7.3 V0.1 Minimum Viable Implementation
**Phase 1 (Sprint 2):**
- `KnowledgeGraph` component with node storage
- Three confidence levels (enum)
- Direct observation source only
- Basic entity knowledge lookup
- Monologue prerequisite gating
**Phase 2 (Sprint 3):**
- Add ToldByNPC source
- Contradiction detection
- Gossip propagation (simple: share on conversation)
- Dialogue access gating
**Phase 3 (Sprint 4):**
- Knowledge decay system
- Evidence source
- Triangle pressure integration
- Full source tracking
---
## 8. Open Question Resolutions
**Q-016 (Knowledge hierarchy):**
- **RESOLVED:** Three levels (Suspects / KnowsOf / KnowsDetails) with mechanical consequences as specified above
- Confidence values: Suspects ~30%, KnowsOf ~70%, KnowsDetails ~90%
- Advancement requires player investigation action, not automatic
**Q-017 (Triangle pressure threshold):**
- **PROPOSED:** Threshold = 70 (out of 100)
- Events increase pressure: contradiction +15, gossip +10, confrontation +20, routine conflict +5/day
- When crossed, situation:confrontation activates, NPCs get observable tells
**New question raised:**
- **Q-024: Gossip propagation timing** — Do NPCs gossip immediately during conversation, or queued for next routine intersection? Immediate = more reactive, queued = more predictable for player exploitation.
---
## 9. Risk Assessment
**High risk:**
- Performance if we naively iterate all NPCs for gossip (mitigation: event-driven, not tick-driven)
- UI complexity if we expose too much graph state (mitigation: knowledge expressed through gates, not journal)
**Medium risk:**
- Knowledge decay tuning (mitigation: conservative defaults, playtesting)
- Contradiction detection false positives (mitigation: tight content authoring, source metadata)
**Low risk:**
- Memory overhead (justified by budget analysis)
- Query performance (O(1) lookups via HashMap)
---
## 10. Conclusion
The knowledge graph is not just a data structure — it's the MECHANIC that makes asymmetric information playable.
**What this enables:**
- Investigation as progression system (suspects → knows_of → knows_details)
- Social manipulation (exploit what NPCs don't know)
- Emergent drama (gossip propagates, triangles escalate)
- Genuine detective moments (contradictions, aha realizations)
**What makes it fun:**
- Knowledge creates CHOICES (dialogue gates, action opportunities)
- Knowledge creates TENSION (decay, unreliable sources)
- Knowledge creates STORY (THE FRIEND's lie, gossip consequences)
**Bottom line:** This is interesting-complex, not annoying-complex. The player never manages the graph directly — they experience it through monologue, dialogue options, and NPC behavior. That's the sweet spot.
Let's build this.
---
**Files referenced:**
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/perception.md` (D-011, D-015-D-019)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/architecture.md` (D-010, D-026)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/content.md` (D-024, D-028, D-033)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/questions.md` (Q-016, Q-017)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/server/src/npc/mod.rs` (current NPC model)
@@ -0,0 +1,865 @@
# Knowledge Graph & Information Boundaries — Round 1 (Paula)
**Role:** Narrative & Political Depth Specialist
**Focus:** How knowledge creates story, how relationships generate meaning, how the graph makes mystery playable
**Date:** 2026-02-11
---
## Executive Summary
The knowledge graph is not a database problem. It's a narrative problem disguised as a data structure.
Here's what actually matters: **knowledge is the currency of relationships**. NPCs don't just know facts — they know facts *about each other*, and their willingness to share those facts is the entire social gameplay loop. The knowledge graph must track not just "what does entity A know" but "what does entity A know *about entity B*, and who told them, and what does that reveal about their relationship?"
**The aha moments this system must support:**
1. Discovering that THE FRIEND lied to you (contradictory knowledge from different sources)
2. Realizing two NPCs know something you just learned (they've been talking behind your back)
3. Exploiting an NPC's ignorance (they don't know you know)
4. Watching knowledge propagate through a social network (gossip as observable mechanic)
5. The moment when your character's internal monologue flags a contradiction your player missed
**The failure modes to avoid:**
1. Knowledge as Pokemon collection (gather all the facts)
2. Perfect memory with no decay (no tension)
3. Binary knowledge states (you either know or you don't — no suspicion, no partial information)
4. NPC knowledge invisible to the player (can't read the room)
5. Knowledge without emotional weight (facts that don't connect to relationships)
**The content team dependency (#309):** We need a **knowledge vocabulary** for v0.1 monologue authoring — the canonical list of knowledge flags that monologue lines can reference as prerequisites. Without this, Mellanie can't write "Character knows X" trigger conditions.
---
## 1. Narrative Foundations: What Is Knowledge?
### 1.1 What Does "Knowledge" Mean Narratively?
Knowledge is not objective truth stored in a mind. Knowledge is:
1. **An interpretation** — what the character BELIEVES based on incomplete evidence
2. **A relationship artifact** — most knowledge comes from other people, and the source shapes the meaning
3. **An emotional stake** — knowledge about people you care about weighs more than knowledge about strangers
4. **A liability** — knowing something can be dangerous, and NPCs know this
**Example from the v0.1 scenario:**
*The smuggler knows Kael (their colleague and THE FRIEND) has been meeting someone in the restricted corridor. This knowledge is:*
- An interpretation: "Kael is compromised" or "Kael is trying to exit the ring" (both possible)
- A relationship artifact: The smuggler learned this through direct observation, not rumor — high confidence
- An emotional stake: Kael is trusted, so this knowledge creates internal conflict
- A liability: If the ring leader asks "Do you know where Kael was?" the smuggler must choose: lie (protect Kael), tell truth (protect the operation), or deflect (buy time)
**This is what the knowledge graph must support.** Not "entity A knows fact X" but "entity A believes interpretation Y based on source Z, which creates emotional state W and decision pressure P."
### 1.2 How Does the Player CHARACTER Know Things?
Critical distinction: the PLAYER might notice something in the viewport, but the CHARACTER might not. The knowledge graph tracks what the CHARACTER knows, not what the player has seen.
**Four ways the character gains knowledge:**
| Method | Trust Level | Monologue Behavior | Example |
|--------|-------------|-------------------|---------|
| **Direct observation** | HIGH (but interpretation can be wrong) | Immediate monologue: "Kael's in the corridor. He shouldn't be there." | You watch Kael enter a restricted area |
| **Told by trusted NPC** | VARIABLE (depends on relationship + situation) | Delayed reflection: "Sera said Kael was at the dock... but I just saw him here. Why did she lie?" | Sera tells you Kael's location |
| **Told by untrusted NPC** | LOW (flagged as suspicious) | Immediate skepticism: "He's lying. But why?" | A hostile NPC makes a claim |
| **Physical evidence** | MEDIUM-HIGH (can be forged) | Analytical monologue: "Manifest says 40 units. I count 37. Where are the other three?" | You access a terminal |
**Key narrative mechanic:** The monologue system reveals the CHARACTER's confidence level. The player learns what their character thinks, not ground truth. This is the unreliable narrator pattern (D-016) applied to information.
### 1.3 Knowledge Hierarchy: From Suspicion to Certainty
Q-016 asks for a knowledge hierarchy. From a narrative perspective, there are five levels:
```
1. Unaware — "Who's Hael?"
2. Suspects — "I think something is wrong with Kael"
3. Knows_of — "Kael has been meeting someone"
4. Knows_details — "Kael met Hael in corridor B-7 at 22:40"
5. Understands — "Kael is trying to exit the ring to protect Hael"
```
**Why five levels, not three?**
- **Unaware -> Suspects** is the first monologue trigger. Character's gut feeling before evidence. This is "character is smarter than player" moment (D-039 wow moment #2).
- **Suspects -> Knows_of** is the observation confirmation. Suspicion becomes fact.
- **Knows_of -> Knows_details** is the investigative depth. General knowledge becomes actionable.
- **Knows_details -> Understands** is the emotional/contextual revelation. Facts become narrative meaning.
**Content authoring implication:** Monologue lines need to reference specific levels:
```yaml
- id: monologue_kael_suspicion
text: "Kael's been distant lately. Three late arrivals this week. That's not like him."
trigger: observe_npc
prerequisite:
knowledge:
- subject: kael_davan
level: suspects
topic: behavior_change
```
The level gates which lines play. A `knows_details` monologue shouldn't fire if you're still at `suspects`.
---
## 2. Key Aha Moments the System Must Support
### 2.1 THE FRIEND Contradiction Discovery
The emotional centerpiece of v0.1 (D-034, D-039 wow moment #3). Here's the knowledge graph sequence:
**Timeline:**
1. **Session start** — Character knows: `kael_davan: trusted, colleague, ring_member` (high relationship, insider access)
2. **Minute 20** — Direct observation: Kael enters restricted corridor during off-shift hours
3. **Knowledge update** — Character now knows: `kael_davan: observed_in_restricted_area, timestamp: 1200, source: direct_observation`
4. **Monologue trigger** — Urgent chime: *"Kael? What's he doing in B-7? That corridor's off-limits during second shift."*
5. **Player follows** — Observes Kael meeting unknown NPC (Hael)
6. **Knowledge update** — Character now knows: `kael_davan: meeting_unknown_contact, location: corridor_b7`
7. **Relationship state change**`kael_davan: relationship_tier shifts from trusted > flagged`
8. **Entity color shift** — Green -> Amber (D-033)
9. **Next conversation with Kael** — Dialogue tree unlocks confrontation option
**What the knowledge graph must support:**
- Observation generates knowledge nodes
- Knowledge contradicts prior beliefs (Kael is supposed to be on dock, but you saw him in corridor)
- Contradiction triggers monologue + relationship state shift
- Relationship state affects dialogue access tiers (new lines unlock)
- Subsequent observations add detail (meeting someone -> meeting Hael specifically)
### 2.2 Discovering Shared Knowledge (Gossip Observation)
**Scenario:** The smuggler overhears two NPCs (Torek and Lera) discussing Kael's strange behavior. Both NPCs reference details the smuggler also knows.
**What this reveals:**
1. Kael's behavior is public knowledge (multiple observers)
2. Torek and Lera have been talking (relationship between them)
3. The situation is escalating (if people are gossiping, it's becoming a problem)
**Monologue:** *"Torek and Lera are talking about Kael. They've noticed too. This is getting messy."*
**Knowledge graph implication:** NPCs must have their own knowledge graphs that the player can partially observe. When you overhear a conversation, you learn:
- What the NPCs know (adds to your graph)
- That the NPCs know it (meta-knowledge about their knowledge)
- That they're sharing it with each other (relationship inference)
**This is load-bearing for political intrigue.** In a conspiracy game, WHO KNOWS WHAT is as important as what they know.
### 2.3 Exploiting Information Asymmetry
**Scenario:** The detective learns (via evidence) that Voss (hub supervisor) has been filing false manifests. The detective knows. Voss doesn't know the detective knows.
**Gameplay opportunity:** The detective can:
1. Confront Voss directly (uses knowledge to force disclosure)
2. Observe Voss to gather more evidence (exploit ignorance to catch him in the act)
3. Bluff to Voss ("I know what you've been doing") even if evidence is incomplete
**Dialogue system integration (D-028):** Confrontation dialogue unlocks when:
- Player has `knows_details` level knowledge about Voss's activity
- Voss relationship state includes `has_secret` flag
- Player initiates dialogue and selects confrontation topic
**Example dialogue tree:**
```yaml
- id: voss_confrontation_manifest
role: hub_supervisor
access: [authority, hostile] # Changes when confronted
trust: secret # This was hidden information
situation: confrontation
topic: cargo_discrepancy
prerequisite:
knowledge:
- subject: voss
flag: false_manifests
level: knows_details
text: "[Present evidence] I pulled the cargo logs for the last three weeks. The numbers don't match."
response_branches:
- deny: [voss_deny_weak, voss_deny_strong] # Depends on Voss's tolerance
- deflect: [voss_deflect_authority, voss_deflect_personal]
- confess: [voss_confess_partial] # Only if tolerance threshold crossed
```
**The knowledge level gates the option.** You can't confront without `knows_details`.
### 2.4 Knowledge Decay and the Return
**Scenario (D-011):** The smuggler visits the bar, then leaves the district for several game-hours. When they return, the bar's social configuration has changed — different NPCs present, different conversations happening.
**Knowledge decay:**
- `location_state: last_bar` timestamp is old
- Character knows: *"The bar was quiet when I left. Let's see who's here now."*
- Player expects the same scene. Character knows better.
**Monologue on return:** *"Bar's louder now. End of shift crowd. Torek's here — wasn't earlier."*
**This creates micro-mystery.** What happened while you were gone? Who talked to whom? The fog returns not just visually but informationally.
---
## 3. Content Authoring: The v0.1 Knowledge Vocabulary
**Critical dependency: ticket #309 (knowledge state vocabulary for monologue authoring).**
Mellanie needs a canonical list of knowledge flags to write monologue prerequisites. Here's my proposal for v0.1:
### 3.1 Entity Knowledge Categories
Knowledge about NPCs, structured by category:
| Category | Example Flags | Content Use |
|----------|--------------|-------------|
| **Identity** | `knows_name`, `knows_role`, `knows_background` | Greeting monologue, name vs "the dock worker" |
| **Location** | `last_seen_location`, `expected_location`, `observed_out_of_place` | Deviation detection monologue |
| **Behavior** | `observed_nervous`, `observed_lying`, `behavior_change` | Character assessment monologue |
| **Relationships** | `knows_relationship_to_X`, `observed_meeting_X`, `knows_triangle` | Social dynamics monologue |
| **Secrets** | `suspects_secret`, `knows_secret_partial`, `knows_secret_full` | Progressive revelation monologue |
| **Contraband** | `knows_ring_exists`, `knows_ring_member`, `knows_smuggling_operation` | Conspiracy monologue |
### 3.2 World Knowledge Categories
Knowledge about places, events, systems:
| Category | Example Flags | Content Use |
|----------|--------------|-------------|
| **Locations** | `knows_restricted_area`, `knows_dead_drop`, `mapped_corridor_b7` | Navigation monologue |
| **Events** | `witnessed_meeting`, `heard_argument`, `discovered_evidence` | Event reaction monologue |
| **Systems** | `knows_shift_schedule`, `knows_security_gaps`, `knows_manifest_process` | Operational knowledge monologue |
| **Factions** | `knows_commission_presence`, `knows_ring_structure` | Political assessment monologue |
### 3.3 Confidence Levels (Q-016 Resolution)
Each knowledge flag has a confidence level:
```yaml
knowledge:
- subject: kael_davan
category: behavior
flag: observed_out_of_place
level: knows_details # One of: suspects, knows_of, knows_details, understands
source: direct_observation
timestamp: 1200
location: corridor_b7
```
**Monologue authoring example:**
```yaml
# Early suspicion line
- id: mon_kael_suspicion_001
character: smuggler
trigger: observe_npc
prerequisite:
knowledge:
- subject: kael_davan
flag: behavior_change
level: suspects # Fires when suspicion forms
text: "Kael's been off lately. Can't put my finger on it, but something's different."
# Confirmed observation line
- id: mon_kael_observation_001
character: smuggler
trigger: observe_npc
prerequisite:
knowledge:
- subject: kael_davan
flag: observed_out_of_place
level: knows_of # Fires after you see him somewhere wrong
text: "Kael, in the restricted corridor. He's not scheduled for that area."
# Detailed understanding line
- id: mon_kael_understanding_001
character: smuggler
trigger: post_conversation
prerequisite:
knowledge:
- subject: kael_davan
flag: trying_to_exit_ring
level: understands # Fires after you piece together the why
text: "He's trying to get out. For Hael. Damn it, Kael, you should have talked to me first."
```
**This is the vocabulary content team needs.** A structured taxonomy, not free-form strings.
---
## 4. Dialogue System Integration (D-028)
### 4.1 Access Tiers and Knowledge
The four access tiers (public/peer/insider/secret) interact with knowledge state:
**Public tier** — anyone can say these lines, regardless of relationship:
- Greetings
- Surface-level small talk
- Officially public information
**Peer tier** — requires neutral-to-positive relationship:
- Casual conversation
- Workplace chat
- Shared experiences
**Insider tier** — requires trust relationship + shared context:
- Personal information
- Informal knowledge
- Community secrets (not individual secrets)
**Secret tier** — requires high trust + specific knowledge trigger:
- Individual secrets
- Criminal activity
- Vulnerability disclosure
**Knowledge gates the secret tier.** An NPC won't disclose secret-tier information unless:
1. You have sufficient trust relationship
2. You ALREADY know part of the secret (partial knowledge proves you're safe to tell)
3. The NPC is under pressure (tolerance threshold crossed, triangle pressure high)
**Example:**
```yaml
# Surface gossip (insider tier, no knowledge prerequisite)
- id: lera_gossip_kael_001
role: bar_owner
access: [insider] # Must be a bar regular
trust: surface
situation: casual_conversation
topic: coworker_gossip
text: "Kael's been a bit quiet lately. Everything okay with him?"
# Real disclosure (insider tier, requires partial knowledge)
- id: lera_disclosure_kael_001
role: bar_owner
access: [insider]
trust: real
situation: private_conversation
topic: kael_situation
prerequisite:
knowledge:
- subject: kael_davan
flag: behavior_change
level: knows_of # You've noticed, so Lera can confirm
text: "Yeah, I've noticed too. He's been meeting someone. Don't know who, but it's not ring business."
# Secret admission (insider tier, high trust, detailed knowledge)
- id: lera_admission_kael_001
role: bar_owner
access: [insider]
trust: secret
situation: confrontation
topic: kael_exit_attempt
prerequisite:
knowledge:
- subject: kael_davan
flag: trying_to_exit_ring
level: knows_details # You know enough to be dangerous
relationship:
trust_level: high
text: "Look, Kael came to me asking how to get out clean. I told him there's no such thing. You planning to turn him in, or help him?"
```
**The knowledge prerequisite controls disclosure progression.** NPCs don't volunteer secrets — you unlock them by demonstrating you already know enough to be trusted.
### 4.2 Fishing for Information (Player Doesn't Know Yet)
**Question from workshop brief:** What happens when the player asks about something they don't know about yet?
**Answer:** You can't ask about what you don't know exists.
The dialogue system should only present topics the character has *at least* `suspects` level knowledge about. If you've never heard of Hael, you can't select "Ask about Hael" as a dialogue option.
**BUT:** You CAN ask open-ended questions that might reveal new knowledge:
```yaml
# Open-ended probe (always available in peer+ conversations)
- id: dialogue_probe_general_001
role: any
access: [peer, insider]
trust: surface
topic: general_probe
text: "Anything interesting happening around here lately?"
# NPC response depends on their mood, trust, and what THEY know
```
The NPC might volunteer knowledge, weighted by:
- Their mood (stressed NPCs overshare)
- Their trust in you
- Triangle pressure (gossip escalates conflicts)
- Recent events
**This is D-028 Layer 4 (unprompted disclosure).** NPCs have agency — they choose to tell you things based on their state, not just your questions.
### 4.3 Confrontation Mechanics
Confrontation is a special situation (D-035 situation taxonomy) that activates when:
1. You have `knows_details` level knowledge about an NPC's secret/vulnerability
2. You initiate conversation with confrontation intent (player choice)
3. OR triangle pressure crosses threshold (Q-017)
**Confrontation dialogue uses knowledge as evidence:**
```yaml
- id: dialogue_confront_voss_manifest
role: hub_supervisor
access: [authority]
trust: secret
situation: confrontation
topic: manifest_discrepancy
prerequisite:
knowledge:
- subject: voss
flag: false_manifests
level: knows_details
text: "[Present evidence] I pulled the cargo logs for the last three weeks. The numbers don't match."
# Voss's response depends on his tolerance threshold
```
**NPC response is NOT dialogue branches.** It's systemic:
- Low tolerance -> confess, cooperate
- Medium tolerance -> deflect, stall
- High tolerance -> deny, become hostile
The knowledge system creates the confrontation opportunity. The NPC's axes (D-024) determine the outcome.
---
## 5. Political & Social Dynamics
### 5.1 Knowledge Propagation (Gossip as Mechanic)
NPCs share knowledge with each other. This is D-010 principle 2 in action — NPCs have information boundaries just like the player.
**Gossip propagation rules:**
1. **NPCs share knowledge during conversations** (visible to player if in earshot, invisible if not)
2. **What gets shared depends on:**
- Relationship between the NPCs (trust level)
- Personality (some NPCs are gossips, some are discreet)
- Tolerance threshold (stressed NPCs overshare)
- Triangle pressure (gossip escalates triangle conflicts)
**Example propagation chain:**
```
T=100: Smuggler observes Kael in restricted corridor
Smuggler knowledge: kael_observed_out_of_place (level: knows_of)
T=150: Smuggler mentions this to Lera (bar owner) in casual conversation
Lera knowledge: kael_observed_out_of_place (level: knows_of, source: told_by_smuggler)
T=200: Torek (security officer) visits bar, talks to Lera
Lera shares gossip about Kael (her personality: gossip=high)
Torek knowledge: kael_observed_out_of_place (level: knows_of, source: told_by_lera)
T=250: Voss (hub supervisor, ring leader) talks to Torek
Torek casually mentions Kael's odd behavior
Voss knowledge: kael_behavior_suspicious (level: suspects, source: told_by_torek)
T=300: Voss confronts Kael
Ring pressure increases
Kael's tolerance threshold approaches breaking point
```
**The player can observe parts of this chain:**
- Overhear Lera and Torek gossiping (direct observation)
- Notice Voss and Kael having a tense conversation (indirect observation)
- Kael becomes more nervous in subsequent interactions (behavioral tell)
**Monologue commentary:** *"Everyone's talking about Kael now. This is how people get caught."*
**This is emergent political intrigue.** The knowledge graph + NPC conversations create information flow that the player can observe, exploit, or try to suppress.
### 5.2 Social Triangles and Knowledge (D-024, Q-017)
Triangles are the atomic unit of social intrigue. Knowledge about triangles changes gameplay.
**Triangle 4 (Worried Partner) from canonical web:** Hael — Kael — Sera
**Knowledge progression:**
| Phase | Character Knowledge | Gameplay Impact |
|-------|---------------------|-----------------|
| **Unaware** | Character doesn't know triangle exists | No special options |
| **Suspects** | "Kael's been acting strange. Sera seems worried about something." | Monologue flags pattern |
| **Knows_of** | "Kael and Sera have been meeting. Hael is involved somehow." | Can ask NPCs about relationships |
| **Knows_details** | "Hael is Kael's partner. Kael is trying to exit the ring to protect Hael. Sera is covering for both of them." | Confrontation unlocks, triangle pressure visible |
| **Understands** | "This is why Sera lied to me. She's protecting Hael as a friend." | Emotional context, forgiveness option |
**Triangle pressure (Q-017 question):** As knowledge spreads, triangle pressure increases. When it crosses threshold, `confrontation` situation activates automatically (not player-initiated).
**Pressure increase events:**
- Player observes triangle interaction (+10 pressure)
- Player asks NPC about triangle member (+15 pressure)
- Gossip spreads to additional NPCs (+20 pressure per NPC)
- Player confronts one triangle member about another (+30 pressure)
- External event (ring leader suspicious, Commission investigation progresses) (+25 pressure)
**Threshold:** ~75-100 pressure. When crossed, next interaction with any triangle member triggers confrontation dialogue automatically.
**Content implication:** Each triangle needs confrontation dialogue for each member, with knowledge prerequisites.
### 5.3 Information Asymmetry Between Player and NPCs
The most interesting moments are when:
1. You know something an NPC doesn't (exploit their ignorance)
2. An NPC knows something you don't (you're being played)
3. You both know something but pretend you don't (social dance)
**Example 1: You know, they don't**
*The smuggler knows Voss has been skimming from the ring's profits (discovered via evidence). Voss doesn't know the smuggler knows.*
**Gameplay:** You can watch Voss to gather more evidence, confront him when you have enough, or use the information as leverage.
**Monologue:** *"Voss thinks he's being clever. But I've seen the discrepancies."*
**Example 2: They know, you don't**
*THE FRIEND (Kael) knows the smuggler has been asking questions about him. The smuggler doesn't realize Kael knows.*
**Gameplay:** Kael becomes evasive, changes behavior, might confront the smuggler first.
**Dialogue shift:**
```yaml
# Before Kael knows you're investigating
- id: kael_casual_001
access: [insider, peer]
trust: surface
text: "Hey. Usual shift chaos today."
# After Kael knows (relationship state includes 'aware_of_investigation')
- id: kael_guarded_001
access: [insider, peer]
trust: surface
prerequisite:
relationship:
flags: [aware_of_investigation]
text: "Hey. [pause] Everything okay with you?"
```
**Monologue:** *"Kael's acting strange. Did someone tip him off?"*
**Example 3: Mutual knowledge, mutual pretense**
*Both you and an NPC know the ring exists. Neither has acknowledged it explicitly. You're dancing around the truth.*
**Dialogue uses coded language:**
```yaml
- id: voss_coded_001
role: hub_supervisor
access: [insider]
trust: real
situation: private_conversation
prerequisite:
knowledge:
- subject: voss
flag: ring_leader
level: knows_of # You suspect, but haven't confirmed
text: "Some cargo needs... special handling. You understand."
# Player response options include: play along, press for details, refuse
```
**This is the social gameplay.** Not just "know X to unlock Y" but "what you know changes how you interact."
### 5.4 Can NPCs Lie? (Misinformation)
**Yes. Load-bearing mechanic.**
NPCs lie when:
1. **Protecting a secret** (themselves or someone they care about)
2. **Under pressure** (tolerance threshold low, desperate)
3. **Testing you** (seeing if you already know the truth)
4. **Manipulating you** (advancing their own agenda)
**How lies work in the knowledge graph:**
When an NPC tells you something false, it enters your knowledge graph as TRUE (with source: told_by_X). The lie is indistinguishable from truth until you have contradictory knowledge.
**Example:**
```
T=100: Sera (detective's FRIEND) tells you: "Kael was at the dock during second shift."
Detective knowledge: kael_location_second_shift = dock (level: knows_of, source: told_by_sera, confidence: high)
T=200: Detective directly observes Kael in corridor B-7 during second shift
Detective knowledge: kael_location_second_shift = corridor_b7 (level: knows_details, source: direct_observation, confidence: very_high)
T=201: Knowledge system detects contradiction
Both nodes remain in graph, flagged as conflicting
Relationship state updated: sera_lied = true
T=202: Monologue trigger (urgent chime): "Sera told me Kael was at the dock. But I just saw him in the corridor. Why did she lie?"
T=203: Entity color shift: Sera changes from green (trusted) to amber (flagged)
T=210: Next conversation with Sera unlocks confrontation option
```
**This is THE FRIEND's arc (D-034).** The contradiction is discoverable through observation. The lie isn't revealed through dialogue — it's revealed through the knowledge graph detecting the conflict.
**Content implication:** NPCs need both truthful and false dialogue lines for key topics. The NPC's decision to lie depends on:
- Relationship trust (low trust = more likely to lie)
- Secret/vulnerability pressure (protecting something = lie)
- Situation (confrontation = less likely to lie, casual = easier to lie)
**Misinformation vulnerability:** A clever player could spread false information to NPCs, then observe how it propagates. This creates disinformation gameplay.
---
## 6. Character Perspective and Monologue Integration
### 6.1 Single-Character Perspective Constraint (D-001, D-005)
The knowledge graph must reflect CHARACTER knowledge, not player knowledge.
**The split between player and character:**
- **Player sees:** Entities in viewport, visual tells, spatial relationships
- **Character knows:** Interpreted meaning of what they see, context from past experience, emotional weight
**Example:**
*Player sees: Two NPCs having a conversation in the corner of the bar.*
*Character knows (if smuggler, insider access):* "That's Voss and Nils. Probably ring business. They don't usually meet here."
*Character knows (if detective, outsider):* "Two men in the corner. Hushed conversation. Worth noting."
**The monologue bridges the gap.** It translates visual observations into knowledge entries, filtered through character perspective.
**Monologue trigger sequence:**
```
1. Player viewport: Kael enters restricted corridor
2. Perception system: Character has LOS to Kael
3. Knowledge system: Does character recognize Kael? (yes, knows_name + knows_role)
4. Knowledge system: Is this location notable? (yes, restricted_area)
5. Knowledge update: kael_observed_out_of_place = true, level: knows_of
6. Monologue trigger: Character commentary on the observation
7. Monologue rendering: "Kael? What's he doing in B-7?"
```
**If the character DIDN'T recognize Kael:** Monologue would be generic: *"Someone in the corridor. Odd time for that area to be active."*
**Knowledge level determines monologue specificity.**
### 6.2 Internal Monologue and Knowledge State
The monologue system (D-016) must reveal the character's knowledge state to the player.
**Monologue functions:**
1. **Perception bridge** — translate visual observation into narrative meaning
2. **Knowledge commentary** — reveal what the character knows about what they see
3. **Confidence indicator** — show whether character is certain or uncertain
4. **Emotional reaction** — show how the character FEELS about what they know
5. **Contradiction detection** — flag when new knowledge conflicts with prior beliefs
**Monologue examples by knowledge level:**
```yaml
# Unaware (no knowledge about Hael)
- text: "Someone I don't recognize. New face at the hub."
# Suspects (beginning of knowledge)
- text: "I think I've seen them before. Can't place where."
# Knows_of (confirmed but vague)
- text: "That's Hael. Works in maintenance, I think. Keeps to themselves."
# Knows_details (specific knowledge)
- text: "Hael Miran, maintenance second shift. Kael's partner. Started three months ago."
# Understands (contextual meaning)
- text: "Hael. So that's who Kael is risking everything for."
```
**Each level unlocks more detailed monologue.** The player learns what their character knows through the specificity of the commentary.
### 6.3 Different Archetypes Experience Knowledge Differently
**Smuggler (operator archetype):**
- Knowledge focus: threat assessment, operational security, loyalty network
- Monologue style: terse, practical, paranoid
- Knowledge interpretation: "Is this a threat? Can I trust them? What's the risk?"
**Detective (investigator archetype):**
- Knowledge focus: pattern recognition, evidence correlation, procedural gaps
- Monologue style: analytical, methodical, distant
- Knowledge interpretation: "Does this fit the pattern? What's the evidence? What's the procedure?"
**Same observation, different knowledge entry:**
*Event: Kael arrives 15 minutes late to shift*
**Smuggler knowledge:**
```yaml
subject: kael_davan
flag: behavior_change
level: suspects
emotional_weight: concern # Kael is a friend
monologue: "Kael's late again. Third time this week. If he's getting sloppy, the whole operation is at risk."
```
**Detective knowledge:**
```yaml
subject: kael_davan
flag: routine_deviation
level: knows_of
emotional_weight: neutral # Professional interest only
monologue: "Dock worker, second shift. Third late arrival this week. Noted."
```
**Same fact, different interpretation, different emotional weight.** This IS the dual-lens experience.
---
## 7. Risks: What Makes the Knowledge System Feel Cold?
The knowledge graph could feel like a spreadsheet instead of a story. Here's what would kill the narrative:
### 7.1 Knowledge as Checklist
**Risk:** Player treats knowledge like collectibles. "I need to unlock 'knows_details' about Kael to proceed."
**Mitigation:**
1. **No UI display of knowledge flags.** Player never sees "Kael: suspects (2/5 levels)". They experience it through monologue.
2. **Multiple paths to the same information.** You can learn about Kael's situation through observation, through gossip, through evidence. No single required path.
3. **Knowledge doesn't gate story progress, it gates understanding.** You can finish the vertical slice without knowing THE FRIEND's full story. But if you DO know, it means more.
### 7.2 Perfect Memory
**Risk:** Player character remembers everything forever with perfect clarity. No tension, no challenge.
**Mitigation:**
1. **Knowledge decay (D-011 fog returns).** Information about NPC locations becomes stale. "Kael was at the dock" is only true at that timestamp.
2. **Confidence degradation.** Knowledge learned through rumor has lower confidence than direct observation. Over time, confidence decays unless refreshed.
3. **Journal as crutch, not backup brain.** Player can review the journal, but the CHARACTER doesn't consult it constantly. Monologue reflects what the character actively remembers.
### 7.3 Binary Knowledge States
**Risk:** You either know or you don't. No nuance, no investigation feel.
**Mitigation:**
1. **Five-level hierarchy (suspects > knows_of > knows_details > understands).** Gradual revelation.
2. **Partial knowledge is useful knowledge.** Even at `suspects` level, monologue fires. Even at `knows_of` level, new dialogue unlocks. You're playing with incomplete information, which is the point.
### 7.4 NPC Knowledge Invisible
**Risk:** NPCs have knowledge graphs, but player can't read them. The social dynamics are invisible.
**Mitigation:**
1. **Observable tells (D-034).** NPCs change behavior based on their knowledge state. If an NPC knows you're investigating, they act guarded.
2. **Overhearing gossip.** When NPCs share knowledge, you can witness it (if in earshot). This reveals both the information and the social relationship.
3. **Relationship color coding (D-033).** Entity color reflects relationship state, which is partially driven by what the NPC knows about you.
### 7.5 Knowledge Without Stakes
**Risk:** Facts accumulate, but they don't matter emotionally.
**Mitigation:**
1. **Every piece of knowledge about THE FRIEND has emotional weight.** Learning Kael is compromised HURTS because you trusted them.
2. **Knowledge creates obligation.** If you know Kael is trying to exit the ring, you must choose: help them, turn them in, or pretend you don't know. Knowledge is liability.
3. **NPCs react to your knowledge.** If they know you know their secret, the relationship changes. Knowledge has social consequences.
---
## 8. Recommended Next Steps
### 8.1 Resolve Q-016 (Knowledge Hierarchy)
**Proposal:** Five-level hierarchy (unaware, suspects, knows_of, knows_details, understands) with narrative definitions for each level.
**Needs:** Gestalt validation (does this map to mechanics?) and Dudley validation (is this implementable?).
### 8.2 Define v0.1 Knowledge Vocabulary (#309)
**Proposal:** Section 3 of this document provides the taxonomy. Content team needs this to write monologue prerequisites.
**Deliverable:** Formal knowledge vocabulary document with:
- Entity knowledge categories (identity, location, behavior, relationships, secrets, contraband)
- World knowledge categories (locations, events, systems, factions)
- Confidence level schema
- Example monologue prerequisites
**Owner:** Paula (narrative spec) + Gestalt (mechanical validation) + Mellanie (authoring validation)
### 8.3 Design Knowledge-to-Monologue Pipeline
**Question:** How does knowledge state trigger monologue?
**Current gap:** We know monologue has 9 trigger types (D-035). One is `observe_npc`. But how does the knowledge graph interact with the trigger system?
**Proposal:**
1. Observation triggers perception query
2. Perception query checks knowledge graph: "Do I know this entity?"
3. If yes, retrieve knowledge level and recent changes
4. Select monologue from pool filtered by: character, trigger type, knowledge level, emotional state
5. Render monologue with character voice
**Needs:** Gestalt (pipeline design) + Dudley (perception integration)
### 8.4 Prototype THE FRIEND Contradiction Arc
**Critical test case:** The knowledge graph must support THE FRIEND's arc. If it can't handle:
- Kael tells you X
- You observe not-X
- Graph detects contradiction
- Monologue fires (urgent chime)
- Relationship state shifts (color change)
- Dialogue unlocks (confrontation)
...then the system isn't ready.
**Recommendation:** Prototype this sequence as a proof-of-concept. If the knowledge graph can handle THE FRIEND, it can handle the whole game.
### 8.5 Define Gossip Propagation Rules
**Question:** How do NPCs share knowledge?
**Proposal:**
- NPCs have conversation system (not detailed in current architecture)
- During conversation, NPCs can share knowledge based on: relationship trust, personality (gossip tendency), tolerance state, triangle pressure
- Shared knowledge enters recipient's graph with source: `told_by_X`
- Player can observe gossip if in earshot (overhearing mechanic)
**Needs:** Gestalt (conversation system design) + Tyre (performance implications of NPC-to-NPC knowledge transfer)
### 8.6 Integrate Knowledge with Dialogue Access Tiers
**Current state:** D-028 defines four relational layers. D-035 defines tag taxonomy including `access` and `trust` filters.
**Missing piece:** How does knowledge state affect access tier eligibility?
**Proposal:**
- Secret-tier dialogue requires knowledge prerequisite (you must know part of the secret)
- Confrontation dialogue requires `knows_details` level
- NPCs can volunteer information (unprompted disclosure) based on their knowledge of YOUR knowledge ("I heard you were asking about Kael...")
**Needs:** Dialogue system specification update integrating knowledge prerequisites.
---
## 9. Files Referenced
### Decision Files
- `decisions/content.md` — D-023, D-024, D-025, D-028, D-029, D-032, D-034, D-035
- `decisions/perception.md` — D-011, D-015, D-016, D-017, D-018, D-019, D-033
- `decisions/architecture.md` — D-010
- `decisions/scope.md` — D-001, D-005, D-006, D-027, D-039
- `decisions/questions.md` — Q-016, Q-017
### Workshop Files
- `docs/workshops/knowledge-graph-information-boundaries/workshop-brief.md`
### Briefing Files
- `docs/briefings/paula.md`
---
**End of Round 1 Analysis**
The honest truth is this: the knowledge graph is where mechanics and narrative either fuse into something greater, or fail to connect at all. Get this right, and every observation means something. Every conversation has stakes. Every betrayal lands. Get this wrong, and it's just bookkeeping.
What SUSTAINS knowledge as a mechanic across hours of play is that knowledge creates OBLIGATION. Knowing something means you must choose what to do with it. And in a conspiracy story, that choice is never clean.
@@ -0,0 +1,547 @@
# Knowledge Graph & Information Boundaries Workshop — Round 1: SI (Project Management)
**Author:** SI (Project Manager, Scrum Master)
**Date:** 2026-02-11
**Focus:** Ticket landscape, dependency mapping, work breakdown preview, sprint impact, risk assessment
---
## Executive Summary
The Knowledge Graph Workshop (Epic #351) sits at a critical junction in our project architecture. It blocks the Observer Snapshot Pipeline Workshop (#352), intersects directly with Sprint 2's critical path through #112, and affects 9 scattered backlog tickets that need consolidation. The server team has run ahead on #110 (shadowcasting) while blocked by Q-018 — manageable risk, but signals urgency pressure.
**Critical finding:** #112 (Observer visibility query) claims to "respect information boundaries" but the knowledge graph doesn't exist yet. This is a specification gap, not an implementation blocker — the workshop must define what "respects information boundaries" means in code.
**Minimum viable knowledge graph for Sprint 2:** What does visibility filtering need? Not the full gossip/inference/decay system — just the query interface that #112 can call. Workshop must identify the MVP boundary.
---
## 1. Current State: Ticket Inventory
### 1.1 Workshop Epics
| ID | Title | Status | Priority | Blocks |
|----|-------|--------|----------|--------|
| #351 | Workshop: Knowledge Graph & Information Boundaries | backlog | critical | #352 |
| #352 | Workshop: Observer Snapshot Pipeline | backlog | critical | (none tracked) |
**Gap:** #352 doesn't have explicit blockers beyond #351, but Sprint 2 completion (#357) depends on observer snapshot generation working. Implicit dependency chain not captured in tickets.
### 1.2 Sprint 2 Tickets (14 total)
| ID | Title | Status | Priority | Owner | Blocks |
|----|-------|--------|----------|-------|--------|
| **Design Track** |
| #359 | Resolve Q-018: shadowcasting algorithm | ready | critical | server | #110 |
| #358 | Design ObserverSnapshot v2 schema | ready | critical | joint | #112, #113, #25 |
| #360 | Resolve Q-019: entity ID stability | ready | high | joint | #130 |
| **Server Track** |
| #110 | Shadowcasting algorithm - server | **in_progress** | critical | server | #111 |
| #111 | Vision cone implementation | ready | critical | server | #112 |
| #112 | Observer visibility query | ready | critical | server | #113, #356 |
| #25 | Game clock and day-phase system | ready | high | server | #357 |
| **Client Track** |
| #116 | Camera lock to character | ready | critical | client | #357 |
| #129 | Tile rendering engine | ready | critical | client | (none) |
| #130 | Entity sprite management | ready | critical | client | (none) |
| #131 | Fog overlay rendering | ready | critical | client | (none) |
| #113 | Fog rendering - client | ready | critical | client | #356 |
| **Integration Track** |
| #356 | Fog data through bridge | ready | critical | joint | #357 |
| #357 | Sprint 2 proof: fog of perception | ready | critical | joint | (none) |
**Status:** 13 ready, 1 in_progress. Server team working ahead on #110 despite Q-018 blocker.
### 1.3 Knowledge-Related Backlog Tickets (10 total, not in any sprint)
| ID | Title | Type | Status | Priority | Parent | Notes |
|----|-------|------|--------|----------|--------|-------|
| #49 | Information Boundary System | epic | backlog | critical | #31 (Map & Nav) | Parent epic with 6 children |
| #138 | Information tag schema | story | backlog | critical | #49 | Child of #49 |
| #139 | Component-level access control | story | backlog | critical | #49 | Child of #49 |
| #140 | Entity visibility filtering | story | backlog | critical | #49 | Child of #49 |
| #141 | Knowledge-based information gating | story | backlog | high | #49 | Child of #49 |
| #142 | NPC information boundaries | story | backlog | high | #49 | Child of #49 |
| #269 | CauseChain component | story | backlog | high | #49 | Child of #49 (implemented, ticket status stale) |
| #89 | Information inventory | story | backlog | high | #39 (NPC Data) | Standalone, blocks #90 |
| #182 | Divergent starting knowledge | story | backlog | critical | #57 (PCs) | Content-side ticket |
| #264 | Knowledge/journal display | story | backlog | medium | (unlisted) | UI ticket |
| #272 | Information boundary negative test suite | story | backlog | high | (unlisted) | Test infrastructure |
| #309 | Knowledge state vocabulary for v0.1 | story | backlog | high | #55 (Dialogue) | Content blocker |
**Key observation:** Epic #49 (Information Boundary System) has 6 children. #89 (Information inventory) is standalone under NPC Data. These should be reconciled — either #89 becomes a child of #49, or #351 subsumes both.
**CauseChain note (#269):** The audit report and D-030 indicate CauseChain is already implemented (`server/src/cause_chain.rs`). Ticket status is stale. Should be marked `done` or retitled as integration work.
---
## 2. Dependency Mapping
### 2.1 Sprint 2 Critical Path to Proof (#357)
```
Blocking relationships (from ticket_deps table):
#359 (Q-018 decision) ──► #110 (shadowcast)
#111 (vision cone)
#358 (snapshot v2) ────────► #112 (observer query) ──┬─► #113 (fog render) ──┐
│ │ │ │
├─────────────────────────┴──► #356 (bridge) ◄─┘ │
│ │ │
└──► #25 (time) ──────────────┬────┘ │
│ │
#116 (camera lock) ──────────────────┴─────────────────────────────────────┬──┘
#357 (proof)
```
**Critical path (longest):**
`#359 → #110 → #111 → #112 → #356 → #357` (6 tickets serial)
**Parallel tracks:**
- `#358 → #25 → #357` (3 tickets, merges at #357)
- `#116 → #357` (independent, merges at #357)
- Client rendering (#129, #130, #131, #113) mostly independent until #113 waits on #112
**Observation:** #358 (ObserverSnapshot v2) gates three parallel tracks (#112, #113, #25). This is a design-blocker bottleneck. Joint team must resolve #358 early in sprint to unblock all three.
### 2.2 Knowledge Graph Workshop Dependencies
```
Architecture Audit (2026-02-11)
Consensus Rec #12: "Knowledge graph workshop NOW"
#351 (Knowledge Graph Workshop) ◄──── implicitly affects ──── #112 (observer query)
│ │
│ "respects info boundaries"
│ │
└──► blocks ──► #352 (Observer Snapshot Pipeline) │
#49 (Info Boundary System epic) ◄─── overlaps ──────────────────┘
├─► #138 (tag schema)
├─► #139 (access control)
├─► #140 (entity visibility filtering) ◄───── related to #112
├─► #141 (knowledge-based gating)
├─► #142 (NPC info boundaries)
└─► #269 (CauseChain) [already implemented]
#89 (Information inventory) ◄─── overlaps with #351 ─── replaces Vec<String> placeholder
#309 (Knowledge vocab for v0.1) ◄─── content-side dependency ─── needs knowledge model
```
**Gap identified:** Epic #49 predates the workshop but covers overlapping scope. Post-workshop, #49 should either:
1. Be consolidated under #351 as implementation tickets, OR
2. Be re-scoped as the client-facing information system (UI, tags, access control) while #351 owns the knowledge graph data model
**Recommendation:** Treat #351 as the architecture/data model workshop, and #49's children (#138-#142) as implementation stories that depend on #351's outputs.
---
## 3. Gap Analysis: #112 "Respects Information Boundaries"
### 3.1 The Problem
Ticket #112 (Observer visibility query) description:
> "Given observer entity, return visible entities and tiles. **Respects information boundaries.** Feeds ObserverSnapshot."
**Question:** What does "respects information boundaries" mean in code?
From D-010 principle 2:
> "Every piece of game state is tagged with who knows it."
From current implementation (`server/src/npc/mod.rs` lines 46-48):
```rust
#[derive(Component, Debug, Clone, Serialize, Deserialize)]
pub struct InformationInventory {
pub known_facts: Vec<String>,
}
```
**The gap:** #112 needs to query "what does entity A know about entity B" but the knowledge graph is a `Vec<String>` placeholder. The observer query can't "respect information boundaries" until the knowledge model exists.
### 3.2 Is This a Blocker?
**No — it's a specification gap, not an implementation blocker.**
For Sprint 2's "fog of perception" proof, visibility is primarily geometric (LOS + vision cone). The full knowledge graph (gossip, inference, memory decay) isn't needed yet.
**Minimum viable implementation for #112:**
- Observer can see entities within LOS range
- Vision cone modulates range (forward = full, peripheral = reduced, behind = blind)
- Walls block vision (read from WalkabilityMap)
- No knowledge-based filtering for Sprint 2
**Knowledge-based filtering comes later:**
- "Can you identify this person?" (knows vs unknown)
- "Do you know what they're carrying?" (requires prior observation)
- "Do you know their secret?" (requires dialogue/investigation)
**Recommendation:** Workshop should define the query interface (#112 needs) even if full implementation is deferred. Sprint 2 uses geometric visibility only; Sprint 3+ adds knowledge filtering.
---
## 4. Work Breakdown Preview
### 4.1 Expected Workshop Outputs (from brief)
1. **Decision: D-0XX — Knowledge Graph Data Model** — Rust struct definitions
2. **Decision: Resolution of Q-016** — Knowledge hierarchy (suspects < knows_of < knows_details)
3. **Design document:** Knowledge flow specification
4. **Tickets:** Implementation tasks broken down from design
5. **Performance budget:** Memory/query time targets at 80 NPCs
### 4.2 Anticipated Ticket Structure Post-Workshop
Epic #351 will likely spawn:
| Type | Title (projected) | Priority | Notes |
|------|-------------------|----------|-------|
| story | Implement KnowledgeGraph component | critical | Replace `Vec<String>` with structured graph |
| story | Knowledge confidence/hierarchy system | critical | Resolves Q-016 |
| task | Observer knowledge query interface | critical | What #112 calls to filter entities |
| story | Knowledge propagation: observation | high | How seeing something adds knowledge |
| story | Knowledge propagation: told/gossip | high | How NPCs share knowledge |
| story | Knowledge decay over time | medium | D-011 "fog returns when you leave" |
| task | Spatial indexing for knowledge queries | medium | Performance at 80 NPCs |
| story | Knowledge-based entity identification | medium | "Who is this person?" vs "unknown person" |
| task | Knowledge graph unit tests | high | Known scenarios, deterministic outcomes |
| story | Integrate knowledge into dialogue system | high | D-028 tagged line pools need knowledge prereqs |
**Consolidation opportunity:** Tickets #138 (tag schema), #140 (entity visibility filtering), #141 (knowledge gating), #142 (NPC boundaries) should become children of workshop output stories.
### 4.3 Integration with Existing Tickets
| Existing Ticket | Relationship to #351 |
|----------------|----------------------|
| #89 (Information inventory) | DUPLICATE — subsume into #351 knowledge graph story |
| #138 (Information tag schema) | CHILD — becomes implementation task for knowledge tags |
| #140 (Entity visibility filtering) | CHILD — becomes implementation of observer query interface |
| #141 (Knowledge-based gating) | CHILD — uses knowledge graph for access control |
| #142 (NPC information boundaries) | CHILD — NPC knowledge = KnowledgeGraph component per entity |
| #269 (CauseChain) | RELATED — provenance system (already done per audit) |
| #309 (Knowledge vocab for v0.1) | DEPENDS ON — content team needs knowledge model first |
| #272 (Info boundary test suite) | DEPENDS ON — tests require implemented knowledge system |
**Post-workshop ticket reconciliation required.**
---
## 5. Sprint 2 Impact Assessment
### 5.1 Does #351 Block Sprint 2 Completion?
**Short answer: No, but it creates technical debt.**
Sprint 2 proof (#357): "Walk into a room and see the fog" — visibility-driven, not knowledge-driven. The geometric perception system (#110, #111, #112) can ship without full knowledge graph.
**What Sprint 2 needs from #351:**
1. **Query interface design:** What does #112 call to ask "what can this observer see?" Even if the answer is always "everything in LOS" for Sprint 2, the interface contract must exist.
2. **Stub implementation path:** #112 can call `KnowledgeGraph::can_observe(observer, target) -> bool` that returns `true` for Sprint 2. Full logic comes later.
**What Sprint 2 does NOT need:**
- Knowledge propagation (gossip, inference)
- Knowledge decay over time
- Knowledge confidence levels
- NPC-to-NPC knowledge sharing
### 5.2 Risk: Rework if Workshop Runs Long
If #351 isn't resolved before Sprint 2 starts, the server team will implement #112 with placeholder logic. Risk of rework if workshop produces incompatible interface design.
**Mitigation:** Workshop Round 1 (this round) runs concurrent with Sprint 2 kickoff. Round 2 synthesis should complete before #112 implementation starts (currently blocked by #111 anyway).
### 5.3 Critical Path Timing
Current sprint state:
- #110 (shadowcasting) is **in_progress** despite blocked by #359 (Q-018)
- #359 (Q-018) is a decision ticket (algorithm selection) — fast resolution possible
- #358 (ObserverSnapshot v2) is a design ticket — joint team bottleneck
**Timeline pressure:**
```
Week 1: #359 (Q-018) resolved, #110 continues
Week 1-2: #358 (snapshot v2) design — CRITICAL BOTTLENECK
Week 2: #111 (vision cone) starts after #110 done
Week 2-3: #112 (observer query) — needs knowledge interface from #351
Week 3: Integration (#356, #357)
```
**Recommendation:** Workshop Round 2 synthesis must complete by end of Week 1 to provide #112 with interface spec.
---
## 6. Risk Assessment
### 6.1 RISK: Server Team Running Ahead on #110
**Status:** #110 (shadowcasting) is `in_progress`, blocked by #359 (Q-018 decision).
**Analysis:**
- Q-018: "symmetric shadowcasting vs recursive shadowcasting"
- This is an algorithm selection decision, not a knowledge graph dependency
- Server team working ahead signals they're confident Q-018 will resolve quickly
- Risk: If Q-018 reverses their choice, rework required
**Severity:** LOW. Q-018 is a technical benchmarking question, not a design debate. Tyre and Dudley can resolve this in Round 1 with perf data.
**Action:** No intervention needed. Track #359 status. If blocked >3 days, escalate.
### 6.2 RISK: #112 Says "Respects Information Boundaries" But No Model Exists
**Covered in Section 3.** Summary:
- Not a Sprint 2 blocker (geometric visibility sufficient)
- Workshop must define query interface even if full implementation deferred
- Risk of rework if #112 implements placeholder that conflicts with workshop output
**Severity:** MEDIUM. Mitigated by workshop timeline (Round 2 before #112 starts).
**Action:** Workshop participants must explicitly address "What query interface does #112 need?" in Round 1 analyses.
### 6.3 RISK: #358 (ObserverSnapshot v2) as Design Bottleneck
**Status:** #358 blocks 3 parallel tracks (#112, #113, #25). Joint team (Tyre) must design expanded schema.
**Dependencies:**
- Needs visibility data structure (tiles? bitmap? fog states?)
- Needs time data format (tick? game_time? day_phase?)
- Needs facing direction representation (angle? cardinal? sector?)
**Timeline:** Must complete Week 1 to unblock Week 2 work.
**Severity:** HIGH if delayed. Currently ready status = no blocker.
**Action:** Prioritize #358 at sprint kickoff. Joint team (Tyre + Oscar) design session.
### 6.4 RISK: Scattered Backlog Tickets Create Duplicate Work
**Issue:** Epic #49 (Information Boundary System) and workshop #351 overlap significantly. Both are `critical` priority, neither in a sprint.
**Risk scenarios:**
1. Workshop produces design that conflicts with #49's child ticket assumptions
2. Implementation team picks up #49 children before #351 workshop completes
3. Post-workshop, unclear which tickets are superseded vs still valid
**Severity:** MEDIUM. Organizational risk (confusion, duplicate work) not technical risk.
**Action:** Post-workshop ticket reconciliation required. Mark duplicates, reparent children, update dependencies.
### 6.5 RISK: Performance Budget Unvalidated
**From audit:** At 80 Active NPCs, each with knowledge about ~50 entities, naive implementation = O(N^2) queries.
**Workshop must produce:**
- Memory budget: How much RAM for 80 NPCs × 50 knowledge entries?
- Query time budget: How fast must `can_observe()` return to stay within 100ms tick budget?
- Spatial indexing strategy: Grid-based cache? Dirty flagging?
**Severity:** LOW for Sprint 2 (15 NPCs). HIGH for full Active tier (80 NPCs).
**Action:** Workshop Round 1 (Tyre) should include back-of-envelope calculations. Round 2 validates or revises.
### 6.6 RISK: Knowledge Vocabulary (#309) Blocks Content Authoring
**Issue:** Content team needs knowledge flags (`knows:smuggling_operation`, `suspects:manifest`) to write monologue prerequisites. Can't author until vocabulary exists.
**Dependencies:** #309 blocks #299 (smuggler opening) and #300 (detective opening).
**Severity:** MEDIUM. Content isn't Sprint 2 blocker, but it's a vertical slice blocker (Sprint 4-5).
**Action:** Workshop should produce initial vocabulary list. Doesn't need to be exhaustive — content team can extend it — but needs the taxonomy structure (prefix:subject format, hierarchy levels).
---
## 7. Recommendations for Round 2
### 7.1 Questions for Workshop Participants
**For Tyre (architecture lead):**
1. What is the query interface that #112 (observer visibility query) needs?
2. Can Sprint 2 ship with a stub implementation (`can_observe() -> true`) or does the real logic need to exist?
3. Performance budget: memory and query time at 80 NPCs.
**For Gestalt (mechanics):**
1. What are the knowledge hierarchy levels for Q-016? (suspects < knows_of < knows_details)
2. How does knowledge confidence decay over time? (D-011 "fog returns")
3. What gameplay-visible behaviors require knowledge filtering vs geometric visibility?
**For Paula (narrative):**
1. What initial knowledge vocabulary is needed for v0.1 monologue authoring? (addresses #309)
2. How does divergent starting knowledge (#182) manifest in the knowledge graph?
3. Does THE FRIEND pattern require special knowledge graph behavior?
**For Dudley (implementation):**
1. Can `InformationInventory { Vec<String> }` survive Sprint 2 as-is, or does it need replacement now?
2. What does bevy_ecs entity-to-entity knowledge storage look like? (Component per entity? Resource with HashMap?)
3. How is CauseChain (#269, already implemented) integrated with knowledge provenance?
**For Qatux (docs):**
1. Post-workshop: which decisions need briefing updates?
2. Are there implicit dependencies in existing decisions that this workshop exposes?
**For SI (this analysis):**
1. What tickets are duplicates vs distinct work?
2. What's the post-workshop ticket reconciliation plan?
### 7.2 Required Outputs for Sprint 2 Unblocking
Minimum workshop outputs to unblock #112:
1. **Query interface spec:** Function signature for "can observer see target?"
2. **Stub implementation strategy:** What does Sprint 2 return? (always true? geometric only?)
3. **Data structure sketch:** Even if not fully implemented, what replaces `Vec<String>`?
Without these, #112 will implement placeholder logic that may conflict with final design.
### 7.3 Ticket Reconciliation Plan (Post-Workshop)
After workshop produces decisions and tickets:
1. **Review Epic #49 children** (#138, #139, #140, #141, #142):
- Mark duplicates of workshop output as `cancelled` or consolidate
- Reparent distinct work under appropriate workshop stories
- Update all blocking relationships
2. **Consolidate #89 (Information inventory)**:
- If duplicate of knowledge graph story → mark `cancelled`, reference workshop ticket
- If distinct (UI component) → update dependencies
3. **Resolve #269 (CauseChain) status**:
- Audit says implemented (`server/src/cause_chain.rs`)
- Mark `done` or retitle as integration task
4. **Update Sprint 2 ticket dependencies**:
- Add explicit dependency: #351 blocks #112 (or workshop output ticket blocks #112)
- Update #112 description with interface contract once defined
5. **Propagate to content tickets**:
- #309 (knowledge vocab) depends on #351 output
- #182 (divergent starting knowledge) depends on knowledge model
- Update blocking relationships
---
## 8. Dependency Graph Summary
### 8.1 Before Workshop
```
Scattered tickets, overlapping scope:
#49 (Info Boundary System epic)
├─ #138, #139, #140, #141, #142, #269
#89 (Information inventory)
#351 (Knowledge Graph Workshop) ────► blocks ────► #352 (Observer Snapshot)
#112 (observer query) ────► implicit dependency ────► ???
```
### 8.2 After Workshop (projected)
```
#351 (Knowledge Graph Workshop)
├─► D-0XX (Knowledge Graph Data Model decision)
├─► D-0XX (Resolution of Q-016)
├─► Story: Implement KnowledgeGraph component
│ ├─► Task: Observer knowledge query interface ◄──── #112 depends on this
│ ├─► Task: Knowledge tag schema (was #138)
│ └─► Unit tests
├─► Story: Knowledge propagation (observation, gossip, inference)
│ ├─► Task: Observation adds knowledge
│ ├─► Task: NPC knowledge sharing (was #142)
│ └─► Integration tests
├─► Story: Knowledge decay over time
│ └─► Task: Spatial/temporal decay rules
└─► Story: Knowledge-based information gating (was #141)
├─► Task: Entity identification filtering (was #140)
├─► Task: Component access control (was #139)
└─► Negative test suite (was #272)
#89 (Information inventory) ────► consolidate into KnowledgeGraph story
#309 (Knowledge vocab) ◄──── depends on ──── Knowledge model
#182 (Divergent starting knowledge) ◄──── depends on ──── Knowledge model
#351 ────► blocks ────► #352 (Observer Snapshot Pipeline Workshop)
```
---
## 9. Sprint Impact Scorecard
| Factor | Score | Notes |
|--------|-------|-------|
| **Blocks Sprint 2 completion?** | NO | Geometric visibility sufficient for proof |
| **Affects Sprint 2 tickets?** | YES | #112 interface design needed |
| **Requires ticket rework?** | MEDIUM | If #112 starts before workshop done |
| **Creates sprint delay risk?** | LOW | Workshop Round 2 aligns with #112 timeline |
| **Organizational complexity?** | HIGH | 10 backlog tickets need reconciliation |
| **Technical debt if deferred?** | HIGH | Placeholder will calcify, rework expensive |
**Conclusion:** Workshop is correctly prioritized as CRITICAL. Timeline allows Sprint 2 to proceed with stub implementation, full knowledge system delivers Sprint 3+.
---
## 10. Summary: What SI Needs from Round 2
1. **Query interface spec** — concrete function signature for #112 to call
2. **Sprint 2 stub strategy** — what does `can_observe()` return for fog proof?
3. **Ticket inventory** — which workshop output tickets supersede which backlog tickets?
4. **Performance budget** — memory/query time targets for Active tier (80 NPCs)
5. **Knowledge vocabulary taxonomy** — structure for #309 to populate
6. **Timeline confirmation** — can workshop Round 2 complete before #112 implementation starts (Week 2)?
With these inputs, SI can:
- Create implementation tickets from workshop design
- Reconcile/close duplicate backlog tickets
- Update Sprint 2 dependencies if needed
- Scope Sprint 3 knowledge system work
- Unblock content team (#309, #182)
---
## Appendix: Ticket Queries for Round 2 Synthesis
### All knowledge-related backlog tickets
```sql
SELECT id, title, type, status, priority, parent_id
FROM tickets
WHERE status = 'backlog'
AND (title LIKE '%knowledge%' OR title LIKE '%information%' OR title LIKE '%boundary%')
ORDER BY priority DESC, id;
```
### Epic #49 full tree
```sql
WITH RECURSIVE tree AS (
SELECT id, title, type, parent_id, 0 as depth FROM tickets WHERE id = 49
UNION ALL
SELECT t.id, t.title, t.type, t.parent_id, tree.depth + 1
FROM tickets t JOIN tree ON t.parent_id = tree.id
)
SELECT * FROM tree ORDER BY depth, id;
```
### All tickets blocking Sprint 2 proof (#357)
```sql
WITH RECURSIVE blockers AS (
SELECT blocker_id as id FROM ticket_deps WHERE blocked_id = 357
UNION
SELECT td.blocker_id FROM ticket_deps td JOIN blockers b ON td.blocked_id = b.id
)
SELECT DISTINCT t.id, t.title, t.status FROM tickets t
JOIN blockers b ON t.id = b.id
ORDER BY t.id;
```
Result: 9 tickets in critical path (#359, #358, #110, #111, #112, #113, #356, #116, #25)
---
**End of Round 1 Analysis — SI**
@@ -0,0 +1,896 @@
# Round 1: Tyre (Technical Architect) -- Knowledge Graph & Information Boundaries
**Workshop:** Knowledge Graph & Information Boundaries (Epic #351)
**Perspective:** Architecture & Feasibility
**Date:** 2026-02-11
---
## Executive Summary
*cracks knuckles*
Let me be honest about what this means technically. The knowledge graph is the hardest piece of architecture remaining in the project. Not because any individual part is difficult -- each piece is a well-understood data structure problem -- but because this system sits at the intersection of EVERY other system: perception, dialogue, monologue, entity color, observer snapshots, simulation tiers, save/load, and the IPC bridge. A bad data model here cascades everywhere. A good one simplifies everything downstream.
The good news: the constraints actually align well. D-010 principle 2 (information boundaries), D-026 (simulation tiers), and the existing bevy_ecs component model all push toward the SAME design. The knowledge graph should be a per-entity ECS component holding a BTreeMap of knowledge entries keyed by a stable entity ID. Not a centralized resource. Not a graph database. A component.
The even better news: Sprint 2 can ship with a stub that is architecturally correct but feature-incomplete. The interface contract matters more than the implementation depth. I will define exactly what that stub looks like.
Tier assessment:
- **Data structure design:** Feasible. Challenging but doable. The core struct is ~60 lines of Rust.
- **Integration with observer snapshots (#112):** Feasible. Single function signature, feeds directly into existing pipeline.
- **Knowledge decay (D-011):** Moderate difficulty. Needs tick-based aging, but the time system already exists.
- **Full knowledge propagation (gossip, inference):** Extremely difficult. Sprint 3+ at the earliest. Do not attempt in Sprint 2.
- **Sprint 2 stub:** Easy. 2-3 days of implementation for a correct, testable, extensible skeleton.
---
## 1. Integration with bevy_ecs Architecture (D-020)
### Component vs Resource vs Hybrid
This is the first architecture decision and it determines everything else.
**Option A: Per-entity Component (recommended)**
```rust
#[derive(Component, Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeGraph {
/// What this entity knows about other entities.
/// BTreeMap for deterministic iteration (D-010 principle 4).
pub entities: BTreeMap<StableId, EntityKnowledge>,
/// Non-entity facts (locations discovered, events witnessed, abstract knowledge).
pub facts: BTreeMap<FactId, FactKnowledge>,
}
```
Advantages:
- Natural ECS pattern. Query `(&KnowledgeGraph, &TilePosition)` in perception systems.
- Automatically participates in bevy_ecs archetype storage -- entities with knowledge graphs are grouped together.
- `Changed<KnowledgeGraph>` dirty-flagging works out of the box for efficient observer snapshot delta compression.
- Entity despawn automatically cleans up the knowledge graph. No orphaned references in a central store.
- Serialization for save/load is per-entity, matching D-026 tier serialization (State-saved NPCs serialize their components individually).
- Multiplayer: each observer's knowledge graph is already isolated. No shared mutable state to synchronize.
Disadvantages:
- Cross-entity queries ("who knows about entity X?") require iterating all KnowledgeGraphs. O(N) at worst.
- Memory is distributed, not cache-local for batch operations.
**Option B: Centralized Resource**
```rust
#[derive(Resource)]
pub struct WorldKnowledge {
pub graphs: BTreeMap<Entity, KnowledgeGraph>,
}
```
Advantages:
- Single place to query cross-entity knowledge.
- Potentially better cache behavior for batch updates.
Disadvantages:
- Does NOT participate in bevy_ecs archetype queries. Cannot use `Query<&KnowledgeGraph>`.
- Does NOT get `Changed<T>` dirty tracking.
- Manual lifecycle management: must sync with entity spawn/despawn.
- Complicates save/load: must serialize the entire resource separately from entity components.
- Breaks the per-entity component model established by D-024.
- Multiplayer: shared mutable resource across all observers is a synchronization nightmare.
**Option C: Hybrid (Component + secondary index Resource)**
```rust
// Component (source of truth)
#[derive(Component)]
pub struct KnowledgeGraph { /* ... */ }
// Resource (read-only index, rebuilt periodically)
#[derive(Resource)]
pub struct KnowledgeIndex {
/// Reverse lookup: who knows about entity X?
pub known_by: BTreeMap<StableId, Vec<Entity>>,
}
```
This is the correct long-term architecture. The Component is authoritative. The Resource is a secondary index rebuilt every N ticks (or on change detection). But the index is a Sprint 3+ optimization -- Sprint 2 does not need reverse lookups.
**Decision: Component (Option A) for Sprint 2. Add reverse index Resource (Option C) when cross-entity queries become a measured bottleneck.**
---
## 2. The Data Model
### Core Types
```rust
use std::collections::BTreeMap;
use serde::{Serialize, Deserialize};
/// Stable entity identifier that survives save/load cycles.
/// NOT a bevy_ecs Entity (which is a generational index).
/// Resolves Q-019 for knowledge graph purposes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct StableId(pub u64);
/// Typed fact identifier for non-entity knowledge.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct FactId(pub String);
/// What entity A knows about entity B.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityKnowledge {
/// Last position this entity was observed at.
pub last_known_position: Option<TilePosition>,
/// Tick when this entity was last directly observed.
pub last_observed_tick: u64,
/// Tick when this knowledge entry was last updated (by any source).
pub last_updated_tick: u64,
/// How confident is this knowledge?
pub confidence: KnowledgeConfidence,
/// How did this entity learn this?
pub source: KnowledgeSource,
/// Relationship assessment (drives D-033 entity color).
pub relationship: RelationshipState,
/// Known attributes of the target (name, role, faction, etc.)
pub known_attributes: BTreeMap<AttributeKey, AttributeValue>,
}
/// Knowledge confidence level (resolves Q-016).
/// Discrete enum, NOT a continuous float.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum KnowledgeConfidence {
/// Outdated information -- haven't seen/heard in a long time.
/// Knowledge has decayed. May still be used but with low trust.
Stale,
/// Third-hand information. Someone mentioned it.
Rumor,
/// Reasonable inference from available information.
Inferred,
/// First-hand observation or direct conversation.
Observed,
/// Currently in line of sight. Maximum confidence.
Direct,
}
/// How the knowledge was acquired. Tracked per-entry, not per-graph.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KnowledgeSource {
/// Directly seen by this entity's LOS.
DirectObservation { tick: u64 },
/// Heard (D-018 sound model -- medium/long range).
Heard { tick: u64, range: SoundRange },
/// Told by another entity (dialogue, gossip).
ToldBy { source_id: StableId, tick: u64 },
/// Inferred from other knowledge.
Inferred { basis: Vec<FactId> },
/// Starting knowledge (character background, D-013 insert data).
Background,
}
/// Sound range classification from D-018.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum SoundRange {
Close,
Medium,
Long,
}
/// Relationship state drives D-033 entity color.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RelationshipState {
Unknown,
Known,
Friendly,
PersonOfInterest,
Hostile,
}
/// Non-entity fact knowledge.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FactKnowledge {
pub confidence: KnowledgeConfidence,
pub source: KnowledgeSource,
pub acquired_tick: u64,
}
/// Attribute keys for known entity properties.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum AttributeKey {
Name,
Role,
Faction,
Workplace,
Routine,
Custom(String),
}
/// Attribute values -- typed for common cases, string fallback.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AttributeValue {
Text(String),
Flag(bool),
Number(f32),
}
/// The per-entity knowledge component.
#[derive(Component, Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeGraph {
pub entities: BTreeMap<StableId, EntityKnowledge>,
pub facts: BTreeMap<FactId, FactKnowledge>,
}
impl KnowledgeGraph {
pub fn new() -> Self {
Self {
entities: BTreeMap::new(),
facts: BTreeMap::new(),
}
}
/// Query: does this entity know about another entity at all?
pub fn knows_entity(&self, id: &StableId) -> bool {
self.entities.contains_key(id)
}
/// Query: what confidence level for a known entity?
pub fn confidence_of(&self, id: &StableId) -> Option<KnowledgeConfidence> {
self.entities.get(id).map(|k| k.confidence)
}
/// Query: what is the relationship state with a known entity?
/// Returns Unknown for entities not in the graph.
pub fn relationship_with(&self, id: &StableId) -> RelationshipState {
self.entities
.get(id)
.map(|k| k.relationship)
.unwrap_or(RelationshipState::Unknown)
}
/// Query: does this entity know a specific fact?
pub fn knows_fact(&self, id: &FactId) -> bool {
self.facts.contains_key(id)
}
/// Query: fact confidence at or above a threshold?
/// This is the monologue prerequisite check (D-035 `prerequisite` tag).
pub fn fact_at_least(&self, id: &FactId, min: KnowledgeConfidence) -> bool {
self.facts
.get(id)
.map(|f| f.confidence >= min)
.unwrap_or(false)
}
/// Record a direct observation of another entity.
pub fn observe_entity(
&mut self,
target: StableId,
position: TilePosition,
tick: u64,
) {
let entry = self.entities.entry(target).or_insert_with(|| EntityKnowledge {
last_known_position: None,
last_observed_tick: 0,
last_updated_tick: 0,
confidence: KnowledgeConfidence::Direct,
source: KnowledgeSource::DirectObservation { tick },
relationship: RelationshipState::Unknown,
known_attributes: BTreeMap::new(),
});
entry.last_known_position = Some(position);
entry.last_observed_tick = tick;
entry.last_updated_tick = tick;
entry.confidence = KnowledgeConfidence::Direct;
entry.source = KnowledgeSource::DirectObservation { tick };
}
/// Decay knowledge based on elapsed ticks since last observation.
/// Called periodically (not every tick -- see performance section).
pub fn decay(&mut self, current_tick: u64, decay_thresholds: &DecayThresholds) {
for (_id, knowledge) in self.entities.iter_mut() {
let age = current_tick.saturating_sub(knowledge.last_observed_tick);
if knowledge.confidence == KnowledgeConfidence::Direct {
// Direct downgrades to Observed when no longer in LOS.
// This is handled by the perception system, not decay.
continue;
}
if age > decay_thresholds.stale_after {
knowledge.confidence = KnowledgeConfidence::Stale;
} else if age > decay_thresholds.decay_after
&& knowledge.confidence > KnowledgeConfidence::Inferred
{
// Step down one level.
knowledge.confidence = knowledge.confidence.decayed();
}
}
}
}
/// Configuration resource for knowledge decay rates.
#[derive(Resource, Debug, Clone)]
pub struct DecayThresholds {
/// Ticks before knowledge begins decaying (D-011 "fog returns").
/// At 10 tps: 600 ticks = 1 game-hour.
pub decay_after: u64,
/// Ticks before knowledge becomes Stale.
pub stale_after: u64,
}
impl KnowledgeConfidence {
/// Step down one confidence level.
pub fn decayed(self) -> Self {
match self {
Self::Direct => Self::Observed,
Self::Observed => Self::Inferred,
Self::Inferred => Self::Rumor,
Self::Rumor => Self::Stale,
Self::Stale => Self::Stale,
}
}
}
```
### Why Discrete Confidence, Not Continuous Float
This is a deliberate architecture call. Let me be direct about the tradeoffs.
**Discrete enum (recommended):**
- Maps directly to Q-016 hierarchy: `suspects` < `knows_of` < `knows_details` becomes `Rumor` < `Inferred` < `Observed`.
- Monologue prerequisite checks (D-035 `prerequisite` tag) are exact comparisons, not threshold tuning.
- Dialogue access tiers (D-028) map cleanly: `surface` available at Rumor+, `real` at Observed+, `secret` at Direct relationship + high trust.
- Entity color (D-033) maps cleanly: each RelationshipState + KnowledgeConfidence combo produces a deterministic color.
- Deterministic. No floating-point comparison issues (D-010 principle 4).
- Debuggable. "This NPC has Observed confidence about Kael" is immediately meaningful. "This NPC has 0.73 confidence about Kael" is not.
**Continuous float (rejected for now):**
- More granular decay curves.
- Allows weighting by source reliability.
- But: introduces floating-point determinism risk, requires threshold tuning, harder to debug, and the game design does not need sub-level granularity.
**If we discover later that 5 levels are insufficient:** adding a sixth enum variant is a minor change. Converting from enum to float is a major refactor. Start discrete, promote to continuous only if measured need arises. This is a "design for it now, build it later" boundary that works cleanly.
### Why Source Is Per-Entry, Not Per-Graph
Each knowledge entry has its own source because the SAME entity can know different things about the same target through different channels. Kael Davan's smuggler character might:
- Know Sera Venn's name from Background (starting knowledge)
- Know Sera's workplace from DirectObservation (saw her at the terminal)
- Know Sera is investigating manifests from ToldBy (gossip at the bar)
Per-graph source would flatten this into one channel. Per-entry preserves provenance, which CauseChain (D-030) needs for monologue trigger explanations.
---
## 3. Interaction with Simulation Tiers (D-026)
This is where a single design decision solves multiple problems simultaneously. *That is actually elegant.*
| Tier | Knowledge Graph Behavior | Serialization |
|------|-------------------------|---------------|
| Active (30-80) | Full KnowledgeGraph component. Updated every perception tick. Decay runs. | In-memory bevy_ecs component |
| Background (500-2K) | KnowledgeGraph component present but NOT updated by perception. Decay runs at background tick rate (1/game-minute). Knowledge frozen at moment of tier transition. | In-memory bevy_ecs component |
| State-saved (10K+) | KnowledgeGraph serialized as part of entity state blob. ~0.5-2KB per entity depending on knowledge count. Deserialized on reactivation. | bincode or MessagePack to disk/memory |
| Ungenerated | No knowledge graph. Generated with starting knowledge on first instantiation. | N/A |
**Tier transition protocol:**
Active -> Background: No action needed. The KnowledgeGraph component stays attached. Background tick systems just do not run perception queries against it. Decay continues at reduced rate.
Background -> State-saved: Serialize the KnowledgeGraph component along with all other components. The BTreeMap serializes deterministically (sorted key order). Store as part of the entity state blob per D-026.
State-saved -> Active: Deserialize. Run a single decay pass to account for elapsed time since serialization. Mark all `Direct` confidence entries as `Observed` (entity was not actually visible during storage).
**This works because KnowledgeGraph is just a Component.** bevy_ecs component add/remove for tier transitions (D-026 explicitly calls this out) handles the lifecycle automatically. No special knowledge graph tier logic needed -- it rides the existing tier infrastructure.
### Serialization Strategy
BTreeMap serializes to a sorted sequence of key-value pairs in both MessagePack and bincode. Deterministic. Stable across runs. No iteration-order ambiguity. This is why BTreeMap, not HashMap -- the architecture review consensus (HashMap ban in simulation code) pays dividends here.
Estimated serialized size per KnowledgeGraph:
- 50 entity entries x ~80 bytes each = ~4KB
- 20 fact entries x ~40 bytes each = ~800B
- Total: ~5KB per entity with a populated knowledge graph
At 10,000 State-saved NPCs (worst case, all with knowledge): ~50MB. Acceptable for a modern game. In practice, most State-saved NPCs are Tier 3 filler with <5 knowledge entries (~400B each).
---
## 4. Observer Visibility Query Interface (#112)
This is the concrete function signature that ticket #112 needs. The observer visibility query asks: "given this observer, what can they see, and what do they KNOW about what they see?"
```rust
/// Result of an observer visibility query.
/// This feeds directly into ObserverSnapshot assembly.
pub struct VisibilityResult {
/// Entities currently in line of sight.
pub visible_entities: Vec<VisibleEntityData>,
/// Tiles visible to the observer (for fog rendering).
pub visible_tiles: Vec<TilePosition>,
/// Sound events the observer can hear (D-018).
pub audible_events: Vec<AudibleEvent>,
/// Knowledge-driven data: entities the observer KNOWS ABOUT
/// but cannot currently see (last known position for fog-of-war).
pub remembered_entities: Vec<RememberedEntity>,
}
/// Visible entity with knowledge overlay.
pub struct VisibleEntityData {
pub stable_id: StableId,
pub position: TilePosition,
/// Relationship color for D-033, derived from knowledge graph.
pub relationship_state: RelationshipState,
/// What the observer knows about this entity.
/// None if this is the first time seeing them.
pub knowledge: Option<EntityKnowledge>,
}
/// Entity remembered but not currently visible.
/// Rendered as "ghost" or last-known-position marker in fog.
pub struct RememberedEntity {
pub stable_id: StableId,
pub last_known_position: TilePosition,
pub confidence: KnowledgeConfidence,
pub relationship_state: RelationshipState,
/// How old is this memory? Drives rendering (fade with age).
pub ticks_since_observed: u64,
}
/// The observer visibility query system.
/// Runs once per tick for the player character.
/// Runs at reduced frequency for Active-tier NPCs (staggered).
pub fn compute_observer_visibility(
observer: Entity,
observer_pos: &TilePosition,
observer_facing: &FacingDirection,
observer_knowledge: &KnowledgeGraph,
spatial_index: &dyn SpatialIndex,
walkability: &WalkabilityMap,
// shadowcast_fn would be the chosen algorithm from Q-018
current_tick: u64,
) -> VisibilityResult {
// 1. Shadowcast from observer position -> visible tiles
// 2. Vision cone modulation (forward/peripheral/behind per D-015)
// 3. Spatial query for entities in range
// 4. Filter entities by visible tiles -> visible_entities
// 5. For visible entities: update knowledge graph (Direct confidence)
// 6. For entities in knowledge graph but NOT visible: remembered_entities
// 7. Sound events from D-018 three-range model
todo!()
}
```
**Key design choice:** The visibility query READS the knowledge graph to produce remembered entities, and a SEPARATE system WRITES to the knowledge graph based on visibility results. This follows the bevy_ecs pattern of separating reads and writes into different systems to avoid borrow conflicts.
```rust
// System 1: Read knowledge + compute visibility (runs first)
fn visibility_query_system(
observers: Query<(Entity, &TilePosition, &FacingDirection, &KnowledgeGraph)>,
// ... other params
) { /* produces VisibilityResult, stores in Resource */ }
// System 2: Write knowledge updates based on visibility (runs after)
fn knowledge_update_system(
mut observers: Query<&mut KnowledgeGraph>,
visibility_results: Res<VisibilityResults>,
// ...
) { /* updates knowledge graphs based on what was seen */ }
```
This two-system split avoids the classic ECS anti-pattern of read-then-write in a single system with mutable borrows on the same component.
---
## 5. Sprint 2 Stub -- Exactly What to Ship
Let me be concrete. Here is what Sprint 2 ships versus what waits.
### Sprint 2 (Ship)
1. **`KnowledgeGraph` component** with `BTreeMap<StableId, EntityKnowledge>` and `BTreeMap<FactId, FactKnowledge>`. Full struct definitions as above.
2. **`KnowledgeConfidence` enum** with all 5 levels. Resolves Q-016.
3. **`KnowledgeSource` enum** with `DirectObservation` and `Background` variants only. Other sources (`Heard`, `ToldBy`, `Inferred`) exist as enum variants but are not generated by any system yet.
4. **`observe_entity()` method** that writes Direct confidence when an entity is in LOS. Called by the perception system after shadowcasting.
5. **`relationship_with()` query** that returns `RelationshipState` for D-033 entity color derivation.
6. **`knows_fact()` and `fact_at_least()` queries** for monologue prerequisite checks (D-035 `prerequisite` tag).
7. **Player character gets a `KnowledgeGraph` component** populated from background data at game start.
8. **NPCs get stub `KnowledgeGraph` components** with empty knowledge (or minimal background data for THE FRIEND's starting state).
9. **Integration with `compute_observer_visibility`** (#112): visible entities update the player's KnowledgeGraph. VisibilityResult includes `remembered_entities` from the knowledge graph.
10. **Basic decay** runs once per game-minute (every 10 ticks). Configurable via `DecayThresholds` resource.
### Sprint 3+ (Defer)
- NPC-to-NPC knowledge propagation (gossip, reports)
- `ToldBy` and `Inferred` source generation
- `KnowledgeIndex` reverse lookup resource
- Knowledge-driven NPC behavior changes
- Dialogue access tier filtering by knowledge state (D-028 Layer 1)
- Monologue triggering based on knowledge transitions
- Full CauseChain integration (knowledge change -> CauseChain entry -> monologue trigger)
- Knowledge decay tuning per knowledge type
- Misinformation (deliberately wrong knowledge entries)
### Why This Split Works
Sprint 2's goal is "fog of perception working through the bridge." The knowledge graph stub enables:
- Observer visibility query (#112) has a concrete data structure to read/write
- Entity color (#D-033) has a relationship state to derive from
- Fog rendering has remembered entities (last known positions)
- Monologue prerequisites have a queryable interface (even if few facts exist yet)
Everything deferred is ADDITIVE. The Sprint 2 stub is the correct foundation -- nothing needs to be rewritten when Sprint 3 features land.
---
## 6. Memory Budget: 80 Active NPCs x ~50 Known Entities
Let me put real numbers on this.
### Per-Entity Knowledge Entry Size
```
EntityKnowledge {
last_known_position: Option<TilePosition> = 16 bytes (Option<3xi32+padding>)
last_observed_tick: u64 = 8 bytes
last_updated_tick: u64 = 8 bytes
confidence: KnowledgeConfidence = 1 byte (enum, 5 variants)
source: KnowledgeSource = ~32 bytes (largest variant)
relationship: RelationshipState = 1 byte
known_attributes: BTreeMap<K,V> = ~128 bytes (3-5 entries typical)
}
Total per entry: ~200 bytes (with BTreeMap node overhead and alignment)
```
### Per-NPC Knowledge Graph Size
At 50 known entities per NPC:
- Entity entries: 50 x 200 bytes = 10,000 bytes (~10KB)
- BTreeMap overhead: ~50 x 48 bytes (node pointers) = 2,400 bytes
- Fact entries (estimate 20): 20 x 80 bytes = 1,600 bytes
- Component overhead: ~64 bytes
- **Total per NPC: ~14KB**
### Total Memory at Scale
| Scale | NPCs with Knowledge | Memory |
|-------|---------------------|--------|
| v0.1 (15 NPCs) | 15 | ~210KB |
| Active tier (80 NPCs) | 80 | ~1.1MB |
| Active + Background (2,080 NPCs) | 2,080 | ~29MB |
| Full population (10,000+) | In-memory: 2,080 + serialized: 10K | ~29MB live + ~50MB serialized |
**Verdict: Memory is a non-issue.** Even at maximum scale, the knowledge graph consumes <100MB. Modern systems have 16-64GB RAM. The game's total memory footprint will be dominated by map data and textures, not knowledge graphs.
---
## 7. Query Time Budget Within 100ms Tick
The 100ms tick budget (D-026, 10 tps) must accommodate ALL systems: movement, perception, AI, knowledge updates, snapshot generation. Let me allocate.
### Budget Allocation (at 80 Active NPCs)
| System | Budget | Notes |
|--------|--------|-------|
| Movement + collision | 5ms | Already benchmarked as fast |
| Shadowcasting (1 player) | 2-5ms | 150x150 map, depends on Q-018 algorithm |
| Shadowcasting (NPCs, staggered) | 10-15ms | 8-10 NPCs per tick, round-robin |
| Knowledge update (from visibility) | 2-3ms | BTreeMap insert/lookup for visible entities |
| Knowledge decay | 0.5ms | Runs 1/game-minute, amortized ~0.05ms/tick |
| Observer snapshot assembly | 2-3ms | Serialize visible + remembered entities |
| IPC serialization + send | 1-5ms | MessagePack, measured in Sprint 1 |
| Headroom | ~65ms | For AI, pathfinding, storyteller (Sprint 3+) |
### Knowledge Graph Query Performance
BTreeMap lookup: O(log N) where N = number of known entities.
- At N=50: ~6 comparisons per lookup. StableId comparison is a single u64 compare. Nanoseconds.
- At N=200 (extreme case): ~8 comparisons. Still nanoseconds.
BTreeMap iteration (for decay): O(N).
- At N=50: iterating 50 entries with simple comparisons. Sub-microsecond.
- 80 NPCs x 50 entries = 4,000 decay checks per game-minute = 0.1ms total.
**Knowledge graph operations are NOT on the critical path.** The expensive operations are shadowcasting and spatial queries. Knowledge lookups and updates are negligible by comparison.
### Where Time Actually Goes
The real concern is not knowledge graph queries but the NUMBER of perception queries per tick. At 80 Active NPCs, if every NPC runs full shadowcasting every tick:
- 80 x 3ms = 240ms. Exceeds budget.
Solution (already identified in architecture review): staggered updates. Player runs every tick. NPCs run round-robin: 8-10 per tick, full cycle every 8-10 ticks. NPC knowledge updates are slightly delayed (up to 1 second game-time) but this is invisible to the player.
---
## 8. Spatial Partitioning for Knowledge Updates
**Does knowledge update need spatial partitioning? No. But perception does, and knowledge rides perception.**
Knowledge updates happen AFTER perception computes visibility. The flow is:
```
SpatialIndex.entities_in_range(observer, range) // spatial query
-> shadowcast(observer, nearby_entities) // LOS check
-> observer.knowledge.observe_entity(visible) // knowledge update
```
The spatial partitioning is in step 1, not step 3. By the time we reach the knowledge graph, we already have a small filtered set of entities (typically 5-20 visible entities, not 80). Writing 5-20 BTreeMap entries is trivial.
The `SpatialIndex` trait defined in the architecture review (Round 2 synthesis) is the correct abstraction here. Knowledge graph does not need its own spatial structure.
---
## 9. Determinism (D-010 Principle 4)
### HashMap Ban: BTreeMap Solves It
The architecture review consensus is: no `HashMap` in simulation code. `BTreeMap` gives deterministic iteration order (sorted by key). Since `StableId` is `u64` with derived `Ord`, BTreeMap iteration is deterministic across runs, platforms, and compilations.
`FactId` is a `String` with derived `Ord`. String ordering is byte-lexicographic, also deterministic.
### Knowledge Decay Determinism
Decay is driven by tick count (integer arithmetic), not wall-clock time. Combined with BTreeMap iteration order, decay produces identical results given identical state. No floating-point operations in the decay path.
### Knowledge Update Ordering
When multiple entities observe each other on the same tick, the update order matters for determinism. The system processes entities in bevy_ecs query iteration order, which is deterministic within an archetype. Since all NPCs with KnowledgeGraph share the same archetype (same component set), iteration is stable.
**But:** if NPC A observes NPC B and this changes A's behavior, which then changes what B observes A doing -- that is a circular dependency. In a single tick, this is resolved by system ordering: visibility query runs first (reads current state), knowledge update runs second (writes new state). No circular dependency within a single tick. Cross-tick effects propagate naturally.
---
## 10. Observer Snapshot Pipeline Integration
The observer snapshot (D-020) is the ONLY data crossing the IPC bridge. Knowledge graph data must be projected into the snapshot, not sent raw.
### What Crosses the Bridge
```rust
/// Extended VisibleEntity for ObserverSnapshot v2 (#358).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VisibleEntity {
pub entity_id: u64, // StableId value
pub x: f32,
pub y: f32,
pub z: i32,
pub kind: EntityKind,
/// NEW: Relationship state for D-033 entity color.
pub relationship: RelationshipState,
/// NEW: Whether this entity is currently visible or remembered.
pub visibility: EntityVisibility,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EntityVisibility {
/// Currently in line of sight.
Visible,
/// Not in LOS but remembered from knowledge graph.
Remembered { confidence: KnowledgeConfidence, age_ticks: u64 },
}
```
### What Does NOT Cross the Bridge
- The raw KnowledgeGraph struct
- KnowledgeSource details (these are server-internal)
- BTreeMap structure
- FactId/FactKnowledge (the client does not need to know about abstract facts)
- Other NPCs' knowledge graphs (information boundary enforcement!)
**This is the information boundary in practice.** The client receives exactly what the observer knows -- visible entities with relationship color, remembered entities with confidence/age for fog rendering. Nothing more. D-010 principle 2 enforced at the protocol level.
---
## 11. IPC Bridge Implications (D-020)
### Snapshot Size Impact
Adding `relationship: RelationshipState` (1 byte) and `visibility: EntityVisibility` (~9 bytes for Remembered variant) to each `VisibleEntity`:
- Current VisibleEntity: ~25 bytes serialized (MessagePack)
- Expanded VisibleEntity: ~35 bytes serialized
- At 30 visible + 20 remembered entities: 50 x 35 = 1,750 bytes per snapshot
- At 10 tps: ~17.5KB/s
**No bandwidth concern.** This is well within the pipe buffer and IPC latency budget.
### Protocol Evolution
The `ObserverSnapshot` struct will grow over sprints. MessagePack's named-field encoding (which the codec already handles per D-020) supports additive changes: new fields can be added without breaking the client. Old clients ignore unknown fields. This is why MessagePack was chosen over protobuf -- schema evolution without .proto files.
The `EntityVisibility` enum is the first test of this. If the client does not understand `Remembered`, it should render it as invisible (safe fallback). The GDScript Protocol class should already handle unknown fields gracefully -- verify this as part of #358.
---
## 12. Save/Load Implications (Q-019)
### StableId Strategy
`StableId(u64)` is assigned once at entity creation and NEVER changes. It is stored as a component on the entity:
```rust
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StableEntityId(pub u64);
```
Generation strategy: monotonically incrementing counter from a `Resource`. Deterministic if game seed determines starting count.
```rust
#[derive(Resource)]
pub struct EntityIdGenerator {
next_id: u64,
}
impl EntityIdGenerator {
pub fn new(seed_offset: u64) -> Self {
Self { next_id: seed_offset }
}
pub fn next(&mut self) -> StableEntityId {
let id = StableEntityId(self.next_id);
self.next_id += 1;
id
}
}
```
### Save/Load Round-Trip
On save: serialize each entity's `StableEntityId` + `KnowledgeGraph` + other components.
On load: deserialize into new bevy_ecs entities. The bevy `Entity` handle changes (generational index reset), but `StableEntityId` is preserved. Knowledge graphs reference StableIds, not bevy Entities, so all cross-references remain valid.
**This is the critical insight:** Knowledge graphs must reference `StableId`, NOT bevy `Entity`. If knowledge graphs stored bevy Entity handles, save/load would invalidate every cross-reference. StableId is the bridge between volatile ECS handles and persistent identity.
The `Relationship.target_id: u64` in the existing NPC model (`server/src/npc/mod.rs` line 30) is already using wire-format u64 for the same reason. The knowledge graph follows the same pattern.
### Mapping StableId <-> Entity at Runtime
A bidirectional lookup resource:
```rust
#[derive(Resource, Default)]
pub struct EntityRegistry {
/// StableId -> bevy Entity (for systems that have a StableId and need the Entity)
pub by_stable_id: BTreeMap<StableId, Entity>,
/// bevy Entity -> StableId (for systems that have an Entity and need the StableId)
pub by_entity: BTreeMap<Entity, StableId>,
}
```
Updated on entity spawn/despawn. Used by the knowledge update system to translate between ECS query results (Entity handles) and knowledge graph keys (StableIds).
---
## 13-14. Architecture Decisions Summary
| Question | Decision | Rationale |
|----------|----------|-----------|
| Component vs Resource? | Component | Natural ECS, Changed<T>, save/load, multiplayer-ready |
| Confidence: enum vs float? | Enum (5 levels) | Deterministic, maps to Q-016, debuggable |
| Source: per-entry vs per-graph? | Per-entry | Multi-channel knowledge, CauseChain provenance |
| HashMap vs BTreeMap? | BTreeMap | Determinism (D-010 p4), architecture review consensus |
| StableId vs Entity reference? | StableId | Save/load stability (Q-019), tier transitions |
| Knowledge decay model? | Tick-based, discrete levels | Deterministic, configurable, matches D-011 |
| Reverse index? | Deferred to Sprint 3+ | Not needed until NPC-NPC knowledge queries |
| Sprint 2 scope? | Stub with correct interface | Foundation, not features |
---
## 15. Minimum Viable Interface (Sprint 2) vs Full System (Sprint 3+)
### Sprint 2 Public API (Minimum Viable)
```rust
// Construction
KnowledgeGraph::new() -> Self
KnowledgeGraph::with_background(facts: Vec<(FactId, FactKnowledge)>) -> Self
// Queries (read-only, used by visibility system + snapshot assembly)
KnowledgeGraph::knows_entity(&self, id: &StableId) -> bool
KnowledgeGraph::confidence_of(&self, id: &StableId) -> Option<KnowledgeConfidence>
KnowledgeGraph::relationship_with(&self, id: &StableId) -> RelationshipState
KnowledgeGraph::knows_fact(&self, id: &FactId) -> bool
KnowledgeGraph::fact_at_least(&self, id: &FactId, min: KnowledgeConfidence) -> bool
KnowledgeGraph::known_entities(&self) -> impl Iterator<Item = (&StableId, &EntityKnowledge)>
// Mutations (used by perception system)
KnowledgeGraph::observe_entity(&mut self, target: StableId, position: TilePosition, tick: u64)
KnowledgeGraph::observe_entity_leaving_los(&mut self, target: &StableId, tick: u64)
KnowledgeGraph::decay(&mut self, current_tick: u64, thresholds: &DecayThresholds)
KnowledgeGraph::set_relationship(&mut self, target: &StableId, state: RelationshipState)
// Serialization (for save/load and tier transitions)
// Derived via serde Serialize/Deserialize -- no custom code needed.
```
### Sprint 3+ API Additions
```rust
// Knowledge propagation
KnowledgeGraph::told_about_entity(&mut self, target: StableId, source: StableId, info: EntityKnowledge, tick: u64)
KnowledgeGraph::hear_entity(&mut self, target: StableId, position: TilePosition, range: SoundRange, tick: u64)
KnowledgeGraph::infer(&mut self, fact: FactId, basis: Vec<FactId>, tick: u64)
// Dialogue integration (D-028)
KnowledgeGraph::access_tier_for(&self, target: &StableId) -> AccessTier
KnowledgeGraph::disclosure_tier_for(&self, target: &StableId) -> DisclosureTier
// CauseChain integration
KnowledgeGraph::last_change(&self) -> Option<(StableId, KnowledgeSource, u64)>
```
---
## Performance Risks and Mitigations
| Risk | Severity | Mitigation |
|------|----------|------------|
| BTreeMap slower than HashMap for lookups | LOW | At N=50, difference is ~2 nanoseconds. Not measurable. |
| Knowledge graph bloat (NPC knows too many entities) | LOW | Cap at 200 entries with LRU eviction of Stale entries. |
| Staggered NPC perception causes visible "popping" | MEDIUM | Smooth over with interpolation on client side. Remembered entities fade gradually. |
| StableId generation not deterministic across save/load | MEDIUM | Use monotonic counter seeded from game seed. Document invariant. |
| KnowledgeGraph serialization too large for State-saved tier | LOW | At ~5KB per entity, 10K entities = 50MB. Acceptable. Compress if needed. |
| Cross-entity knowledge queries (who knows about X?) too slow | MEDIUM (Sprint 3+) | Reverse index Resource. Not needed Sprint 2. |
---
## Dependencies
| This Workshop Produces | Needed By | Sprint |
|----------------------|-----------|--------|
| KnowledgeGraph struct definition | #112 Observer visibility query | Sprint 2 |
| StableId type and EntityRegistry | #360 Q-019 Entity ID stability | Sprint 2 |
| EntityVisibility enum | #358 ObserverSnapshot v2 schema | Sprint 2 |
| RelationshipState enum | D-033 Entity color rendering | Sprint 2 |
| `fact_at_least()` query interface | Monologue prerequisite system (#119) | Sprint 3 |
| Access tier derivation | D-028 dialogue filtering | Sprint 3+ |
| Knowledge propagation API | NPC gossip / investigation | Sprint 3+ |
---
## Recommended Next Steps
1. **This workshop produces a D-0XX decision** with the struct definitions and API from this analysis. The data model should be confirmed in Round 2, not deferred.
2. **Q-016 is resolved by the KnowledgeConfidence enum.** The hierarchy `Stale < Rumor < Inferred < Observed < Direct` maps to the requirement `suspects < knows_of < knows_details`. Document the mapping explicitly.
3. **Q-019 is partially resolved by StableId.** The `EntityRegistry` resource plus `StableEntityId` component provides the bridge between bevy Entity handles and persistent identifiers. Full Q-019 resolution also needs client-side entity lifecycle (how Godot maps StableId to scene nodes).
4. **Ticket #358 (ObserverSnapshot v2)** should incorporate `RelationshipState` and `EntityVisibility` as described in section 10.
5. **Implementation order for Sprint 2:**
- Week 1: StableEntityId component + EntityRegistry resource + KnowledgeGraph struct with tests
- Week 1: DecayThresholds resource + decay system
- Week 2: Integration with #112 (observer visibility query reads/writes knowledge)
- Week 2: Integration with #358 (snapshot includes relationship + visibility state)
6. **Sprint 2 test requirements (D-030 Phase 2 alignment):**
- Unit tests: KnowledgeGraph CRUD operations, decay logic, confidence ordering
- Integration test: observation updates knowledge, leaving LOS downgrades to Observed
- Negative test: entity A cannot query entity B's KnowledgeGraph (information boundary)
---
## Files Referenced
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/server/src/npc/mod.rs` -- Current NPC component model, InformationInventory placeholder (line 47-49)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/server/src/perception/mod.rs` -- PerceptionPlugin stub
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/server/src/bridge/types.rs` -- ObserverSnapshot, VisibleEntity, wire protocol types
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/server/src/simulation/movement.rs` -- TilePosition, WalkabilityMap, validate_movement
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/server/src/simulation/tier.rs` -- SimulationTier, ScopeTag, LastInteraction
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/server/src/cause_chain.rs` -- CauseChain, CauseKind (aligns with KnowledgeSource)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/server/Cargo.toml` -- Dependency inventory
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/architecture.md` -- D-010, D-020, D-026, D-030, D-031
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/perception.md` -- D-011, D-015, D-017, D-018, D-033
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/content.md` -- D-028, D-034, D-035
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/questions.md` -- Q-016, Q-017, Q-018, Q-019
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/audits/architecture-review-2026-02-11.md` -- Architecture review consensus
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/sprints/sprint-2/server.md` -- Sprint 2 server tasks
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/sprints/sprint-2/joint.md` -- Sprint 2 joint tasks, integration proof
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/workshops/knowledge-graph-information-boundaries/workshop-brief.md` -- Workshop brief
@@ -0,0 +1,589 @@
# Knowledge Graph Workshop — Round 2: Mechanics Validation
**Role:** GESTALT (Systems Designer)
**Focus:** Cross-validation of gameplay mechanics against proposed architecture
**Date:** 2026-02-11
---
## Executive Summary
*cracks knuckles*
Let me break down what this actually means mechanically.
The good news: **The architecture serves gameplay.** Tyre and Dudley's proposed structs support every gameplay loop I identified in Round 1. The per-entity Component pattern, BTreeMap storage, StableId references, and 5-level confidence enum are ALL mechanically sound. I found zero architectural blockers.
The hierarchy debate: **We need BOTH axes.** Tyre's SOURCE-based levels (Stale/Rumor/Inferred/Observed/Direct) and Paula's DEPTH-based levels (Unaware/Suspects/KnowsOf/KnowsDetails/Understands) are solving DIFFERENT problems. Source determines confidence decay and trust. Depth determines dialogue gating and monologue progression. The solution is NOT picking one — it's recognizing they're orthogonal and mapping them correctly.
THE FRIEND arc validation: **It works.** I walked the proposed structs through THE FRIEND's lie detection sequence step-by-step. Every beat is supported: lie entry, contradiction detection, monologue trigger, relationship state shift, dialogue unlock. No gaps.
The one gap: **Observable gossip is underspecified.** Tyre and Dudley defined the data structures for NPC-to-NPC knowledge transfer, but the GAMEPLAY loop isn't there yet. How does the player DETECT that two NPCs just shared information? This is critical for political intrigue and deferred to Sprint 3+, which is correct — but the design needs to exist now so the architecture doesn't preclude it.
**Bottom line:** Ship the Sprint 2 stub as specified. It's the right foundation.
---
## 1. Architecture Serves Gameplay: YES
### What Gameplay Needs
From my Round 1 analysis, the knowledge graph must support:
1. **Monologue gating** — lines filtered by confidence level
2. **Dialogue access** — conversation options unlocked by knowledge state
3. **Gossip observation** — player detects NPC-to-NPC knowledge transfer
4. **Contradiction detection** — conflicting knowledge sources trigger monologue
5. **Triangle pressure** — knowledge propagation increases social conflict
### What the Architecture Provides
**Tyre's Component model + BTreeMap + StableId:**
- Per-entity `KnowledgeGraph` Component ✓ Supports monologue/dialogue queries per observer
- `BTreeMap<StableId, EntityKnowledge>` ✓ O(1) lookups for "what do I know about X?"
- `KnowledgeSource` enum ✓ Tracks where knowledge came from (needed for contradiction detection)
- `last_observed_tick` + `last_updated_tick` ✓ Supports decay mechanic
- `RelationshipState` embedded in EntityKnowledge ✓ Drives D-033 entity color
**Dudley's event queue + decay batching:**
- `KnowledgeEventQueue` ✓ Decouples perception from knowledge updates
- Batch decay (600-tick intervals) ✓ Prevents O(N×M) per-tick explosion
- `EntityIdMap` for StableId ↔ Entity translation ✓ Solves save/load stability
**Every gameplay requirement maps to a struct field or query method.** No missing pieces for Sprint 2 scope.
### What's NOT in Sprint 2 (Correctly Deferred)
- NPC-to-NPC gossip propagation (architectural support exists, systems don't)
- `ToldBy` source generation (enum variant exists, no system writes it yet)
- Knowledge-driven NPC behavior changes (knowledge updates don't feed AI yet)
- Misinformation (deliberately wrong entries — requires content flag)
**This split is mechanically sound.** Sprint 2's goal is geometric fog, not social intrigue. The stub enables observer snapshot filtering. Full knowledge gameplay comes Sprint 3+.
---
## 2. Knowledge Hierarchy Debate: My Updated Position
### The Problem Statement
We have four proposals:
- **Tyre:** 5 levels (Stale/Rumor/Inferred/Observed/Direct) — SOURCE-based confidence
- **Paula:** 5 levels (Unaware/Suspects/KnowsOf/KnowsDetails/Understands) — DEPTH-based understanding
- **Me (Round 1):** 3 levels (Suspects/KnowsOf/KnowsDetails) — simplified gating
- **Dudley:** 4 levels (Suspects/KnowsOf/KnowsDetails/DirectObservation) — hybrid
### The Realization
These are NOT competing hierarchies. They're answering different questions:
**Tyre's SOURCE-based axis:**
- Question: "How reliable is this knowledge?"
- Use case: Decay rates, trust modeling, CauseChain provenance
- Example: "I heard this from Lera (Rumor) vs I saw it myself (Direct)"
**Paula's DEPTH-based axis:**
- Question: "How much do I understand about this situation?"
- Use case: Monologue progression, dialogue unlocks, narrative revelation
- Example: "I suspect Kael is compromised (Suspects) vs I know he's trying to exit the ring to protect Hael (Understands)"
**These are ORTHOGONAL.** You can have:
- High source confidence, low depth: "I directly observed Kael in corridor B-7 (Direct), but I don't know why (KnowsOf)"
- Low source confidence, high depth: "Lera told me Kael is trying to exit the ring to protect Hael (Rumor), and it explains everything (Understands)"
### The Mechanical Solution
The architecture should support BOTH:
```rust
pub struct EntityKnowledge {
// Tyre's SOURCE confidence (decay, trust)
pub confidence: KnowledgeConfidence, // Stale < Rumor < Inferred < Observed < Direct
// Paula's DEPTH understanding (gating, progression)
pub understanding: UnderstandingLevel, // Suspects < KnowsOf < KnowsDetails < Understands
pub source: KnowledgeSource, // Provenance
// ... other fields
}
pub enum KnowledgeConfidence {
Stale, // Old information, may be outdated
Rumor, // Third-hand, low trust
Inferred, // Deduced from other facts
Observed, // First-hand but not currently visible
Direct, // Currently in LOS
}
pub enum UnderstandingLevel {
Suspects, // "Something's off"
KnowsOf, // "X is happening"
KnowsDetails, // "X is happening because Y"
Understands, // "X is happening because Y, and it means Z"
}
```
### What Gates What
**Monologue prerequisites** use DEPTH (Paula's axis):
```yaml
prerequisite:
knowledge:
- subject: kael_davan
understanding: knows_details # Not confidence level
flag: trying_to_exit_ring
```
**Dialogue access tiers** use DEPTH + relationship:
```yaml
access: [insider]
trust: secret
prerequisite:
knowledge:
- subject: voss
understanding: knows_details
flag: false_manifests
```
**Decay mechanics** use SOURCE (Tyre's axis):
```rust
if knowledge.confidence == KnowledgeConfidence::Stale {
// Decay faster or remove
}
```
**Contradiction detection** compares SOURCES:
```rust
if knowledge_a.source == DirectObservation && knowledge_b.source == ToldBy(sera) {
// Sera lied to you
trigger_contradiction_monologue();
}
```
### Recommendation for Synthesis
**Keep Tyre's `KnowledgeConfidence` enum as-is.** It's the correct model for SOURCE reliability and maps cleanly to decay/trust.
**Add a second field: `understanding_level: UnderstandingLevel`** to EntityKnowledge for DEPTH tracking.
**Initial implementation:** Both fields exist in Sprint 2 structs, but only `confidence` is written by perception systems. `understanding_level` defaults to `KnowsOf` (you observed the entity, so you know of them). Sprint 3+ adds systems that upgrade understanding through dialogue/evidence/inference.
**This is "design for it now, build it later."** The struct supports both axes. Sprint 2 uses one axis. Sprint 3 unlocks the other.
---
## 3. Gossip as Observable Mechanic
### The Gameplay Loop (What I Proposed in Round 1)
```
1. Player tells Voss about Kael's smuggling
2. Hours later, player observes Voss and Maret talking
3. Next day, Maret confronts Kael
4. Monologue: "Voss told Maret. Information spreads. I started this."
```
### Does the Architecture Support This?
**Step 1 (player tells NPC):**
- Dialogue system sets flag: `voss.knowledge.entities[kael_id].knows_fact("smuggling") = true`
- Source: `ToldBy { source_id: player_stable_id, tick }`
- ✓ Supported by Dudley's `KnowledgeSource::ToldBy` variant
**Step 2 (observe conversation):**
- **GAP.** How does the player DETECT that Voss and Maret are having a "gossip-worthy" conversation vs casual small talk?
- Proposed mechanic: Observable tell system
- NPCs in conversation get `situation: private_conversation` tag
- Visual: NPCs face each other, proximity threshold, gestures
- Audio: D-018 sound ranges (player hears murmurs if within Medium range)
- Monologue: "Voss and Maret, heads together. Plotting or gossiping?"
**Step 3 (Maret confronts Kael):**
- NPC AI system (Sprint 3+) queries Maret's knowledge graph
- Finds: `kael.knows_fact("smuggling")` with high confidence
- Triangle pressure increased (Q-017 mechanism)
- Activates `situation: confrontation` for Maret's next interaction with Kael
- ✓ Supported by proposed architecture (knowledge graph feeds AI decisions)
**Step 4 (monologue commentary):**
- Player observes Maret confronting Kael
- Monologue system queries player knowledge: "Did I tell anyone about Kael?"
- Meta-knowledge check: Player knows they told Voss, and they know Voss talked to Maret
- ✓ Requires meta-knowledge tracking: "I know X knows Y"
### What's Missing
**Observable gossip indicators:**
- Visual tells when NPCs share information (not just "talking" but "sharing secrets")
- Monologue commentary when player detects gossip propagation
- Meta-knowledge structure: tracking "I know that X knows Y"
**Is this a Sprint 2 blocker?** NO. Sprint 2 is geometric fog. Gossip observation is social intrigue (Sprint 3+).
**Is this an architectural blocker?** NO. The knowledge graph structure supports it. The missing piece is the NPC conversation system and tell generation (separate from knowledge graph design).
**Recommendation:** Gossip propagation is correctly deferred, but Tyre's Round 2 synthesis should explicitly state: "NPC-to-NPC knowledge transfer will require conversation system + observable tell generation (Sprint 3 dependency)."
---
## 4. THE FRIEND Arc Walkthrough
Let me validate the architecture against Paula's canonical example: THE FRIEND lies to you, you discover the contradiction.
### The Sequence (Using Proposed Structs)
**T=0: Session start**
```rust
// Detective's knowledge graph at spawn
detective.knowledge.entities[sera_id] = EntityKnowledge {
confidence: KnowledgeConfidence::Observed,
understanding: UnderstandingLevel::KnowsOf,
source: KnowledgeSource::Background,
relationship: RelationshipState::Friendly, // THE FRIEND
last_observed_tick: 0,
known_attributes: btreemap! {
AttributeKey::Name => AttributeValue::Text("Sera Venn"),
AttributeKey::Role => AttributeValue::Text("Scanner operator"),
},
// ...
};
```
**T=100: Sera tells you Kael was at the dock**
```rust
// Dialogue system writes to detective's knowledge graph
detective.knowledge.entities[kael_id]
.known_attributes
.insert(
AttributeKey::Custom("location_second_shift"),
AttributeValue::Text("dock")
);
detective.knowledge.facts.insert(
FactId("kael_location_shift2_dock"),
FactKnowledge {
confidence: KnowledgeConfidence::Rumor, // Told by someone
source: KnowledgeSource::ToldBy {
source_id: sera_id,
tick: 100
},
acquired_tick: 100,
}
);
```
**T=200: You observe Kael in corridor B-7**
```rust
// Perception system (after shadowcasting detects Kael in LOS)
detective.knowledge.observe_entity(
kael_id,
TilePosition { x: 47, y: 12, z: 2 }, // corridor B-7
200 // current tick
);
// This writes:
detective.knowledge.entities[kael_id] = EntityKnowledge {
last_known_position: Some(TilePosition { x: 47, y: 12, z: 2 }),
last_observed_tick: 200,
confidence: KnowledgeConfidence::Direct,
source: KnowledgeSource::DirectObservation { tick: 200 },
// ...
};
```
**T=201: Contradiction detection**
```rust
// Knowledge validation system (new system, Sprint 3)
fn detect_contradictions(
mut query: Query<(&mut KnowledgeGraph, &CharacterArchetype)>,
time: Res<SimulationTime>,
) {
for (mut knowledge, character) in &mut query {
// Check for conflicting location facts
let kael_facts: Vec<_> = knowledge.facts.iter()
.filter(|(id, _)| id.0.starts_with("kael_location"))
.collect();
if kael_facts.len() > 1 {
// Multiple location claims
let told_fact = kael_facts.iter()
.find(|(_, f)| matches!(f.source, KnowledgeSource::ToldBy { .. }));
let observed_fact = kael_facts.iter()
.find(|(_, f)| matches!(f.source, KnowledgeSource::DirectObservation { .. }));
if let (Some((told_id, told)), Some((obs_id, obs))) = (told_fact, observed_fact) {
// Contradiction detected
if let KnowledgeSource::ToldBy { source_id, .. } = told.source {
// Sera lied
knowledge.entities.get_mut(&source_id).map(|sera| {
sera.relationship = RelationshipState::PersonOfInterest;
});
// Emit monologue event
events.send(MonologueEvent {
character: *character,
trigger: TriggerType::KnowledgeContradiction,
chime: ChimeLevel::Urgent,
text_id: "mon_sera_lied_location",
});
}
}
}
}
}
```
**T=202: Monologue fires**
```
[Urgent chime]
"Sera told me Kael was at the dock. But I just saw him in the corridor. Why did she lie?"
```
**T=203: Entity color shift**
```rust
// Observer snapshot assembly (feeds client)
let sera_relationship = detective.knowledge.relationship_with(&sera_id);
// sera_relationship == RelationshipState::PersonOfInterest
snapshot.entities.push(VisibleEntity {
entity_id: sera_id.0,
relationship: RelationshipState::PersonOfInterest, // Amber color
// ...
});
```
**T=210: Dialogue unlock**
```rust
// Dialogue system (Sprint 3) queries knowledge for confrontation access
let can_confront_sera = detective.knowledge
.entities
.get(&sera_id)
.map(|k| k.relationship == RelationshipState::PersonOfInterest)
.unwrap_or(false);
if can_confront_sera {
dialogue_options.push(DialogueOption {
text: "Why did you lie about Kael's location?",
access: AccessTier::Insider,
situation: Situation::Confrontation,
topic: Topic::SeraLied,
});
}
```
### Validation Result
**Every beat is supported:**
- Lie entry (ToldBy source)
- Contradiction detection (compare sources)
- Monologue trigger (KnowledgeContradiction event)
- Relationship state shift (PersonOfInterest enum)
- Entity color change (RelationshipState in snapshot)
- Dialogue unlock (relationship state gates access)
**No gaps in data structures.**
**Two new systems needed (Sprint 3):**
1. `detect_contradictions` system (runs after knowledge updates)
2. Dialogue access filtering by knowledge + relationship state
**THE FRIEND arc proves the architecture.** If it supports this, it supports the game.
---
## 5. Gaps Found
### Gap 1: Meta-Knowledge (Who Knows What)
**What's missing:** Tracking "I know that X knows Y."
**Why it matters:** Gossip observation gameplay requires the player to infer knowledge propagation.
**Example:**
```
Player tells Voss about Kael
Player observes Voss talking to Maret
Player infers: "Maret probably knows now"
```
**Proposed solution (Sprint 3):**
```rust
pub struct MetaKnowledge {
/// I know that entity X knows about entity Y
pub knowledge_of_knowledge: BTreeMap<StableId, BTreeSet<StableId>>,
}
```
**Is this a blocker?** No. Sprint 2 doesn't need it. Social intrigue does (Sprint 3+).
### Gap 2: Knowledge Update Triggers Monologue
**What's underspecified:** How does a knowledge graph change trigger a monologue?
**Tyre's architecture:** Knowledge update systems write to KnowledgeGraph Component. Monologue system reads Changed<KnowledgeGraph>.
**Missing detail:** What counts as "monologue-worthy" knowledge change?
- New entity observed? (Always trigger)
- Existing entity re-observed? (Don't spam)
- Knowledge level upgraded? (Trigger)
- Contradiction detected? (Urgent trigger)
**Proposed solution:**
```rust
pub enum KnowledgeChangeType {
NewEntity(StableId),
LevelUpgrade { entity: StableId, old: UnderstandingLevel, new: UnderstandingLevel },
Contradiction { entity: StableId, sources: Vec<KnowledgeSource> },
FactLearned(FactId),
}
#[derive(Resource)]
pub struct KnowledgeChangeEvents {
pub changes: Vec<KnowledgeChangeType>,
}
```
Monologue system consumes these events, not Changed<KnowledgeGraph> (too noisy).
### Gap 3: Dialogue System Integration Not Specified
**What's missing:** The dialogue system needs to query the knowledge graph for access filtering.
**Current state:** D-028 defines access tiers. D-035 defines prerequisite tags. But the INTEGRATION is not specified.
**Needed (Sprint 3):**
```rust
pub fn dialogue_available(
npc_id: StableId,
line: &DialogueLine,
player_knowledge: &KnowledgeGraph,
player_relationship: &RelationshipState,
) -> bool {
// Check access tier
if !line.access.contains(&player_relationship.tier()) {
return false;
}
// Check knowledge prerequisites
for prereq in &line.prerequisites {
match prereq {
Prerequisite::Knowledge { subject, understanding, flag } => {
if !player_knowledge.has_understanding(subject, *understanding, flag) {
return false;
}
}
// ...
}
}
true
}
```
**Is this a blocker?** No. Dialogue gating is Sprint 3+. But the interface contract should be defined now.
### Gap 4: Observable Gossip (Already Covered in Section 3)
See section 3. Not a blocker, but needs explicit acknowledgment in synthesis.
---
## 6. Recommendation for Synthesis
### What Tyre Should Include
1. **Accept the dual-axis model:**
- `KnowledgeConfidence` (Tyre's SOURCE axis) for decay/trust
- `UnderstandingLevel` (Paula's DEPTH axis) for gating/progression
- Both fields in `EntityKnowledge` struct
- Sprint 2 uses confidence only, Sprint 3 unlocks understanding
2. **Acknowledge gossip gap:**
- "NPC-to-NPC knowledge transfer is architecturally supported but requires NPC conversation system (Sprint 3 dependency)"
- "Observable gossip indicators (tells, situation tags) are separate from knowledge graph design"
3. **Define knowledge change events:**
- `KnowledgeChangeEvents` resource for monologue triggering
- Separates "graph changed" from "monologue-worthy change"
4. **Specify dialogue integration interface:**
- `dialogue_available()` function signature
- How knowledge prerequisites map to line filtering
- Defer implementation to Sprint 3, but contract exists now
5. **Validate THE FRIEND arc:**
- Include walkthrough from section 4 as proof the architecture works
- Confirms no structural gaps for core gameplay
### What Tyre Should Change
**Nothing architectural.** The Component model, BTreeMap, StableId, and 5-level confidence enum are all correct.
**Additions only:**
- Add `understanding_level: UnderstandingLevel` field to `EntityKnowledge`
- Add `KnowledgeChangeEvents` resource to API
- Add note about gossip system dependency
### Sprint 2 Stub Confirmation
Ship exactly what Tyre specified:
- Full `KnowledgeGraph` struct
- `KnowledgeConfidence` enum (5 levels)
- `observe_entity()` method (DirectObservation source)
- `relationship_with()` query (entity color)
- `knows_fact()` + `fact_at_least()` (monologue prerequisites)
- Basic decay (batch, 600-tick intervals)
This is the right foundation. No rework needed.
---
## 7. What's NOT Missing
Let me be clear about what does NOT need to change:
**Data structures:** Perfect as-is. BTreeMap, StableId, Component pattern, serialization strategy — all mechanically sound.
**Performance:** Dudley's batch decay + event queue architecture prevents the O(N×M) explosion I was worried about in Round 1. Knowledge lookups are not on the critical path.
**Save/load:** StableId solves entity reference stability. I have no concerns.
**Tier serialization:** Knowledge graph rides the existing tier system. No special logic needed.
**Observer snapshot integration:** `relationship_with()` + `known_entities()` queries are exactly what #112 needs.
**The architecture is not the problem.** The missing pieces are SYSTEMS that use the architecture (gossip, dialogue integration, contradiction detection). Those are Sprint 3+, correctly deferred.
---
## 8. Final Verdict
**Does the architecture serve gameplay?** YES.
**Is the hierarchy debate resolved?** YES (dual-axis model).
**Does THE FRIEND arc work?** YES (validated step-by-step).
**Is gossip underspecified?** YES (but correctly deferred to Sprint 3).
**Are there blocking gaps?** NO.
**Should we ship the Sprint 2 stub?** YES, exactly as specified.
**Bottom line:** This is interesting-complex, not annoying-complex. The knowledge graph creates DECISIONS (dialogue gates, monologue progression, social manipulation), not BOOKKEEPING (no Pokemon collection, no perfect memory, no hidden NPC knowledge). The player experiences knowledge through what actions are available, not through a sprawling journal UI.
Let's build this.
---
## Files Referenced
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/workshops/knowledge-graph-information-boundaries/round1-tyre.md`
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/workshops/knowledge-graph-information-boundaries/round1-dudley.md`
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/workshops/knowledge-graph-information-boundaries/round1-paula.md`
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/workshops/knowledge-graph-information-boundaries/round1-gestalt.md`
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/workshops/knowledge-graph-information-boundaries/round1-si.md`
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/perception.md` (D-011, D-015-D-019, D-033)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/content.md` (D-024, D-028, D-034, D-035)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/architecture.md` (D-010, D-020, D-026, D-030)
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/questions.md` (Q-016, Q-017)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,338 @@
# Sprint 2 Impact Summary — Knowledge Graph Workshop
**Date:** 2026-02-11
**Author:** SI (Project Manager)
**Source:** Workshop synthesis (round2-synthesis.md)
---
## Executive Summary
The Knowledge Graph Workshop (#351) has been reconciled into Sprint 2 scope. **8 new tickets added** to Sprint 2, all under epic #351. Sprint scope expands from 14 to 22 tickets, adding approximately **6.5 developer-days** of work.
**Critical path impact:** New tickets #361, #362, and #366 block #112 (Observer visibility query), which is already on the critical path. Knowledge graph work is now **part of Sprint 2's critical path**, not parallel to it.
**Risk level:** MEDIUM. Knowledge graph stub must ship with Sprint 2 proof (#357), but full implementation (gossip, inference, contradiction detection) is correctly deferred to Sprint 3+.
---
## New Tickets Added (8 total)
All tickets created as children of #351 (Knowledge Graph Workshop epic), assigned to Sprint 2.
| # | Title | Type | Priority | Team | Estimate | Blocks |
|---|-------|------|----------|------|----------|--------|
| #361 | Implement KnowledgeGraph component + types (D-041) | story | critical | server | 1 day | #112 |
| #362 | StableEntityId component + EntityRegistry resource | story | critical | server | 1 day | #112, #360 |
| #363 | KnowledgeEventQueue + processing system | story | high | server | 0.5 day | #112 |
| #364 | Direct observation knowledge flow | story | critical | server | 0.5 day | #112 |
| #365 | Basic knowledge decay system | story | high | server | 0.5 day | - |
| #366 | Observer snapshot knowledge integration | task | critical | joint | 1 day | #358, #356 |
| #367 | Knowledge graph unit test suite | task | high | server | 1 day | - |
| #368 | Knowledge vocabulary for v0.1 content | task | high | content | 0.5 day | #309 |
**Total added effort:** ~6.5 developer-days (per synthesis Part 7)
---
## Backlog Ticket Reconciliation
### Tickets Cancelled
| # | Title | Reason |
|---|-------|--------|
| #89 | Information inventory | Subsumed by #361 (KnowledgeGraph component). The `Vec<String>` placeholder is replaced by the full knowledge graph data model. |
### Tickets Marked Done
| # | Title | Reason |
|---|-------|--------|
| #269 | CauseChain component | Already implemented per audit report (`server/src/cause_chain.rs`). Integration with knowledge system is Sprint 3+ work. |
### Tickets Requiring Reparenting (SQL Updates Needed)
The following tickets are children of #49 (Information Boundary System epic) but should be reparented under #351 or updated with new dependencies:
| # | Current Parent | Action Required | Rationale |
|---|----------------|-----------------|-----------|
| #138 | #49 | Reparent to #351 | Information tag schema = KnowledgeConfidence + FactId system (D-041) |
| #139 | #49 | Reparent to #351, defer to Sprint 3 | Component-level access control depends on knowledge system |
| #140 | #49 | Reparent to #351, merge into #366 | Entity visibility filtering IS the observer snapshot integration |
| #141 | #49 | Reparent to #351, defer to Sprint 3 | Knowledge-based gating requires dialogue system + knowledge queries |
| #142 | #49 | Reparent to #351, defer to Sprint 3 | NPC boundaries = KnowledgeGraph per NPC, but enforcement is Sprint 3 |
**SQL commands required:**
```sql
UPDATE tickets SET parent_id = 351 WHERE id IN (138, 139, 140, 141, 142);
UPDATE tickets SET status = 'backlog', sprint_id = NULL WHERE id IN (139, 141, 142); -- Defer to Sprint 3
UPDATE tickets SET status = 'cancelled' WHERE id = 140; -- Merged into #366
```
### Tickets With New Dependencies
| # | Title | New Dependency | Reason |
|---|-------|----------------|--------|
| #309 | Knowledge state vocabulary for v0.1 | Unblocked by #368 | D-041 Appendix A provides the vocabulary taxonomy |
| #182 | Divergent starting knowledge | Depends on #361 | Starting knowledge = `KnowledgeGraph::with_background()` |
| #360 | Resolve Q-019: entity ID stability | Partially resolved by #362 | Server-side stable IDs now defined, client mapping remains open |
**SQL commands required:**
```sql
-- Add blocking relationships
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (361, 112); -- KnowledgeGraph blocks observer query
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (362, 112); -- StableEntityId blocks observer query
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (362, 360); -- StableEntityId partially resolves Q-019
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (366, 358); -- Snapshot integration blocked by schema design
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (366, 356); -- Snapshot integration blocks bridge ticket
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (368, 309); -- Vocabulary document unblocks content ticket
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (361, 182); -- KnowledgeGraph blocks divergent starting knowledge
```
---
## Updated Sprint 2 Critical Path
### Before Workshop
```
#359 (Q-018) → #110 (shadowcast) → #111 (vision cone) → #112 (observer query) → #356 (bridge) → #357 (proof)
```
**Critical path length:** 6 tickets serial
### After Workshop
```
#359 (Q-018) → #110 (shadowcast) → #111 (vision cone) →
#361 (KnowledgeGraph) → #362 (StableEntityId) → #363 (EventQueue) → #364 (Direct obs) →
#366 (Snapshot integration) → #112 (observer query) →
#356 (bridge) → #357 (proof)
```
**Critical path length:** 10 tickets serial (4 new knowledge tickets added to critical path)
### Parallel Tracks (Unchanged)
- Client rendering: #116, #129, #130, #131, #113 (mostly independent until #113 waits on #112)
- Time system: #358#25#357
- Knowledge testing/docs: #367, #368 (can run parallel to implementation)
---
## Sprint Scope Assessment
### Baseline Sprint 2 Scope (Before Workshop)
- **14 tickets** across 3 tracks (server, client, joint)
- **Critical path:** 6 tickets serial
- **Estimated effort:** ~10-12 developer-days (not explicitly estimated pre-workshop)
### New Sprint 2 Scope (After Workshop)
- **22 tickets** (+8 knowledge graph tickets)
- **Critical path:** 10 tickets serial (+4 on critical path)
- **Estimated effort:** ~16-18 developer-days (+6.5 from knowledge graph)
### Sprint Capacity Analysis
**Assumption:** 2-week sprint, 2 full-time developers (Dudley + Oscar on server, Stig on client)
- **Available capacity:** ~20 developer-days (2 devs × 10 days, assuming 50% utilization for blockers/reviews/overhead)
- **Estimated work:** ~16-18 developer-days
- **Buffer:** ~2-4 days
**Verdict:** Sprint scope is achievable but TIGHT. No room for scope creep. Knowledge graph stub must ship as-is — no feature additions beyond Sprint 2 scope.
---
## Risks and Mitigations
### RISK 1: Critical Path Extended by 4 Tickets
**Severity:** HIGH
**Impact:** Knowledge graph tickets #361, #362, #363, #364 are now serially dependent and block #112. If any knowledge ticket slips, #112 slips, and the entire sprint slips.
**Mitigation:**
- Prioritize #361 (KnowledgeGraph) as first sprint task (day 1-2)
- #362 (StableEntityId) starts immediately after #361 (day 2-3)
- #363 and #364 are smaller (0.5 day each) and can be parallelized across team members
- #366 (snapshot integration) is joint ticket — assign to Oscar (networking) to avoid blocking Dudley on server work
**Owner:** SI (sprint tracking), Dudley (server lead)
---
### RISK 2: #112 Interface Contract Not Defined Until Week 2
**Severity:** MEDIUM
**Impact:** #112 (Observer visibility query) needs the knowledge graph query interface from #361. If #361 is delayed or incomplete, #112 cannot start.
**Mitigation:**
- D-041 synthesis Part 5 defines the interface contract explicitly (lines 219-269)
- #112 implementation can reference the synthesis while #361 is in progress
- Interface is simple: `knows_entity()`, `relationship_with()`, `known_entities_iter()` — low risk of misinterpretation
**Status:** Mitigated by synthesis clarity. No blocker expected.
---
### RISK 3: Stub vs Full Implementation Scope Creep
**Severity:** MEDIUM
**Impact:** Knowledge graph has many deferred features (gossip, inference, contradiction detection). Risk that developers implement more than Sprint 2 scope during #361.
**Mitigation:**
- Synthesis Part 7 explicitly defines Sprint 2 scope: data structures + direct observation + basic decay. No gossip, no inference.
- #361 ticket description references D-041 Part 3 (structs) — implementation is struct definition, not full systems
- Sprint 2 success criteria: `observe_entity()` works, relationship color appears in snapshot, remembered entities render as ghosts. That's it.
**Owner:** Dudley (server), SI (scope enforcement)
---
### RISK 4: Testing Effort Underestimated
**Severity:** LOW
**Impact:** #367 (unit tests) is 1 day estimate, but knowledge graph has complex state (confidence levels, decay, serialization). 1 day may not be enough.
**Mitigation:**
- D-041 synthesis explicitly calls out test scenarios: CRUD, decay, confidence ordering, serialization roundtrip
- If #367 slips, move to Sprint 3 — knowledge graph ships without full test coverage initially
- Integration testing via Sprint 2 proof (#357) provides baseline validation
**Owner:** Dudley (implementation + tests)
---
## Sprint Completion Criteria (Updated)
Sprint 2 is **DONE** when:
1. Player character moves on screen (Sprint 1 baseline, maintained) ✓
2. Camera follows the player character (no panning) (#116) ✓
3. Tiles render from snapshot data (floor + walls visible) (#129) ✓
4. Entities appear/disappear based on line-of-sight (#110, #111, #112) ✓
5. Fog covers areas outside the vision cone (#131, #113) ✓
6. Walking behind a wall hides what's on the other side (#110, #112) ✓
7. Walking around a corner reveals what's there (#110, #112) ✓
8. **[NEW]** Entity color reflects relationship state from knowledge graph (#361, #366) ✓
9. **[NEW]** Remembered (not visible) entities appear as ghosts with faded color (#361, #366) ✓
**Knowledge graph proof:** Observe an NPC, walk away, return. The NPC appears as a ghost at last-known position while not in LOS. Color shifts based on relationship state (green = known, amber = person of interest).
---
## Open Questions (Not Sprint 2 Blockers)
From synthesis Part 9:
| Question | Description | Sprint Impact |
|----------|-------------|---------------|
| Q-024 | Gossip propagation timing (immediate vs queued) | Sprint 3+ decision |
| Q-025 | Knowledge graph cap and eviction policy | Sprint 3+ optimization |
| Q-026 | Contradiction detection algorithm | Sprint 3+ (THE FRIEND arc) |
**None of these block Sprint 2.** Defer to Sprint 3 planning.
---
## Recommendations
### For Sprint Planning
1. **Prioritize #361 as first sprint task.** Everything depends on the KnowledgeGraph struct existing. Start day 1.
2. **Assign #366 (snapshot integration) to Oscar.** Dudley is overloaded on server critical path. Oscar can handle joint ticket after #358 (snapshot schema) is done.
3. **Defer #367 (tests) if needed.** If sprint runs tight, ship #361-#366 without full test coverage. Tests can be Sprint 3 follow-up.
4. **Track critical path daily.** With 10 serial tickets, 1-day slip = sprint slip. Daily standup must surface blockers immediately.
### For Epic #49 (Information Boundary System)
Epic #49 now overlaps significantly with #351 (Knowledge Graph Workshop). Post-sprint, consolidate:
- Mark #49 as superseded by #351, OR
- Re-scope #49 as "client-facing information UI" (journal, knowledge display) while #351 owns the data model
**Owner:** SI (post-sprint cleanup), Tyre (architecture reconciliation)
---
## Files Updated
- **New tickets:** #361-#368 created, assigned to Sprint 2
- **Cancelled tickets:** #89 (information inventory)
- **Done tickets:** #269 (CauseChain)
- **Reparenting needed:** #138, #139, #140, #141, #142 (SQL updates required)
- **New dependencies:** See "Tickets With New Dependencies" section above
---
## SQL Updates Required (Permission Denied — Manual Execution Needed)
The following SQL commands need to be executed manually (ticket CLI does not support reparenting or dependency addition):
```sql
-- Reparent information boundary tickets under #351
UPDATE tickets SET parent_id = 351, updated_at = datetime('now') WHERE id IN (138, 139, 140, 141, 142);
-- Defer Sprint 3+ tickets
UPDATE tickets SET status = 'backlog', sprint_id = NULL, updated_at = datetime('now') WHERE id IN (139, 141, 142);
-- Cancel #140 (merged into #366)
UPDATE tickets SET status = 'cancelled', updated_at = datetime('now') WHERE id = 140;
-- Add ticket descriptions for new tickets
UPDATE tickets SET description = 'Full KnowledgeGraph component with BTreeMap<StableId, EntityKnowledge> and BTreeMap<FactId, FactKnowledge>. Includes KnowledgeConfidence (4 levels), KnowledgeState (3 variants), KnowledgeSource enums. All types per D-041 synthesis Part 3. Sprint 2 uses DirectObservation and Background sources only. Estimate: 1 day.', updated_at = datetime('now') WHERE id = 361;
UPDATE tickets SET description = 'StableEntityId component and EntityRegistry resource for bidirectional StableId <-> Entity mapping. Assigns stable IDs at spawn, survives save/load. Partially resolves Q-019. Estimate: 1 day.', updated_at = datetime('now') WHERE id = 362;
UPDATE tickets SET description = 'Event-driven knowledge updates. KnowledgeEventQueue resource + processing system drains queue per tick. Decouples perception from knowledge writes. Estimate: 0.5 day.', updated_at = datetime('now') WHERE id = 363;
UPDATE tickets SET description = 'Perception system calls observe_entity() when entity in LOS, observe_entity_leaving_los() when exits. Sets Direct confidence, downgrades to KnowsDetails on departure. Estimate: 0.5 day.', updated_at = datetime('now') WHERE id = 364;
UPDATE tickets SET description = 'Knowledge decay pass once per game-minute (every 10 ticks per D-031). Configurable via DecayThresholds resource. Downgrades confidence based on tick age. Does not generate Stale state yet (Sprint 3). Estimate: 0.5 day.', updated_at = datetime('now') WHERE id = 365;
UPDATE tickets SET description = 'Observer visibility query (#112) reads KnowledgeGraph to: 1) overlay relationship color on visible entities, 2) include remembered (not visible) entities in snapshot. Implements interface from D-041 Part 5. Estimate: 1 day.', updated_at = datetime('now') WHERE id = 366;
UPDATE tickets SET description = 'Unit tests for CRUD operations, decay mechanics, confidence ordering, serialization roundtrip, relationship state transitions. Estimate: 1 day.', updated_at = datetime('now') WHERE id = 367;
UPDATE tickets SET description = 'Content authoring vocabulary document from D-041 Appendix A. Entity knowledge categories (identity, location, behavior, relationship, secret, contraband), world knowledge categories, prerequisite format for D-035 integration. Unblocks content team ticket #309. Estimate: 0.5 day.', updated_at = datetime('now') WHERE id = 368;
-- Add blocking dependencies
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (361, 112); -- KnowledgeGraph blocks observer query
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (362, 112); -- StableEntityId blocks observer query
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (362, 360); -- StableEntityId partially resolves Q-019
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (366, 358); -- Snapshot integration blocked by schema design
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (366, 356); -- Snapshot integration blocks bridge ticket
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (368, 309); -- Vocabulary document unblocks content ticket
INSERT INTO ticket_deps (blocker_id, blocked_id) VALUES (361, 182); -- KnowledgeGraph blocks divergent starting knowledge
```
**Execute via:** `db/connectors/sqlite-exec "<command>"` for each UPDATE/INSERT statement (requires bash permission restoration)
---
## Next Steps
1. **Execute SQL updates** (requires manual intervention — bash permission denied during reconciliation)
2. **Update sprint briefings** with new ticket IDs (#361-#368)
3. **Communicate critical path changes** to team (standup: knowledge graph is now on critical path)
4. **Begin #361 implementation** immediately (day 1 of sprint)
5. **Schedule daily critical path reviews** (10 serial tickets = daily tracking required)
---
**End of Sprint 2 Impact Summary**
**Files referenced:**
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.md`
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/sprints/sprint-2/server.md`
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/sprints/sprint-2/joint.md`
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/sprints/sprint-2/client.md`
- `/var/home/jeroenschweitzer/Projects/settled-reach/planning/db/connectors/ticket` (CLI used for ticket operations)