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>
61 KiB
Knowledge Graph & Information Boundaries Workshop -- Round 2 Synthesis
Author: Tyre (Architecture Lead, synthesis role) Date: 2026-02-11 Workshop: Knowledge Graph & Information Boundaries (Epic #351) Status: FINAL -- decisions are canonical, structs are authoritative
Part 1: Agreements (Convergence Across All Five Analyses)
cracks knuckles
Before resolving the debates, let me document what everyone independently arrived at. The convergence is striking -- five agents analyzing from completely different angles landed in the same place on the fundamentals. That is a good sign for the architecture.
1.1 Universal Consensus
1. Per-entity ECS Component, not centralized Resource.
All five analyses agree: KnowledgeGraph is a bevy_ecs Component attached to each entity that has knowledge. Dudley, Tyre, and Gestalt each independently proposed this. SI and Paula assumed it. No dissent.
Rationale alignment: natural ECS pattern, Changed<KnowledgeGraph> dirty tracking, per-entity serialization for D-026 tier transitions, no shared mutable state.
2. Stable entity IDs, not bevy Entity handles.
Dudley and Tyre both proposed u64-based stable IDs mapped to bevy Entity at runtime. Knowledge graphs reference stable IDs. This resolves Q-019 for knowledge graph purposes.
3. KnowledgeSource tracked per-entry, not per-graph. Tyre proposed it explicitly. Gestalt and Paula independently required it for their mechanics (gossip tracking, lie detection). Dudley's struct included it. Universal agreement: each knowledge entry carries its own provenance.
4. Sprint 2 ships a stub with correct interface, not full system. SI, Dudley, and Tyre all independently identified this as the right strategy. Full gossip, inference, and decay are Sprint 3+. Sprint 2 ships: data structures, direct observation, observer query interface for #112.
5. Memory is not a concern at project scale. Dudley: ~1.5MB. Tyre: ~1.1MB for Active tier. Gestalt: ~3-7MB including Background. All well under any meaningful threshold. Performance focus should be on perception queries (shadowcasting), not knowledge graph operations.
6. Knowledge must support contradiction detection. Gestalt and Paula both independently identified THE FRIEND contradiction discovery (D-034, D-039 wow moment #3) as the emotional centerpiece. The knowledge graph must support two conflicting entries existing simultaneously and being detected as contradictory. This is load-bearing for v0.1.
7. Knowledge gates dialogue and monologue.
Gestalt (mechanical gating), Paula (narrative gating), and the existing D-028/D-035 decisions all require knowledge prerequisites on tagged line pools. The prerequisite tag on monologue lines (D-035) needs a knowledge graph to query against.
8. NPCs need their own knowledge graphs. D-010 principle 2 is explicit: information boundaries are universal. Gestalt, Paula, and Dudley all require NPC knowledge graphs for gossip propagation, observable social dynamics, and the core asymmetric information mechanic.
Part 2: Resolved Debates
Debate 1: Knowledge Confidence Hierarchy (Q-016)
Options on the table:
| Proposal | Levels | Source |
|---|---|---|
| Gestalt | 3: Suspects / KnowsOf / KnowsDetails | Mechanical simplicity, maps to dialogue gates |
| Dudley | 4: Suspects / KnowsOf / KnowsDetails / DirectObservation | Adds "currently seeing" as distinct level |
| Tyre | 5: Stale / Rumor / Inferred / Observed / Direct | Source-quality hierarchy, decay as level |
| Paula | 5: Unaware / Suspects / KnowsOf / KnowsDetails / Understands | Narrative progression, emotional depth |
Decision: 4 levels. Suspects / KnowsOf / KnowsDetails / Direct.
Rationale:
-
Paula's "Unaware" is not a level -- it is the absence of a knowledge entry. If entity A has no entry for entity B in its knowledge graph, A is unaware of B. We do not need an enum variant for "I don't know." The absence of data IS the state. Adding Unaware to the enum would require placeholder entries for every entity in the world.
-
Paula's "Understands" is mechanically indistinguishable from KnowsDetails. The narrative difference between "Kael is meeting Hael in corridor B-7" (KnowsDetails) and "Kael is trying to exit the ring to protect Hael" (Understands) is real and important -- but it is expressed through WHICH facts you know, not a higher confidence level on the same fact. If you know
kael_exit_motivation, you have KnowsDetails about that fact. The understanding emerges from having multiple KnowsDetails entries that connect. This is a content problem, not a data model problem. -
Tyre's "Stale" is not a confidence level -- it is a KnowledgeState. Stale describes temporal degradation, not acquisition quality. An entry can be Stale regardless of whether it was originally Suspects or KnowsDetails. This belongs in a separate state field (see Debate 4).
-
Tyre's "Rumor" and "Inferred" collapse into Suspects. Both represent low-confidence, indirect knowledge. The distinction between "someone mentioned X" and "I deduced X from evidence" is captured by
KnowledgeSource, not confidence level. The game design does not gate different actions at "rumor" vs "inferred" -- both gate the same thing: initial investigation options. -
Dudley's "DirectObservation" is necessary. "Currently in my LOS" is mechanically distinct from "I know details but they're not here right now." Direct observation means the entity's current position is live, not remembered. This affects fog rendering (visible vs ghost), entity color (live vs faded), and is the natural result of perception system output.
-
Gestalt's 3 levels are insufficient. "I'm currently looking at them" (Direct) is mechanically different from "I know their schedule" (KnowsDetails). Collapsing these loses the perception-to-knowledge boundary.
The hierarchy maps to game mechanics as follows:
| Level | What the character believes | How you get it | What it unlocks |
|---|---|---|---|
| Suspects | "Something's off about X" / "I've heard of X" | Observation triggers, tells, rumors, overheard fragments, single weak evidence | Initial investigation. Can ask open-ended questions. Monologue is vague. |
| KnowsOf | "X is involved in Y" / "X has connection to Z" | Confirmed observation, trusted source confirms, physical evidence found | Topic-specific dialogue. Can ask about the connection. Monologue is specific. Peer-tier access. |
| KnowsDetails | "X did Y at Z because W" | Deep evidence, full disclosure, caught in the act, multiple corroborating sources | Confrontation unlocks. Secret-tier dialogue. Monologue explains. Full investigation options. |
| Direct | "I'm looking at X right now" | Entity is in observer's LOS this tick | Live position data. Current activity visible. Maximum rendering fidelity. |
Mapping to existing decisions:
- D-028 access tiers:
surfaceavailable at any level,realat KnowsOf+,secretat KnowsDetails+ - D-033 entity color: Unknown if no entry, Known/Friendly at Suspects+, PersonOfInterest at KnowsOf+ with flagged relationship, Hostile at KnowsDetails+ with hostile relationship
- D-035 monologue
prerequisitetag:suspects:topic,knows_of:topic,knows_details:topic
Q-016 is resolved. Four levels: Suspects < KnowsOf < KnowsDetails < Direct.
Debate 2: HashMap vs BTreeMap
Options:
| Structure | Proposer | Argument |
|---|---|---|
HashMap<u64, EntityKnowledge> |
Dudley | O(1) lookup, standard Rust pattern, fast |
BTreeMap<StableId, EntityKnowledge> |
Tyre | Deterministic iteration order, D-010 principle 4 |
Decision: BTreeMap<StableId, EntityKnowledge>.
Rationale:
-
D-010 principle 4 (deterministic simulation) is a non-negotiable baseline. The architecture review consensus already established: no
HashMapin simulation code. This was a collective decision, not a preference. -
The performance difference is negligible at our scale. BTreeMap lookup is O(log N). At N=50: ~6 comparisons of
u64values. That is nanoseconds. At N=200 (extreme case): ~8 comparisons. Still nanoseconds. Dudley is correct that HashMap is O(1), but the constant factor for our N is irrelevant. -
Deterministic iteration IS used in game logic. Knowledge decay iterates all entries. Batch knowledge updates iterate entries. System ordering depends on processing entities in a stable order. If iteration order is non-deterministic, replay diverges. Even if we only "need" it for debugging today, deterministic replay (D-030 sub-decision #7, promoted to CRITICAL) requires it tomorrow.
-
Dudley himself flagged this in section 8.1: "HashMap iteration order is a determinism time bomb." The answer to his own warning is BTreeMap.
-
Serialization is deterministic with BTreeMap (sorted key order). Save files are bit-identical across runs. This matters for testing, debugging, and future multiplayer sync.
BTreeMap wins. Not close.
Debate 3: Graph Model -- Flat vs Node-based
Options:
| Model | Proposer | Structure |
|---|---|---|
| Flat map per entity | Dudley, Tyre | BTreeMap<StableId, EntityKnowledge> + BTreeMap<FactId, FactKnowledge> |
| Node-based graph | Gestalt | HashMap<KnowledgeNodeId, KnowledgeNode> + entity_knowledge index + location_knowledge index |
Decision: Flat map per entity (Dudley/Tyre model).
Rationale:
-
Gestalt's node-based model introduces a layer of indirection (
KnowledgeNodeId->KnowledgeNode) that adds complexity without corresponding mechanical benefit. Every query must resolve the node ID to get the actual data. The primary queries are: "what do I know about entity X?" and "do I know fact Y?" Both are direct map lookups in the flat model. In the node model, they require index lookup then node resolution -- two hops. -
The location_knowledge index that Gestalt proposes is a secondary index. Tyre's Option C (Component + secondary index Resource) is the correct long-term approach: build the secondary index when we need it (Sprint 3+), not baked into the primary data structure.
-
Gestalt's
pending_gossip: Vec<GossipEvent>embedded in the KnowledgeGraph is the wrong place for event data. Gossip events belong in theKnowledgeEventQueueresource (as Dudley proposed) or a system-level event channel. Mixing event queues into persistent state creates serialization problems. -
The flat model is simpler to serialize, simpler to debug, and simpler to test. At our scale (50-100 entries per entity), the simplicity advantage dominates any theoretical benefit of graph structure.
-
Memory overhead: the flat model stores data directly in the map values. The node model stores it indirectly via node IDs, requiring separate allocation for the node storage. At our scale this is negligible, but the indirection is pure cost with no benefit.
Flat map wins. The complexity budget is better spent elsewhere.
Debate 4: KnowledgeState (Active / Contradicted / Outdated)
Options:
| Approach | Proposer | Details |
|---|---|---|
| Include KnowledgeState enum | Gestalt | Active / Contradicted / Outdated / Superseded |
| Required for THE FRIEND arc | Paula | Contradiction detection is load-bearing |
| Not explicitly addressed | Dudley | Implicit in confidence decay |
Decision: Include KnowledgeState enum in Sprint 2. Stubbed to Active-only.
Rationale:
-
Paula and Gestalt are right: contradiction detection is load-bearing for the emotional centerpiece of v0.1 (THE FRIEND arc, D-034, D-039). The data model must support it from day one, even if the detection logic ships in Sprint 3.
-
However, Gestalt's "Superseded" state is unnecessary complexity. If knowledge B supersedes knowledge A, you simply update the entry. There is no need to keep the old entry with a "superseded" flag pointing to the new one. The update itself IS the supersession.
-
"Outdated" is a function of time, not a discrete state to track. An entry with
last_observed_tickfar in the past is outdated by definition. The rendering system can derive "outdated" from tick age without a separate state field. This simplifies the model. -
What we actually need for Sprint 2:
- The
KnowledgeStatefield exists on every entry. - Sprint 2 only sets
Active. - Sprint 3 adds contradiction detection: when a new observation conflicts with a
ToldByentry, both entries get markedContradicted, aContradictionEventfires, and the monologue system picks it up.
- The
-
Simplified enum for the data model:
pub enum KnowledgeState {
Active, // Currently believed true
Contradicted, // Conflicting information exists (both entries get this)
Stale, // Decay system has marked this as aged out
}
Three states, not four. Active is the default. Contradicted is the THE FRIEND detector. Stale replaces the time-based "Outdated" with an explicit flag set by the decay system, which is cleaner than having both a tick-age check AND a state flag.
KnowledgeState ships in Sprint 2 as a field. Only Active is used. Contradicted and Stale logic ships Sprint 3.
Debate 5: Sprint 2 Stub Strategy and #112 Interface
Options:
| Approach | Proposer | Details |
|---|---|---|
| Define query interface, stub returns true | SI | #112 needs the contract, not the logic |
| Concrete Sprint 2 deliverables list | Tyre | Full struct + direct observation + basic decay |
| Phased approach, direct observation only | Dudley | No gossip, no inference, no decay in Sprint 2 |
Decision: Ship data structures + direct observation + basic decay. No gossip, no inference.
This synthesizes all three positions. Here is exactly what Sprint 2 delivers:
Ships in Sprint 2:
KnowledgeGraphcomponent withBTreeMap<StableId, EntityKnowledge>andBTreeMap<FactId, FactKnowledge>.KnowledgeConfidenceenum with all 4 levels (Suspects, KnowsOf, KnowsDetails, Direct).KnowledgeSourceenum withDirectObservationandBackgroundvariants functional. Other variants (Heard,ToldBy,Inferred) exist in the enum but no system generates them.KnowledgeStateenum with all 3 variants. OnlyActiveis set by any system.StableEntityIdcomponent andEntityRegistryresource for bidirectional StableId <-> Entity mapping.observe_entity()method: perception system calls this when entity is in LOS. Sets confidence to Direct.observe_entity_leaving_los()method: perception system calls this when entity leaves LOS. Downgrades Direct to KnowsDetails (or KnowsOf if brief sighting).knows_entity(),confidence_of(),relationship_with(),knows_fact(),fact_at_least()query methods.- Basic decay: once per game-minute (every 10 ticks per D-031), configurable via
DecayThresholdsresource. Only downgrades confidence levels based on tick age. Does not generate Stale state yet. - Player character gets
KnowledgeGraphpopulated from background data at game start. - Active-tier NPCs get
KnowledgeGraphwith empty or minimal background knowledge.
The #112 Interface Contract:
This is the answer SI needs. Ticket #112 (Observer visibility query) calls the following:
/// The observer visibility query reads the knowledge graph to:
/// 1. Overlay relationship color on visible entities
/// 2. Include remembered (not currently visible) entities in the snapshot
/// For Sprint 2, the query pattern is:
fn build_observer_snapshot(
observer_knowledge: &KnowledgeGraph,
visible_entities: &[VisibleEntityRaw], // From shadowcasting + spatial query
entity_registry: &EntityRegistry,
current_tick: u64,
) -> Vec<SnapshotEntity> {
let mut result = Vec::new();
// Visible entities: update knowledge, include with live data
for entity in visible_entities {
let stable_id = entity_registry.to_stable(entity.entity);
// Knowledge system records Direct observation (separate write system)
let relationship = observer_knowledge.relationship_with(&stable_id);
result.push(SnapshotEntity {
stable_id,
position: entity.position,
relationship,
visibility: EntityVisibility::Visible,
});
}
// Remembered entities: include from knowledge graph
for (stable_id, knowledge) in observer_knowledge.known_entities_iter() {
if knowledge.confidence < KnowledgeConfidence::Direct {
// Not currently visible, but remembered
if let Some(pos) = knowledge.last_known_position {
let age = current_tick.saturating_sub(knowledge.last_observed_tick);
result.push(SnapshotEntity {
stable_id: *stable_id,
position: pos,
relationship: knowledge.relationship,
visibility: EntityVisibility::Remembered {
confidence: knowledge.confidence,
age_ticks: age,
},
});
}
}
}
result
}
Sprint 2 behavior: Geometric visibility (shadowcasting + vision cone) determines what is visible. The knowledge graph provides relationship color for visible entities and last-known-position ghosts for remembered entities. No knowledge-based filtering of visibility itself -- that is Sprint 3+ (e.g., "can you identify this person?" requires prior observation).
Deferred to Sprint 3+:
- NPC-to-NPC knowledge propagation (gossip, reports)
ToldByandInferredsource generationContradictedstate detection and event firingStalestate from decay systemKnowledgeIndexreverse lookup resource- Knowledge-driven NPC behavior changes
- Dialogue access tier filtering by knowledge state (D-028 Layer 1 integration)
- Monologue triggering based on knowledge transitions
- Full CauseChain integration (knowledge change -> provenance -> monologue trigger)
- Misinformation (deliberately false knowledge entries)
Debate 6: Gossip Propagation Timing (Q-024)
Options:
| Approach | When gossip propagates | Effect |
|---|---|---|
| Immediate during conversation | When two NPCs talk, knowledge transfers instantly | More reactive, harder to predict |
| Queued for routine intersection | Knowledge transfers at scheduled meeting points | More predictable, player can exploit timing |
Decision: Deferred to Sprint 3. Queued approach preferred.
Rationale for deferral: Gossip propagation is not in Sprint 2 scope. Making this decision now without implementation experience would be premature.
Rationale for preferring queued: The queued approach aligns better with the core gameplay. If gosssip propagates at routine intersections (bar visits, shift changes, scheduled meetings), the player can:
- Observe NPCs meeting and predict information flow
- Time their actions between intersection points to exploit windows of ignorance
- Strategically attend or skip routine events to control what they overhear
This creates the "I need to be at the bar before shift change to see who talks to whom" mechanic that Gestalt identified as fun.
Immediate propagation makes gossip invisible and unpredictable. Queued propagation makes it observable and exploitable. Observable and exploitable is the design goal (D-010 principle 2, D-028 Layer 4).
Record as Q-024 with preferred direction. Resolve formally in Sprint 3 planning.
Part 3: Decision D-041 -- Knowledge Graph Data Model
Decision ID: D-041 Status: CONFIRMED Resolves: Q-016 (knowledge hierarchy), partially resolves Q-019 (entity ID stability) Blocks: #352 (Observer Snapshot Pipeline Workshop)
The following Rust struct definitions are the canonical reference for the knowledge graph data model. All implementation must conform to these types.
// ============================================================
// D-041: Knowledge Graph Data Model
// Canonical reference. All implementations conform to this.
// ============================================================
use std::collections::BTreeMap;
use serde::{Serialize, Deserialize};
// --- Identity Types ---
/// Stable entity identifier that survives save/load cycles.
/// NOT a bevy_ecs Entity (which is a generational index).
/// Assigned once at entity creation, never changes.
/// Resolves Q-019 for knowledge graph and snapshot purposes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct StableId(pub u64);
/// Typed fact identifier for non-entity knowledge.
/// Format: "category.topic" (e.g., "contraband.ring_exists", "location.corridor_b7_restricted").
/// Lexicographic ordering in BTreeMap provides deterministic iteration.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct FactId(pub String);
// --- Knowledge Confidence (Q-016 Resolution) ---
/// Knowledge confidence hierarchy. Discrete enum, NOT a continuous float.
/// Derives Ord: Suspects < KnowsOf < KnowsDetails < Direct.
/// This ordering is load-bearing -- do not reorder variants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum KnowledgeConfidence {
/// "Something seems off about X" / "I've heard the name"
/// Gates: initial investigation, vague monologue, open-ended questions.
Suspects = 0,
/// "X is involved in Y" / "X has a connection to Z"
/// Gates: topic-specific dialogue, specific monologue, peer-tier access.
KnowsOf = 1,
/// "X did Y at Z because W" -- actionable detail.
/// Gates: confrontation, secret-tier dialogue, detailed monologue.
KnowsDetails = 2,
/// "I'm looking at X right now" -- currently in observer's LOS.
/// Gates: live position data, current activity visible, maximum rendering fidelity.
/// Automatically set by perception system, downgraded when entity leaves LOS.
Direct = 3,
}
impl KnowledgeConfidence {
/// Step down one confidence level (used by decay system).
pub fn decayed(self) -> Self {
match self {
Self::Direct => Self::KnowsDetails,
Self::KnowsDetails => Self::KnowsOf,
Self::KnowsOf => Self::Suspects,
Self::Suspects => Self::Suspects, // Floor -- does not decay below Suspects
}
}
}
// --- Knowledge State ---
/// Temporal/logical state of a knowledge entry.
/// Orthogonal to confidence: a KnowsDetails entry can be Active or Contradicted.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum KnowledgeState {
/// Currently believed true. Default state.
Active,
/// Conflicting information exists. Both conflicting entries receive this state.
/// Triggers monologue event when set. THE FRIEND arc detector.
Contradicted,
/// Decay system has aged this entry beyond stale threshold.
/// Entry remains in graph but is treated as unreliable.
Stale,
}
impl Default for KnowledgeState {
fn default() -> Self {
Self::Active
}
}
// --- Knowledge Source ---
/// How knowledge was acquired. Tracked per-entry for provenance.
/// CauseChain (D-030) can reference this for monologue trigger explanations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KnowledgeSource {
/// Directly seen by this entity's LOS (perception system).
DirectObservation { tick: u64 },
/// Heard via D-018 sound model (medium/long range).
Heard { tick: u64, range: SoundRange },
/// Told by another entity during conversation (dialogue, gossip).
ToldBy { source_id: StableId, tick: u64 },
/// Inferred from combining other knowledge entries.
Inferred { basis: Vec<FactId> },
/// Starting knowledge from character background (D-013 insert data).
Background,
}
/// Sound range classification from D-018.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SoundRange {
Close,
Medium,
Long,
}
// --- Relationship State (D-033) ---
/// Relationship state drives D-033 entity color rendering.
/// Derived from knowledge + NPC relationship axes (D-024).
/// Client maps this to color palette defined in D-033.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RelationshipState {
/// No prior knowledge. Teal #4a9ebb.
Unknown,
/// Recognized, neutral-to-positive. Soft green #6bc9a6.
Known,
/// Positive relationship, trusted. Soft green #6bc9a6.
Friendly,
/// Flagged by monologue/investigation. Amber #e8c547.
PersonOfInterest,
/// Character KNOWS there is danger. Red #d45d5d.
Hostile,
}
impl Default for RelationshipState {
fn default() -> Self {
Self::Unknown
}
}
// --- Entity Knowledge ---
/// What entity A knows about entity B.
/// One entry per known entity in the BTreeMap.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityKnowledge {
/// Last position this entity was observed at. None if never directly seen.
pub last_known_position: Option<TilePosition>,
/// Tick when this entity was last directly observed by LOS.
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? (Q-016 hierarchy)
pub confidence: KnowledgeConfidence,
/// How was this knowledge acquired?
pub source: KnowledgeSource,
/// Logical state (active, contradicted, stale).
pub state: KnowledgeState,
/// Relationship assessment (drives D-033 entity color).
pub relationship: RelationshipState,
/// Known attributes of the target entity.
/// Keys are structured (name, role, faction, etc.)
pub known_attributes: BTreeMap<String, String>,
}
/// Non-entity fact knowledge (locations, events, abstract knowledge).
/// Used for monologue prerequisites and dialogue gating.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FactKnowledge {
/// Confidence level for this fact.
pub confidence: KnowledgeConfidence,
/// How was this fact acquired?
pub source: KnowledgeSource,
/// Logical state.
pub state: KnowledgeState,
/// Tick when this fact was learned.
pub acquired_tick: u64,
}
// --- The Knowledge Graph Component ---
/// Per-entity knowledge component. THE core data structure.
/// Attached as a bevy_ecs Component to every entity that has knowledge
/// (player character, Active-tier NPCs, Background-tier NPCs).
#[derive(Component, Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeGraph {
/// What this entity knows about other entities.
/// Key: StableId of the known entity.
/// BTreeMap for deterministic iteration (D-010 principle 4).
pub entities: BTreeMap<StableId, EntityKnowledge>,
/// Non-entity facts this entity knows.
/// Key: FactId in "category.topic" format.
/// BTreeMap for deterministic iteration.
pub facts: BTreeMap<FactId, FactKnowledge>,
}
impl KnowledgeGraph {
pub fn new() -> Self {
Self {
entities: BTreeMap::new(),
facts: BTreeMap::new(),
}
}
/// Construct with starting facts (character background per D-013).
pub fn with_background(facts: Vec<(FactId, FactKnowledge)>) -> Self {
Self {
entities: BTreeMap::new(),
facts: facts.into_iter().collect(),
}
}
// --- Read Queries (used by visibility system, snapshot assembly, monologue) ---
/// Does this entity know about another entity at all?
pub fn knows_entity(&self, id: &StableId) -> bool {
self.entities.contains_key(id)
}
/// What confidence level for a known entity?
pub fn confidence_of(&self, id: &StableId) -> Option<KnowledgeConfidence> {
self.entities.get(id).map(|k| k.confidence)
}
/// 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)
}
/// Does this entity know a specific fact?
pub fn knows_fact(&self, id: &FactId) -> bool {
self.facts.contains_key(id)
}
/// Is 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)
}
/// Iterate all known entities (deterministic order via BTreeMap).
pub fn known_entities_iter(&self) -> impl Iterator<Item = (&StableId, &EntityKnowledge)> {
self.entities.iter()
}
// --- Write Operations (used by perception system, knowledge update system) ---
/// Record a direct observation of another entity (entity is in LOS).
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 },
state: KnowledgeState::Active,
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 };
// Do NOT reset state here -- a Contradicted entry stays Contradicted
// even if you're looking at the entity right now.
}
/// Entity has left the observer's LOS. Downgrade from Direct.
pub fn observe_entity_leaving_los(&mut self, target: &StableId, tick: u64) {
if let Some(entry) = self.entities.get_mut(target) {
if entry.confidence == KnowledgeConfidence::Direct {
entry.confidence = KnowledgeConfidence::KnowsDetails;
entry.last_updated_tick = tick;
}
}
}
/// Set relationship state for an entity.
pub fn set_relationship(&mut self, target: &StableId, state: RelationshipState) {
if let Some(entry) = self.entities.get_mut(target) {
entry.relationship = state;
}
}
/// Run knowledge decay pass. Called periodically (once per game-minute).
pub fn decay(&mut self, current_tick: u64, thresholds: &DecayThresholds) {
for (_id, knowledge) in self.entities.iter_mut() {
// Direct confidence is managed by perception system, not decay.
if knowledge.confidence == KnowledgeConfidence::Direct {
continue;
}
let age = current_tick.saturating_sub(knowledge.last_observed_tick);
if age > thresholds.stale_after {
knowledge.state = KnowledgeState::Stale;
} else if age > thresholds.decay_after {
knowledge.confidence = knowledge.confidence.decayed();
knowledge.last_updated_tick = current_tick;
}
}
}
}
// --- Decay Configuration ---
/// Configuration resource for knowledge decay rates.
/// D-011: "fog returns when you leave."
#[derive(Resource, Debug, Clone)]
pub struct DecayThresholds {
/// Ticks before knowledge begins decaying.
/// Default: 600 ticks = 1 game-hour (at 10 tps per D-031).
pub decay_after: u64,
/// Ticks before knowledge becomes Stale.
/// Default: 3600 ticks = 6 game-hours.
pub stale_after: u64,
}
impl Default for DecayThresholds {
fn default() -> Self {
Self {
decay_after: 600, // 1 game-hour
stale_after: 3600, // 6 game-hours
}
}
}
// --- Entity ID Infrastructure (Q-019) ---
/// Stable entity ID component. Assigned once at spawn, never changes.
/// Serialized with entity for save/load.
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StableEntityId(pub StableId);
/// Bidirectional mapping between StableId and bevy Entity.
/// Resource, updated on spawn/despawn.
#[derive(Resource, Debug, Default)]
pub struct EntityRegistry {
pub by_stable_id: BTreeMap<StableId, Entity>,
pub by_entity: BTreeMap<Entity, StableId>,
next_id: u64,
}
impl EntityRegistry {
pub fn new(seed_offset: u64) -> Self {
Self {
by_stable_id: BTreeMap::new(),
by_entity: BTreeMap::new(),
next_id: seed_offset,
}
}
/// Register a new entity and assign a StableId.
pub fn register(&mut self, entity: Entity) -> StableId {
if let Some(id) = self.by_entity.get(&entity) {
return *id;
}
let id = StableId(self.next_id);
self.next_id += 1;
self.by_stable_id.insert(id, entity);
self.by_entity.insert(entity, id);
id
}
/// Lookup: StableId -> Entity (for ECS queries).
pub fn to_entity(&self, id: &StableId) -> Option<Entity> {
self.by_stable_id.get(id).copied()
}
/// Lookup: Entity -> StableId (for knowledge graph keys).
pub fn to_stable(&self, entity: Entity) -> Option<StableId> {
self.by_entity.get(&entity).copied()
}
/// Remove a despawned entity from the registry.
/// Called AFTER knowledge cleanup (tombstone pattern).
pub fn unregister(&mut self, entity: Entity) {
if let Some(id) = self.by_entity.remove(&entity) {
self.by_stable_id.remove(&id);
}
}
}
// --- Observer Snapshot Integration ---
/// How an entity appears in the observer snapshot.
/// Extends VisibleEntity for #358 (ObserverSnapshot v2).
#[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,
},
}
// --- Knowledge Event Queue ---
/// Events that modify knowledge graphs. Produced by perception and
/// other systems. Consumed by the knowledge update system.
#[derive(Debug, Clone)]
pub struct KnowledgeEvent {
pub observer: Entity,
pub tick: u64,
pub event_type: KnowledgeEventType,
}
#[derive(Debug, Clone)]
pub enum KnowledgeEventType {
/// Observer saw entity at position (sets Direct confidence).
DirectObservation { target: Entity, position: TilePosition },
/// Entity left observer's LOS (downgrades from Direct).
LeftLOS { target: Entity },
/// Sprint 3+: Observer heard something.
Heard { target: Entity, position: TilePosition, range: SoundRange },
/// Sprint 3+: Entity told observer about another entity.
ToldBy { source: Entity, about: Entity, knowledge: EntityKnowledge },
}
/// Resource: queue of pending knowledge events.
/// Drained once per tick by the knowledge update system.
#[derive(Resource, Default)]
pub struct KnowledgeEventQueue {
pub events: Vec<KnowledgeEvent>,
}
Part 4: Decision -- Q-016 Resolution
Q-016 Status: RESOLVED.
Hierarchy Levels
Four levels: Suspects < KnowsOf < KnowsDetails < Direct
"Unaware" is the absence of an entry, not a level.
Mechanical Consequences Per Level
| Level | Dialogue Access | Monologue Behavior | Entity Color Eligible | Investigation Options |
|---|---|---|---|---|
| Suspects | Open-ended probes only. Cannot ask about specific topics. | Vague: "Something's off about..." / "I think I've seen them before." | Unknown (teal) or Known (green) depending on relationship | Can investigate. Can observe. Cannot confront. |
| KnowsOf | Topic-specific questions. Peer-tier access (D-028 Layer 1). | Specific: "Kael has been meeting someone. Sera seems worried about it." | PersonOfInterest (amber) if flagged | Can ask about connections. Can investigate deeper. |
| KnowsDetails | Confrontation lines unlock. Secret-tier access with sufficient trust. | Detailed: "Kael met Hael in corridor B-7 at second shift. Sera is covering for them." | PersonOfInterest (amber) or Hostile (red) | Can confront. Can exploit. Can make informed choices. |
| Direct | As KnowsDetails, plus real-time observations available as topics. | Live commentary: "Kael is heading toward the restricted corridor. Right now." | Same as underlying relationship | Live position data. Current activity visible. |
Narrative Examples Per Level (v0.1: Smuggler observing Kael)
Suspects:
- id: mon_kael_suspicion_001
character: smuggler
trigger: observe_npc
prerequisite:
knowledge:
- subject: kael_davan
flag: behavior_change
level: suspects
text: "Kael's been off lately. Can't put my finger on it, but something's different."
KnowsOf:
- id: mon_kael_observation_001
character: smuggler
trigger: observe_npc
prerequisite:
knowledge:
- subject: kael_davan
flag: observed_out_of_place
level: knows_of
text: "Kael, in the restricted corridor. He's not scheduled for that area."
KnowsDetails:
- id: mon_kael_detail_001
character: smuggler
trigger: post_conversation
prerequisite:
knowledge:
- subject: kael_davan
flag: exit_attempt
level: knows_details
text: "He's trying to get out. For Hael. Damn it, Kael, you should have talked to me first."
Content Authoring Format (resolves #309 dependency)
The prerequisite tag on monologue and dialogue lines (D-035) uses this format:
prerequisite:
knowledge:
- subject: <entity_stable_id or named_alias>
flag: <fact_id topic portion>
level: suspects | knows_of | knows_details
The level maps directly to KnowledgeConfidence enum. Direct is never used as a prerequisite -- it is a perception state, not a knowledge threshold. Content authors use suspects, knows_of, or knows_details.
The flag maps to a FactId. The convention is category.topic where category is one of: identity, location, behavior, relationship, secret, contraband, event, system. The engine strips the subject prefix to construct the full FactId: if subject is kael_davan and flag is exit_attempt, the FactId queried is kael_davan.exit_attempt.
This gives Paula's content team (#309) the vocabulary they need.
Part 5: Knowledge Flow Specification
How Knowledge Enters the Graph
| Source | Trigger | Confidence Set | Sprint |
|---|---|---|---|
| Direct observation | Entity in observer's LOS (perception system) | Direct | Sprint 2 |
| Leaves LOS | Entity exits observer's LOS | KnowsDetails (downgrade from Direct) | Sprint 2 |
| Background | Character creation / game start | Varies (set per content definition) | Sprint 2 |
| Told by NPC | Dialogue / gossip conversation | KnowsOf (trusted source) or Suspects (untrusted) | Sprint 3 |
| Overheard | Within D-018 medium sound range of NPC conversation | Suspects | Sprint 3 |
| Physical evidence | Interact with terminal, document, manifest | KnowsOf or KnowsDetails depending on evidence | Sprint 3 |
| Inference | Multiple related facts trigger pattern detection | Suspects (auto-generated) | Sprint 4+ |
How Knowledge Propagates
Sprint 3+ (deferred per Debate 5 and 6):
- NPC A and NPC B have a conversation (triggered by routine intersection, Q-024 preferred: queued approach).
- Gossip eligibility check: A's personality (gossip tendency from D-024 personality traits), trust in B, current mood, triangle pressure.
- For each eligible knowledge entry A decides to share: insert into B's knowledge graph with
source: ToldBy { source_id: A, tick }, confidence one level below A's confidence (floor: Suspects). - If player is within medium sound range (D-018): player's knowledge graph gets the entry with
source: Heard, confidence: Suspects.
How Knowledge Decays
D-011: "Fog returns when you leave."
- Decay system runs once per game-minute (every 10 ticks per D-031).
- For each entity in the knowledge graph where
confidence != Direct:- If
age > stale_after(default 6 game-hours): setstate = Stale. - Else if
age > decay_after(default 1 game-hour): callconfidence.decayed().
- If
- Entries with
state == Staleremain in the graph but:- Are not included in observer snapshot
remembered_entities. - Do not satisfy
fact_at_least()checks at KnowsOf or higher. - Can be refreshed to Active by a new observation.
- Are not included in observer snapshot
- Entries are never deleted by decay. They persist indefinitely at Suspects/Stale. This preserves "I used to know X" and supports tombstone queries.
System Ordering
Phase 1: Time
advance_tick (SimulationTime resource)
Phase 2: Perception (read world state, produce events)
spatial_query_system -> entities in range per observer
shadowcasting_system -> LOS per observer (Q-018 algorithm)
vision_cone_system -> modulate by facing direction (D-015)
emit_knowledge_events -> produce KnowledgeEvents for visible/departed entities
Phase 3: Knowledge (consume events, update graphs)
process_knowledge_events -> drain KnowledgeEventQueue, write to KnowledgeGraph components
decay_knowledge -> periodic decay pass (once per game-minute)
Phase 4: Snapshot (read knowledge, produce output)
build_observer_snapshot -> read KnowledgeGraph + visibility for ObserverSnapshot
serialize_and_send -> MessagePack -> IPC bridge -> Godot client
The key constraint: Phase 2 READS world state but does NOT write to KnowledgeGraph. Phase 3 WRITES to KnowledgeGraph. Phase 4 READS KnowledgeGraph. This avoids mutable borrow conflicts in bevy_ecs.
Event Queue Design
The KnowledgeEventQueue resource is the decoupling point between perception and knowledge systems.
- Produced by: Perception systems (Phase 2). Multiple systems can push events concurrently because the queue is a
Vec<KnowledgeEvent>on aResMut. - Consumed by:
process_knowledge_events(Phase 3). Drains the entire queue each tick. - Events are NOT serialized. They are transient, per-tick. The KnowledgeGraph component IS the persistent state.
- Ordering within a tick: Events are processed in insertion order (Vec). Perception systems run in a stable bevy_ecs schedule order. This is deterministic.
Part 6: Performance Budget
Memory at 80 Active NPCs
Per EntityKnowledge entry:
last_known_position: Option<TilePosition> = 16 bytes
last_observed_tick: u64 = 8 bytes
last_updated_tick: u64 = 8 bytes
confidence: KnowledgeConfidence = 1 byte (+ 3 padding)
source: KnowledgeSource = ~32 bytes (largest variant: ToldBy)
state: KnowledgeState = 1 byte (+ 3 padding)
relationship: RelationshipState = 1 byte (+ 3 padding)
known_attributes: BTreeMap<String, String> = ~128 bytes (3-5 entries typical)
BTreeMap node overhead = ~48 bytes
---
Total per entry: ~250 bytes
Per NPC knowledge graph (50 known entities, 20 facts):
Entity entries: 50 x 250 bytes = 12,500 bytes
Fact entries: 20 x 80 bytes = 1,600 bytes
Component overhead = ~64 bytes
---
Total per NPC: ~14 KB
Active tier (80 NPCs): ~1.1 MB
Background tier (2,000 NPCs, 10 entries each): ~5 MB
State-saved (serialized, not in memory): ~0 MB live
---
Total live memory: ~6 MB
Verdict: Memory is a non-issue. Total knowledge graph memory is less than a single texture. This is well within budget for any modern system.
Query Time Targets
| Operation | Target | Actual (estimated) | Frequency |
|---|---|---|---|
knows_entity() lookup |
<1us | ~100ns (BTreeMap, N=50) | Per visible entity per tick |
observe_entity() write |
<1us | ~200ns (BTreeMap insert) | Per visible entity per tick |
| Full decay pass (80 NPCs x 50 entries) | <1ms | ~0.5ms | Once per game-minute |
| Snapshot assembly (30 visible + 20 remembered) | <2ms | ~1ms | Once per tick (player) |
| Total knowledge system budget per tick | <5ms | ~2-3ms | Every tick |
The knowledge graph is NOT on the critical path. Shadowcasting and spatial queries consume 10-20ms per tick. Knowledge operations are an order of magnitude cheaper.
Spatial Partitioning Strategy
The knowledge graph does not need its own spatial partitioning. Knowledge updates happen AFTER perception computes visibility. By the time we reach the knowledge graph, we already have a filtered set of 5-20 visible entities from the spatial query + shadowcasting pipeline. Writing 5-20 BTreeMap entries is trivial.
Spatial partitioning is needed for the perception system (spatial index for "entities in range" queries). That is a separate concern addressed by #110-#112. The knowledge graph rides the perception system's spatial infrastructure -- it does not need its own.
Part 7: Sprint 2 Scope
Exactly What Ships
| Item | Description | Effort Estimate | Ticket |
|---|---|---|---|
KnowledgeGraph component |
Full struct per Part 3. BTreeMap<StableId, EntityKnowledge> + BTreeMap<FactId, FactKnowledge> | 1 day | New: child of #351 |
KnowledgeConfidence enum |
4 levels with Ord derivation | Included above | Included above |
KnowledgeState enum |
3 variants, only Active used | Included above | Included above |
KnowledgeSource enum |
All variants defined, only DirectObservation + Background generated | Included above | Included above |
StableEntityId + EntityRegistry |
Stable ID assignment, bidirectional mapping | 1 day | New: resolves Q-019, child of #351 |
KnowledgeEventQueue + processing system |
Event-driven knowledge updates, drain per tick | 0.5 day | New: child of #351 |
| Direct observation flow | Perception -> KnowledgeEvent -> KnowledgeGraph | 0.5 day | New: child of #351 |
| LOS departure handling | observe_entity_leaving_los() called when entity exits LOS | 0.5 day | Included in above |
| Basic decay system | Once per game-minute, configurable thresholds, confidence downgrade | 0.5 day | New: child of #351 |
| Observer snapshot integration (#112) | VisibilityResult includes relationship + remembered entities | 1 day | Part of #112 |
| Unit tests | CRUD, decay, confidence ordering, serialization roundtrip | 1 day | New: child of #351 |
| Player character background knowledge | KnowledgeGraph populated at spawn from content data | 0.5 day | Part of character spawn |
Total Sprint 2 estimate: ~6.5 developer-days for the knowledge graph system.
The #112 Interface Contract
Ticket #112 (Observer visibility query) integrates with the knowledge graph as follows:
- Input:
#112produces a list of geometrically visible entities (from shadowcasting + vision cone). - Knowledge write: For each visible entity,
#112emits aKnowledgeEvent::DirectObservation. The knowledge update system (Phase 3) writes Direct confidence to the observer's KnowledgeGraph. - Knowledge read:
#112reads the observer's KnowledgeGraph to:- Get
RelationshipStatefor each visible entity (D-033 entity color). - Get
remembered_entities-- entities in the knowledge graph but NOT currently visible (fog ghosts).
- Get
- Output:
VisibilityResultcontaining visible entities (with relationship color) and remembered entities (with confidence + age for rendering).
The read/write split is enforced by system ordering (Phase 2 reads, Phase 3 writes, Phase 4 reads). #112 sits in Phase 2 + Phase 4.
Sprint 2 behavior: Knowledge-based filtering of visibility does NOT happen in Sprint 2. All geometrically visible entities are visible regardless of knowledge state. Knowledge only adds information (relationship color, remembered ghosts) -- it does not subtract visibility. Knowledge-based visibility filtering (e.g., "can you identify this person?") is Sprint 3+.
The Stub Strategy
Sprint 2 ships a knowledge graph that is:
- Architecturally complete: All types defined. All query methods work. Serialization works.
- Feature-incomplete: Only DirectObservation and Background sources generate knowledge. No gossip, no inference, no misinformation, no contradiction detection.
- Extensible without refactoring: Adding Sprint 3 features (ToldBy source, Contradicted state, gossip propagation) requires NEW systems that write to the EXISTING data structures. No struct changes needed.
This is the "design for it now, build it later" pattern that actually works cleanly. The Sprint 2 stub IS the Sprint 3 foundation.
Part 8: Sprint 2 Ticket Impact
Based on SI's analysis in Round 1.
New Tickets (children of #351)
| Title | Type | Priority | Blocks | Estimate |
|---|---|---|---|---|
| Implement KnowledgeGraph component + types (D-041) | story | critical | #112 | 1 day |
| StableEntityId component + EntityRegistry resource | story | critical | #112, #360 | 1 day |
| KnowledgeEventQueue + processing system | story | high | #112 | 0.5 day |
| Direct observation knowledge flow | story | critical | #112 | 0.5 day |
| Basic knowledge decay system | story | high | - | 0.5 day |
| Observer snapshot knowledge integration | task | critical | #358, #356 | 1 day |
| Knowledge graph unit test suite | task | high | - | 1 day |
| Knowledge vocabulary for v0.1 content (#309) | task | high | content team | 0.5 day |
Existing Ticket Reconciliation
| Ticket | Action | Rationale |
|---|---|---|
| #49 (Information Boundary System epic) | Reparent children under #351 | #351 is now the authoritative epic for knowledge/information systems |
| #89 (Information inventory) | Mark cancelled, reference #351 |
Subsumed by KnowledgeGraph component story |
| #138 (Information tag schema) | Reparent under #351, mark as child of KnowledgeGraph story | Tag schema IS the KnowledgeConfidence + FactId system |
| #139 (Component-level access control) | Reparent under #351, defer to Sprint 3 | Access control depends on knowledge system |
| #140 (Entity visibility filtering) | Reparent under #351, merge into observer snapshot integration task | Visibility filtering IS the #112 knowledge integration |
| #141 (Knowledge-based information gating) | Reparent under #351, defer to Sprint 3 | Gating depends on knowledge system + dialogue system |
| #142 (NPC information boundaries) | Reparent under #351, defer to Sprint 3 | NPC knowledge = KnowledgeGraph component per NPC, but NPC-to-NPC boundary enforcement is Sprint 3 |
| #269 (CauseChain component) | Mark done |
Already implemented per audit (server/src/cause_chain.rs). Integration with knowledge is Sprint 3+ |
| #272 (Information boundary negative test suite) | Reparent under #351, defer to Sprint 3 (D-030 Phase 2) | Negative tests require implemented knowledge system |
| #309 (Knowledge state vocabulary) | Unblock with this workshop output | Part 4 of this document provides the vocabulary. Content team can begin authoring. |
| #182 (Divergent starting knowledge) | Add dependency on #351 KnowledgeGraph story | Starting knowledge = KnowledgeGraph::with_background() call at character spawn |
| #360 (Q-019 entity ID stability) | Partially resolved by StableEntityId + EntityRegistry | Client-side mapping (Godot StableId -> scene node) remains open |
Sprint 2 Dependency Updates
- Add explicit dependency:
KnowledgeGraph component (new ticket) -> blocks -> #112 - Add explicit dependency:
StableEntityId (new ticket) -> blocks -> #360 - Add explicit dependency:
Observer snapshot knowledge integration (new ticket) -> blocks -> #358 - #112 description should be updated with the interface contract from Part 7.
Part 9: Open Questions Remaining
Resolved by This Workshop
| Question | Resolution |
|---|---|
| Q-016 (Knowledge hierarchy) | RESOLVED. Four levels: Suspects < KnowsOf < KnowsDetails < Direct. See Part 4. |
| Q-019 (Entity ID stability) | PARTIALLY RESOLVED. Server-side: StableEntityId + EntityRegistry. Client-side mapping remains open. |
New Questions Raised
| Question | Description | Assigned To | Source |
|---|---|---|---|
| Q-024 (Gossip propagation timing) | Immediate during conversation vs queued for routine intersection. Preferred direction: queued. | Gestalt, Tyre | Gestalt Round 1 |
| Q-025 (Knowledge graph cap and eviction) | At what point does an NPC's knowledge graph need entry eviction? Dudley proposed MAX_ENTITY_KNOWLEDGE = 100. What is the eviction policy? LRU by last_observed_tick? Lowest confidence first? | Tyre, Dudley | Dudley Round 1 section 8.3 |
| Q-026 (Contradiction detection algorithm) | How exactly does the system detect that two knowledge entries contradict each other? Content-authored contradiction pairs? Automatic same-subject different-value detection? Hybrid? | Gestalt, Paula, Dudley | Gestalt/Paula Round 1 |
Questions Deferred (Not In Scope For This Workshop)
- Q-017 (Triangle pressure threshold): Gestalt proposed 70/100, Paula proposed 75-100. Not blocking Sprint 2. Resolve in Sprint 3 planning.
- Knowledge-based dialogue filtering details: How exactly does the dialogue system query knowledge prerequisites at runtime? Blocked by dialogue system implementation (Sprint 3).
- Knowledge graph visualization/debugging tool: Q-023 mentions debug overlay. Knowledge graph overlay is useful but not Sprint 2 critical.
- Cross-entity queries ("who knows about X?"): KnowledgeIndex reverse lookup resource. Deferred to Sprint 3+ when NPC-NPC knowledge interactions require it.
Appendix A: Knowledge Vocabulary for Content Authoring (#309)
This section directly addresses ticket #309 and unblocks the content team.
Entity Knowledge Categories
| Category | Example Flags | Content Use |
|---|---|---|
| identity | knows_name, knows_role, knows_faction |
Name vs "the dock worker" in monologue |
| location | last_seen_location, observed_out_of_place, knows_routine_location |
Deviation detection, return-visit monologue |
| behavior | behavior_change, observed_nervous, observed_lying |
Character assessment monologue |
| relationship | knows_relationship_to_{npc}, observed_meeting_{npc}, knows_triangle |
Social dynamics monologue, gossip detection |
| secret | suspects_secret, knows_secret, knows_motivation |
Progressive revelation, confrontation gating |
| contraband | knows_ring_exists, knows_ring_member, knows_smuggling_operation |
Conspiracy monologue, investigation progression |
World Knowledge Categories
| Category | Example Flags | Content Use |
|---|---|---|
| location | knows_restricted_area, mapped_corridor_b7, knows_dead_drop |
Navigation, location-specific monologue |
| event | witnessed_meeting, heard_argument, discovered_evidence |
Event reaction monologue |
| system | knows_shift_schedule, knows_security_gaps, knows_manifest_process |
Operational knowledge, investigation tools |
| faction | knows_commission_presence, knows_ring_structure |
Political assessment monologue |
Prerequisite Format (D-035 Integration)
# Entity knowledge prerequisite
prerequisite:
knowledge:
- subject: kael_davan
flag: exit_attempt
level: knows_details
# World knowledge prerequisite
prerequisite:
knowledge:
- flag: contraband.ring_exists
level: knows_of
# Multiple prerequisites (AND logic)
prerequisite:
knowledge:
- subject: kael_davan
flag: observed_out_of_place
level: knows_of
- subject: sera_venn
flag: observed_lying
level: suspects
v0.1 Starting Knowledge (Per Character)
Smuggler (Kael Davan's perspective):
kael_davan.identity: knows_name, knows_role (KnowsDetails, Background)kael_davan.relationship: trusted_colleague (KnowsDetails, Background)contraband.ring_exists: KnowsDetails (Background -- smuggler is a ring member)contraband.ring_member.kael: KnowsDetails (Background)contraband.ring_member.voss: KnowsOf (Background -- knows Voss runs things)lera_sessik.identity: knows_name, knows_role (KnowsOf, Background)torek_lintar.identity: knows_name, knows_role (Suspects, Background -- heard the name)
Detective (Sera Venn's perspective):
sera_venn.identity: knows_name, knows_role (KnowsDetails, Background)sera_venn.relationship: social_anchor (KnowsDetails, Background)system.commission_mandate: KnowsDetails (Background -- detective is Commission)location.sova_transit_layout: KnowsOf (Background -- has map data)torek_lintar.identity: knows_name, knows_role (KnowsOf, Background -- Commission colleague)lera_sessik.identity: knows_name (Suspects, Background -- heard the name)
This provides enough starting knowledge for the opening monologue to distinguish the two characters and gate initial investigation options.
Appendix B: Interaction with Existing Decisions
| Decision | How Knowledge Graph Interacts |
|---|---|
| D-010 (Architectural baseline) | Principle 2: knowledge graph IS the information boundary system. Principle 4: BTreeMap + tick-based decay ensures determinism. |
| D-011 (Fog of perception) | "Fog returns when you leave" = knowledge decay system. Remembered entities = fog ghosts. |
| D-015 (Camera locked) | Vision cone modulates what enters the knowledge graph (forward = Direct, peripheral = reduced range). |
| D-017 (Perception modes) | Each mode is a different observer query. Knowledge graph is mode-agnostic -- it records what was perceived, not how. |
| D-018 (Three-range sound) | Close/Medium = DirectObservation or Heard source. Long = insert notification, treated as ToldBy with system trust. |
| D-020 (Engine architecture) | KnowledgeGraph is server-side only. Client receives projected data via ObserverSnapshot. No raw graph crosses the bridge. |
| D-024 (NPC 10 axes) | Information inventory axis = KnowledgeGraph component. Relationship axes feed RelationshipState. Tolerance affects gossip willingness. |
| D-026 (Simulation tiers) | KnowledgeGraph component attached at all tiers. Serializes with entity for tier transitions. Background tier: decay runs at reduced rate. State-saved: serialized to disk. |
| D-028 (Dialogue architecture) | Knowledge prerequisites gate access tiers. surface at any level, real at KnowsOf+, secret at KnowsDetails+ with trust. |
| D-030 (Testability) | CauseChain integration: knowledge source feeds provenance. Unit tests via #[cfg(test)] in knowledge module. Integration tests in tests/. |
| D-033 (Entity color) | RelationshipState -> color palette mapping. Color derived from knowledge graph, not objective NPC property. |
| D-034 (THE FRIEND) | Contradiction detection (KnowledgeState::Contradicted) is THE FRIEND arc detector. Color shift green->amber fires when relationship changes to PersonOfInterest. |
| D-035 (Tag taxonomy) | prerequisite tag format resolved: knowledge: [{subject, flag, level}]. Maps to fact_at_least() query. |
End of Round 2 Synthesis.
This document is the canonical reference for the knowledge graph data model. The Rust structs in Part 3 define D-041. The Q-016 resolution in Part 4 defines the confidence hierarchy. The Sprint 2 scope in Part 7 defines what ships. The ticket impact in Part 8 defines what SI needs to create and reconcile.
Scope-wise, this means: the knowledge graph is a ~6.5 developer-day Sprint 2 deliverable that provides the correct architectural foundation for every system that touches information boundaries. The full feature set (gossip, inference, contradiction detection, misinformation) lands incrementally over Sprints 3-4. Nothing in Sprint 2 needs to be rewritten when those features arrive.
That is actually elegant.
Files referenced:
/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/workshops/knowledge-graph-information-boundaries/workshop-brief.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-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/docs/workshops/knowledge-graph-information-boundaries/round1-tyre.md/var/home/jeroenschweitzer/Projects/settled-reach/planning/docs/workshops/knowledge-graph-information-boundaries/round1-paula.md/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-024, D-028, D-034, D-035)/var/home/jeroenschweitzer/Projects/settled-reach/planning/decisions/questions.md(Q-016, Q-019)