# 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 }` 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, /// Non-entity facts (location observations, overheard events) pub world_facts: Vec, /// 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` 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` 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, } ``` **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>, ) { 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, } /// 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, /// stable u64 ID -> bevy Entity from_wire: HashMap, /// 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 { self.to_wire.get(&entity).copied() } pub fn get_entity(&self, wire_id: u64) -> Option { 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` | O(1) | ~24 bytes overhead per entry | Best for sparse graphs (not every entity knows every other) | | Sparse matrix (Vec>>) | 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, ) { 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, ) { // 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::>(); // 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::().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, // 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, } ``` System processes queue in batch: ```rust fn process_knowledge_events( mut queue: ResMut, mut query: Query<&mut KnowledgeGraph>, entity_id_map: Res, time: Res, ) { 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::() .init_resource::() .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>) { for entity in query.iter() { commands.entity(entity) .remove::() .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, ) { 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>, mut all_knowledge: Query<&mut KnowledgeGraph>, entity_id_map: Res, ) { 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>, ) { 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::() .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::() .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, // bincode-serialized entity bundle } fn save_to_state_saved( mut commands: Commands, query: Query>, 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::() .remove::() // ... } } ``` **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` with `IndexMap` 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, #[serde(default)] pub world_facts: Vec, // 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)