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>
28 KiB
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:
- ECS Integration: Knowledge graph should be a per-entity Component, not a centralized Resource — fits bevy_ecs model and supports tier serialization
- Stable entity references: Wire-format
u64entity IDs solve the save/load stability problem — already in the protocol - Performance at scale: 80 Active NPCs × 50 known entities = 4,000 knowledge entries — feasible with proper indexing
- Complexity landmine: Knowledge decay implementation has hidden state explosion — needs batching
- 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.
#[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):
#[derive(Component, Debug, Clone, Serialize, Deserialize)]
pub struct InformationInventory {
pub known_facts: Vec<String>,
}
Migration strategy:
- Keep
InformationInventoryas deprecated component for v0.1 compatibility - Add
KnowledgeGraphcomponent to new NPCs - Add migration system that runs once at spawn:
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); } } - Remove
InformationInventoryafter migration complete
v0.1 scope: Only KnowledgeGraph. No migration — fresh world generation.
1.3 Concrete Rust Struct Proposal
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: u64uses wire-format IDs (stable), not bevyEntity(unstable across save/load)KnowledgeConfidenceas enum withOrdenables hierarchy queries (>= KnowsOf)KnowledgeSourcecarries context (who told me? how much do I trust them?)confidence: f32enables gradual decay (not binary)FactTypeenum 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:
#[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_wiremapping 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>>) | 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):
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):
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?"
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:
#[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:
-
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
-
Knowledge update: "Entity A observed entity B at tick T"
- Direct HashMap insert/update
- Cost: O(1)
-
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)
-
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:
#[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:
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
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
#[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
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
// 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:
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:
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
#[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:
- Q-019 (Entity ID stability): Wire-format IDs must be generated/mapped. Proposed
EntityIdMapabove solves this. - Spatial partitioning: Observer queries need spatial index. Architecture audit identified this as CRITICAL (section 3.1).
- Q-018 (Shadowcasting algorithm): LOS checks required for perception → knowledge updates.
6.2 Design Blockers
Must be decided before implementation:
- Q-016 (Knowledge hierarchy):
KnowledgeConfidenceenum needs concrete levels. Proposed:Suspects < KnowsOf < KnowsDetails < DirectObservation. - 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)
- Replace
InformationInventorywithKnowledgeGraphcomponent - Implement
EntityIdMapresource for stable IDs - Add
KnowledgeEventqueue and processing system - Direct observation only (no gossip, no inference)
- 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)
- Add gossip (entity A tells entity B about C)
- Add inference (entity A saw entity B enter building → knows B is inside)
- Add knowledge decay (batch system, once per game-minute)
Phase 3: Advanced Features (Sprint 4+)
- Knowledge confidence hierarchy (Q-016 resolved)
- Misinformation (wrong facts, discoverable)
- 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:
[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:
#[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:
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
- Workshop Round 2: Resolve Q-016 (knowledge hierarchy) with concrete enum values
- Architecture spike: Implement
EntityIdMap+ basicKnowledgeGraphin a test harness (~2-3 days) - 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)
- 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
KnowledgeGraphComponent (fits bevy_ecs, supports tier serialization) - Wire-format
u64entity 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)