Files
settled-reach/docs/workshops/knowledge-graph-information-boundaries/round2-synthesis.md
T
jpmschweitzerandClaude Opus 4.6 93abd9b9d0 docs(workshops): complete Knowledge Graph & Information Boundaries workshop
Two-round workshop producing D-041 (Knowledge Graph Data Model):
- Round 1: independent analyses from Dudley, Gestalt, SI, Tyre, Paula
- Round 2: synthesis resolving debates + Gestalt mechanics validation

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:24:37 +01:00

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: surface available at any level, real at KnowsOf+, secret at 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 prerequisite tag: 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 HashMap in 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 u64 values. 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 the KnowledgeEventQueue resource (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_tick far 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 KnowledgeState field exists on every entry.
    • Sprint 2 only sets Active.
    • Sprint 3 adds contradiction detection: when a new observation conflicts with a ToldBy entry, both entries get marked Contradicted, a ContradictionEvent fires, and the monologue system picks it up.
  • 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:

  1. KnowledgeGraph component with BTreeMap<StableId, EntityKnowledge> and BTreeMap<FactId, FactKnowledge>.
  2. KnowledgeConfidence enum with all 4 levels (Suspects, KnowsOf, KnowsDetails, Direct).
  3. KnowledgeSource enum with DirectObservation and Background variants functional. Other variants (Heard, ToldBy, Inferred) exist in the enum but no system generates them.
  4. KnowledgeState enum with all 3 variants. Only Active is set by any system.
  5. StableEntityId component and EntityRegistry resource for bidirectional StableId <-> Entity mapping.
  6. observe_entity() method: perception system calls this when entity is in LOS. Sets confidence to Direct.
  7. observe_entity_leaving_los() method: perception system calls this when entity leaves LOS. Downgrades Direct to KnowsDetails (or KnowsOf if brief sighting).
  8. knows_entity(), confidence_of(), relationship_with(), knows_fact(), fact_at_least() query methods.
  9. Basic decay: once per game-minute (every 10 ticks per D-031), configurable via DecayThresholds resource. Only downgrades confidence levels based on tick age. Does not generate Stale state yet.
  10. Player character gets KnowledgeGraph populated from background data at game start.
  11. Active-tier NPCs get KnowledgeGraph with 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)
  • ToldBy and Inferred source generation
  • Contradicted state detection and event firing
  • Stale state from decay system
  • KnowledgeIndex reverse 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:

  1. Observe NPCs meeting and predict information flow
  2. Time their actions between intersection points to exploit windows of ignorance
  3. 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):

  1. NPC A and NPC B have a conversation (triggered by routine intersection, Q-024 preferred: queued approach).
  2. Gossip eligibility check: A's personality (gossip tendency from D-024 personality traits), trust in B, current mood, triangle pressure.
  3. 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).
  4. 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."

  1. Decay system runs once per game-minute (every 10 ticks per D-031).
  2. For each entity in the knowledge graph where confidence != Direct:
    • If age > stale_after (default 6 game-hours): set state = Stale.
    • Else if age > decay_after (default 1 game-hour): call confidence.decayed().
  3. Entries with state == Stale remain 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.
  4. 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 a ResMut.
  • 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:

  1. Input: #112 produces a list of geometrically visible entities (from shadowcasting + vision cone).
  2. Knowledge write: For each visible entity, #112 emits a KnowledgeEvent::DirectObservation. The knowledge update system (Phase 3) writes Direct confidence to the observer's KnowledgeGraph.
  3. Knowledge read: #112 reads the observer's KnowledgeGraph to:
    • Get RelationshipState for each visible entity (D-033 entity color).
    • Get remembered_entities -- entities in the knowledge graph but NOT currently visible (fog ghosts).
  4. Output: VisibilityResult containing 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)