Files
settled-reach/docs/architecture/interaction-memory-schema.md
T
jpmschweitzerandClaude Opus 4.6 3100190b40 docs(docs): add frontmatter to architecture docs
Standardized YAML frontmatter on all 10 docs/architecture/ files with
title, description, type, status, ticket, decision_refs, and author
fields. Enables context-aware document loading.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:22:03 +01:00

673 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "InteractionMemory Knowledge Graph Schema Design"
description: "Schema extension for tracking player-NPC conversation history in the knowledge graph — walk-away consequences, confrontation tracking, and monologue prerequisite integration"
type: architecture
status: active
ticket: "#442"
decision_refs: [D-041, D-064, D-063, D-028]
author: ""
created: YYYY-MM-DD
updated: YYYY-MM-DD
---
# InteractionMemory Knowledge Graph Schema Design
**Ticket:** #442 | **Sprint:** 7 | **Priority:** MEDIUM
**Decisions:** D-041 (KG data model), D-064 (walk-away consequences), D-063 (confrontation), D-028 (dialogue architecture)
**Audience:** Engine developers (Dudley), narrative designers (Paula, Mellanie), QA (Hoshe)
---
## 1. Purpose
The InteractionMemory schema extends the existing `FactKnowledge` system (D-041) to track conversation history between the player character and NPCs. This enables:
- **Walk-away consequences** (D-064): The KG records that an interaction was started but not finished, affecting future dialogue and NPC behavior.
- **Confrontation tracking** (D-063): The KG records whether an interaction escalated to confrontation.
- **Dialogue history queries**: "Have I talked to X before?", "Did I walk away from X?", "When did I last complete a conversation with X?"
- **Monologue triggers**: Post-interaction reflection that references prior conversation outcomes.
This is a schema design document. It defines data structures and query patterns — implementation follows in a future ticket.
---
## 2. Design Constraints
From D-041:
- All maps are `BTreeMap` (deterministic iteration, non-negotiable per D-010 principle 4)
- Facts live in `KnowledgeGraph.facts: BTreeMap<FactId, FactKnowledge>`
- `FactId` format is `"category.topic"` (string)
- `FactKnowledge` has `confidence`, `source`, `state`, `acquired_tick`
From D-064:
- Walk-away triggers three phases: immediate break, NPC reaction, KG recording
- KG must log that the interaction was initiated but not completed
- This is queryable and affects future dialogue, monologue, and NPC behavior
- NPC tolerance thresholds vary per seed (no universal rules to metagame)
From the existing codebase (`server/src/knowledge/types.rs`):
- `KnowledgeConfidence`: Suspects < KnowsOf < KnowsDetails < Direct
- `KnowledgeState`: Active, Contradicted, Stale
- `KnowledgeSource`: DirectObservation, Heard, ToldBy, Inferred, Background
- `RelationshipState`: Unknown, Known, Friendly, PersonOfInterest, Hostile
---
## 3. Schema Extension
### 3.1 Interaction FactId format
Interaction facts use a dedicated `interaction` category in the FactId namespace:
```
interaction.{entity_slug}_{sequence}
```
| Segment | Format | Description |
|---------|--------|-------------|
| `interaction` | literal | Category prefix — distinguishes from other fact categories |
| `entity_slug` | kebab-case | NPC slug matching their content file (e.g., `kael-davan`, `lera-sessik`) |
| `_sequence` | `_NNN` | Zero-padded sequence number for multiple interactions with the same NPC |
**Examples:**
- `interaction.kael-davan_001` — first tracked interaction with Kael
- `interaction.kael-davan_002` — second tracked interaction with Kael
- `interaction.sera-venn_001` — first tracked interaction with Sera
**Why sequence numbers, not timestamps?** FactIds are string keys in a BTreeMap. Sequence numbers are shorter, sortable, and don't tie the ID format to the tick system. The timestamp is stored in the metadata (see Section 3.3).
#### Entity slug resolution at runtime
The `{entity_slug}` segment in the FactId (e.g., `kael-davan`) must be resolvable from an `Entity` (bevy ECS handle) during gameplay. Currently, no component maps Entity → content slug at runtime.
**Proposed approach:** Add a `ContentSlug` component to the NPC entity bundle, populated from the NPC profile filename during spawn:
```rust
/// Stable content identifier for an entity, derived from its YAML profile filename.
/// e.g., NPC profile `kael-davan.yaml` → ContentSlug("kael-davan").
/// Used to construct FactIds like `interaction.kael-davan_001`.
#[derive(Component, Debug, Clone)]
pub struct ContentSlug(pub String);
```
The interaction event handler resolves Entity → ContentSlug → FactId:
```rust
// In InteractionStarted event processing:
let slug = world.get::<ContentSlug>(target)
.expect("NPC must have ContentSlug component");
let seq = next_interaction_sequence(&kg.facts, &slug.0);
let fact_id = FactId(format!("interaction.{}_{:03}", slug.0, seq));
```
**Implementation note:** The `ContentSlug` component is a prerequisite for this schema. It should be added as part of the NPC spawn pipeline (#423 or a follow-up ticket) before interaction tracking is implemented. Until `ContentSlug` exists, the interaction system cannot construct FactIds.
### 3.2 InteractionState enum
New enum representing the outcome of an interaction:
```rust
/// Outcome state of a tracked interaction.
/// Drives walk-away mechanics (D-064) and confrontation tracking (D-063).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum InteractionState {
/// Conversation started, still in progress.
/// Set when dialogue box opens. Cleared on transition to another state.
Active,
/// Conversation completed normally (both parties finished talking).
/// The default "good" outcome.
Completed,
/// Player walked away mid-conversation (D-064 Phase 3).
/// The KG records incompleteness. NPC tolerance threshold determines
/// severity of consequence.
WalkedAway,
/// Conversation escalated to confrontation (D-063).
/// Player made an accusation or NPC became hostile during dialogue.
/// Separate from WalkedAway — a confrontation can be completed.
Confrontation,
/// Confrontation was initiated but player walked away before resolution.
/// The most consequential outcome — walking away from a confrontation
/// you started is worse than walking away from smalltalk.
ConflictAbandoned,
}
```
**State transitions:**
```
┌─────────────┐
│ Active │ ← dialogue opens
└──────┬──────┘
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌──────────┐ ┌──────────────┐
│ Completed │ │ WalkedAway│ │Confrontation │
└───────────┘ └──────────┘ └──────┬───────┘
┌─────────┼──────────┐
│ │
▼ ▼
┌───────────┐ ┌──────────────────┐
│ Completed │ │ConflictAbandoned │
└───────────┘ └──────────────────┘
```
All transitions are one-way. Once an interaction reaches a terminal state (Completed, WalkedAway, ConflictAbandoned), it cannot change.
### 3.3 InteractionMetadata
New metadata struct attached to interaction facts. This extends the base `FactKnowledge` with interaction-specific data.
```rust
/// Metadata for an interaction fact.
/// Stored alongside the FactKnowledge in the knowledge graph.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InteractionMetadata {
/// The NPC this interaction was with.
pub target_entity: StableId,
/// Interaction outcome.
pub interaction_state: InteractionState,
/// Tick when dialogue was initiated.
pub started_tick: u64,
/// Tick when interaction ended (completed, walked away, etc.).
/// None if interaction is still Active.
pub ended_tick: Option<u64>,
/// Primary conversation topic(s) discussed.
/// References topic enum values from content/global/enums/topics.yaml.
/// Empty if no specific topic was identified.
pub topics: Vec<String>,
/// Whether a confrontation occurred during this interaction.
/// True if InteractionState is Confrontation or ConflictAbandoned.
pub confrontation: bool,
/// Whether the player initiated the conversation (approached NPC)
/// or the NPC initiated (unprompted disclosure, D-028 Layer 4).
pub player_initiated: bool,
/// Location slug where the interaction took place.
pub location: String,
}
```
### 3.4 Storage strategy
Interaction metadata is stored in a new field on `FactKnowledge`. There are two viable approaches:
**Option A: Typed metadata field (recommended)**
Add an optional metadata enum to `FactKnowledge`:
```rust
/// Optional typed metadata attached to a fact.
/// Extensible for future fact types beyond interactions.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FactMetadata {
Interaction(InteractionMetadata),
// Future: Evidence(EvidenceMetadata), Gossip(GossipMetadata), etc.
}
/// Updated FactKnowledge with optional metadata.
pub struct FactKnowledge {
pub confidence: KnowledgeConfidence,
pub source: KnowledgeSource,
pub state: KnowledgeState,
pub acquired_tick: u64,
pub metadata: Option<FactMetadata>, // NEW
}
```
**Why Option A over alternatives:**
- No separate data structure — interactions are facts, stored in the existing `BTreeMap<FactId, FactKnowledge>`
- The `FactMetadata` enum is extensible for future fact types (evidence, gossip) without changing the graph structure
- `Option<FactMetadata>` adds zero overhead to facts that don't need metadata (the vast majority)
- Queries use existing `KnowledgeGraph.facts` infrastructure — no new maps to maintain
**Option B (rejected): Separate InteractionLog map**
A separate `BTreeMap<InteractionId, InteractionRecord>` on KnowledgeGraph. Rejected because it fragments the knowledge model — interactions ARE facts about the world, and should be queryable through the same API as other facts. Separate storage also complicates serialization and tier transitions (D-026).
---
## 4. Confidence Mapping
Interaction facts use the existing 4-level confidence hierarchy, but the levels have interaction-specific semantics:
| Confidence | Interaction meaning | When set |
|------------|-------------------|----------|
| `Suspects` | Not used for interactions | — |
| `KnowsOf` | "I talked to X" | Set when dialogue starts |
| `KnowsDetails` | "I talked to X about Y, and it ended with Z" | Set when dialogue ends with meaningful outcome |
| `Direct` | "I am talking to X right now" | Set during active dialogue, cleared on end |
Interaction facts start at `Direct` (active conversation) and settle to `KnowsDetails` when the conversation ends. They do not decay below `KnowsDetails` — you don't forget that you had a conversation.
#### Fact decay exemption status
The current `decay_knowledge` system (`server/src/knowledge/events.rs:106`) only iterates `KnowledgeGraph.entities` (EntityKnowledge), **not** `KnowledgeGraph.facts` (FactKnowledge). This means interaction facts stored in `self.facts` are **inherently immune to decay today** — no code path touches them.
However, this is an implementation coincidence, not an explicit guarantee. When a future fact decay system is built (to handle stale evidence, fading gossip, etc.), it **must** respect `DECAY_FLOOR` for interaction facts. Interaction facts should never decay below `KnowsDetails` — you remember that you had a conversation, even if other details fade.
**Requirement for future fact decay implementation:** Check `FactMetadata::Interaction` and clamp confidence at `DECAY_FLOOR`:
```rust
impl InteractionMetadata {
/// Interaction facts do not decay below KnowsDetails.
/// You remember that you had a conversation, even if details fade.
pub const DECAY_FLOOR: KnowledgeConfidence = KnowledgeConfidence::KnowsDetails;
}
// In future fact decay system:
for (id, fact) in kg.facts.iter_mut() {
if let Some(FactMetadata::Interaction(_)) = &fact.metadata {
// Interaction facts: clamp at DECAY_FLOOR
if fact.confidence.decayed() < InteractionMetadata::DECAY_FLOOR {
continue; // Skip — would drop below floor
}
}
// ... normal decay logic ...
}
```
---
## 5. Query Patterns
### 5.1 Core queries
These are the queries the dialogue system, monologue system, and NPC behavior need:
```rust
impl KnowledgeGraph {
/// Has the player ever interacted with this entity?
fn has_interacted_with(&self, target: StableId) -> bool;
/// Get the most recent interaction with an entity.
fn last_interaction_with(&self, target: StableId) -> Option<&InteractionMetadata>;
/// Get all interactions with an entity, ordered by started_tick.
fn interactions_with(&self, target: StableId) -> Vec<&InteractionMetadata>;
/// Has the player walked away from this entity?
fn has_walked_away_from(&self, target: StableId) -> bool;
/// Count of interactions with an entity.
fn interaction_count_with(&self, target: StableId) -> usize;
/// Has the player ever had a confrontation with this entity?
fn has_confronted(&self, target: StableId) -> bool;
/// Is there currently an active (in-progress) interaction?
fn active_interaction(&self) -> Option<&InteractionMetadata>;
}
```
### 5.2 Implementation sketch
All queries scan `self.facts` for entries with `interaction.{slug}` prefix and `FactMetadata::Interaction` metadata. Since BTreeMap iterates in lexicographic order, all interactions with the same entity are contiguous (they share the `interaction.{slug}` prefix).
```rust
/// Iterate all interaction facts for a given entity.
/// Uses BTreeMap range query for O(log N + K) where K = number of interactions.
fn interaction_facts_for<'a>(
&'a self,
entity_slug: &str,
) -> impl Iterator<Item = (&'a FactId, &'a InteractionMetadata)> {
let prefix = format!("interaction.{entity_slug}_");
self.facts
.range(FactId(prefix.clone())..)
.take_while(move |(id, _)| id.0.starts_with(&prefix))
.filter_map(|(id, fact)| {
match &fact.metadata {
Some(FactMetadata::Interaction(meta)) => Some((id, meta)),
_ => None,
}
})
}
```
**Performance:** At expected scale (10-30 interactions per playthrough), this is negligible. BTreeMap range queries are O(log N) to find the start, then O(K) to iterate matches. With N=50 total facts and K=5 interactions per NPC, this is ~6-8 comparisons + 5 iterations.
### 5.3 Dialogue system queries
The dialogue selection pipeline (D-028) needs interaction history for Layer 2 (relationship history):
| Query | Use case | Implementation |
|-------|----------|----------------|
| "First time talking to X?" | Greeting selection — fresh vs. returning | `interaction_count_with(target) == 0` |
| "Did I walk away last time?" | NPC reacts differently, may reference it | `last_interaction_with(target).state == WalkedAway` |
| "How many times have we talked?" | Familiarity progression | `interaction_count_with(target)` |
| "Was there a confrontation?" | Permanently shifts NPC posture | `has_confronted(target)` |
| "Active conversation right now?" | Block new dialogue initiation | `active_interaction().is_some()` |
### 5.4 Monologue system queries
The monologue system (D-032, D-035) uses interaction facts as prerequisites:
```yaml
# Monologue line that fires after walking away from Kael
- id: the-last-shift_m_s_050
text: "Shouldn't have left like that. Kael noticed."
trigger: post_conversation
prerequisites:
facts:
- fact_id: interaction.kael-davan_001
min_confidence: knows_details
tags: [kael, walk-away, regret]
```
For more precise gating on interaction outcome, the monologue system needs to check the `InteractionMetadata.interaction_state` field. This extends the prerequisite system:
```yaml
# More specific: only fires if the player walked away
prerequisites:
facts:
- fact_id: interaction.kael-davan_001
min_confidence: knows_details
interaction: # NEW prerequisite type
target: npc:kael-davan
state: walked_away # matches InteractionState::WalkedAway
```
This extends the monologue prerequisite schema (Section 6).
---
## 6. Monologue Prerequisite Extension
The existing `monologue-pool.schema.json` supports `facts`, `entity_attributes`, and `relationship` prerequisites. Interaction memory adds a new prerequisite type:
```json
"interaction_prerequisite": {
"type": "object",
"required": ["target"],
"additionalProperties": false,
"properties": {
"target": {
"type": "string",
"description": "Entity reference (e.g., npc:kael-davan)"
},
"state": {
"type": "string",
"enum": ["active", "completed", "walked_away", "confrontation", "conflict_abandoned"],
"description": "Required interaction outcome. If omitted, any interaction satisfies."
},
"min_count": {
"type": "integer",
"minimum": 1,
"default": 1,
"description": "Minimum number of interactions matching the criteria"
}
}
}
```
Added to the `prerequisites` object in the monologue schema:
```json
"prerequisites": {
"type": "object",
"properties": {
"facts": { ... },
"entity_attributes": { ... },
"relationship": { ... },
"interaction": { "$ref": "#/$defs/interaction_prerequisite" }
}
}
```
**Examples:**
```yaml
# Fires after any interaction with Kael (first meeting)
prerequisites:
interaction:
target: npc:kael-davan
# Fires only if player walked away from Kael
prerequisites:
interaction:
target: npc:kael-davan
state: walked_away
# Fires after 3+ completed conversations with Lera
prerequisites:
interaction:
target: npc:lera-sessik
state: completed
min_count: 3
# Combined: walked away from Kael AND knows about the ring
prerequisites:
facts:
- fact_id: contraband.ring_exists
min_confidence: knows_of
interaction:
target: npc:kael-davan
state: walked_away
```
---
## 7. Event Integration
### 7.1 New KnowledgeEventType variants
Extend `KnowledgeEventType` in `server/src/knowledge/events.rs`:
```rust
pub enum KnowledgeEventType {
// ... existing variants ...
/// Dialogue initiated with an NPC.
InteractionStarted {
target: Entity,
player_initiated: bool,
location: String,
},
/// Dialogue completed normally.
InteractionCompleted {
target: Entity,
topics: Vec<String>,
},
/// Player walked away mid-conversation (D-064).
InteractionWalkedAway {
target: Entity,
},
/// Conversation escalated to confrontation (D-063).
InteractionConfrontation {
target: Entity,
},
/// Player walked away from a confrontation they started.
InteractionConflictAbandoned {
target: Entity,
},
}
```
### 7.2 Event flow
```
Player approaches NPC → dialogue system opens
├─ Emit InteractionStarted
│ → KG creates interaction.{slug}_{seq} fact with InteractionState::Active
│ → FactKnowledge.confidence = Direct (in conversation now)
├─ ... conversation proceeds ...
└─ Conversation ends (one of):
├─ Normal end → Emit InteractionCompleted
│ → KG updates fact: Active → Completed, confidence → KnowsDetails
│ → Record topics discussed
├─ Player walks away (WASD) → Emit InteractionWalkedAway
│ → KG updates fact: Active → WalkedAway, confidence → KnowsDetails
│ → D-064 Phase 3: KG records incompleteness
├─ Confrontation escalation → Emit InteractionConfrontation
│ → KG updates fact: Active → Confrontation
│ → Conversation continues (may still complete or be abandoned)
└─ Walk away from confrontation → Emit InteractionConflictAbandoned
→ KG updates fact: Confrontation → ConflictAbandoned
→ Most severe consequence path
```
### 7.3 Sequence number allocation
The `process_knowledge_events` system allocates sequence numbers when processing `InteractionStarted` events:
```rust
/// Determine next sequence number for an interaction FactId.
/// Scans existing facts for the highest sequence number with this prefix.
fn next_interaction_sequence(
facts: &BTreeMap<FactId, FactKnowledge>,
entity_slug: &str,
) -> u32 {
let prefix = format!("interaction.{entity_slug}_");
facts
.range(FactId(prefix.clone())..)
.take_while(|(id, _)| id.0.starts_with(&prefix))
.count() as u32
+ 1
}
```
This is O(log N + K) — acceptable for the expected interaction count.
---
## 8. NPC Tolerance System
Per D-064, walk-away consequences vary by NPC tolerance threshold per seed. This is orthogonal to the KG schema — it lives on the NPC entity, not in the player's knowledge graph.
### 8.1 Relationship to existing tolerance types
The codebase already has two tolerance-related types:
| Type | Location | Purpose |
|------|----------|---------|
| `ToleranceThreshold` | `server/src/npc/mod.rs:122` | **ECS Component.** General NPC stress tracker with `current_stress: i16` and `threshold: i16`. Drives overall NPC frustration behavior (D-024 Axis 4). |
| `NpcTolerance` | `server/src/content/types.rs:302` | **Content definition.** Deserialization struct from NPC YAML profiles with `threshold: Option<i32>`. Populates `ToleranceThreshold` at spawn. |
The proposed `InteractionTolerance` is **not a replacement** for either. It tracks walk-awayspecific consequence parameters that `ToleranceThreshold` does not cover (penalty magnitude, forgiveness policy). The relationship:
- `NpcTolerance` (content YAML) → populates `ToleranceThreshold` (ECS runtime)
- `ToleranceThreshold` tracks **general stress accumulation** — any irritant contributes
- `InteractionTolerance` tracks **walk-awayspecific consequence parameters** — how the NPC reacts to conversational abandonment specifically
**Recommended implementation approach:** Rather than adding a third standalone component, extend the existing `ToleranceThreshold` or co-locate walk-away fields on a new `InteractionTolerance` component that **reads** `ToleranceThreshold.current_stress` as a factor. Walk-away consequences should feed back into `ToleranceThreshold.current_stress` (walking away from a stressed NPC is worse). The walk-away system would:
1. Check `InteractionTolerance.walk_away_threshold` — has the NPC been walked away from too many times?
2. If threshold exceeded, apply `walk_away_penalty` to relationship
3. Increase `ToleranceThreshold.current_stress` by a walk-awayspecific delta
4. Check `permanent_confrontation_memory` for confrontation abandonment
```rust
/// Walk-awayspecific consequence parameters. Extends general tolerance
/// (ToleranceThreshold) with interaction-specific mechanics per D-064.
/// Populated from NPC profile YAML, seeded per playthrough.
/// Reads ToleranceThreshold.current_stress as an aggravating factor.
#[derive(Component, Debug, Clone)]
pub struct InteractionTolerance {
/// How many walk-aways before relationship degradation.
/// Range: 1-5, seeded per NPC per playthrough.
pub walk_away_threshold: u8,
/// How much relationship damage per walk-away beyond threshold.
/// Range: 0.1-0.5, seeded per NPC.
pub walk_away_penalty: f32,
/// Stress delta applied to ToleranceThreshold.current_stress per walk-away.
/// A walk-away from a stressed NPC (high current_stress) hits harder.
pub walk_away_stress_delta: i16,
/// Whether this NPC remembers confrontation abandonment permanently.
/// Some NPCs forgive, some don't (per seed).
pub permanent_confrontation_memory: bool,
}
```
The NPC behavior system queries both the player's KG (for interaction history) and the NPC's tolerance components (for consequence magnitude). This separation means:
- The player's KG is what THEY remember
- `ToleranceThreshold` is the NPC's general frustration level
- `InteractionTolerance` is how the NPC reacts to walk-aways specifically (feeding back into general stress)
- The NPC may also have their own KG entry for the interaction (future: NPC-to-NPC gossip about "the person who walked out on Kael")
---
## 9. Scope and Limitations
### 9.1 v0.1 scope (this ticket)
- Player character interaction tracking only
- Interactions with Active-tier NPCs (30-80 per D-026)
- Single `interaction` prerequisite per monologue line
- No NPC-to-NPC interaction tracking
- No gossip about interactions ("I heard you walked out on Kael")
### 9.2 Future extensions
| Feature | Ticket/Sprint | Schema impact |
|---------|--------------|---------------|
| NPC-to-NPC interaction tracking | Future | Same schema, applied to NPC KnowledgeGraphs |
| Gossip about interactions | Sprint 3+ (ToldBy source) | New `KnowledgeSource::ToldBy` entries referencing interaction facts |
| Interaction-driven relationship decay | Future | Query interaction history to modulate decay rates |
| Multi-NPC conversations | Future | `InteractionMetadata.target_entity` becomes `Vec<StableId>` |
### 9.3 Memory budget
Per-interaction memory cost: ~200 bytes (InteractionMetadata + FactKnowledge overhead).
Expected interactions per playthrough: 50-150 (2-5 per Active NPC).
Total interaction memory: ~10-30 KB per player character. Negligible against the D-041 budget of ~6 MB for the full knowledge graph.
---
## 10. Schema Summary
| Component | Location | Type | Description |
|-----------|----------|------|-------------|
| `InteractionState` | `server/src/knowledge/types.rs` | enum | 5 outcome states |
| `InteractionMetadata` | `server/src/knowledge/types.rs` | struct | Per-interaction data |
| `FactMetadata` | `server/src/knowledge/types.rs` | enum | Extensible typed metadata on FactKnowledge |
| `FactKnowledge.metadata` | `server/src/knowledge/types.rs` | `Option<FactMetadata>` | New field on existing struct |
| `ContentSlug` | `server/src/npc/` (proposed) | Component | Entity → content slug mapping for FactId construction |
| `InteractionTolerance` | NPC template system | Component | Walk-away consequence parameters (extends ToleranceThreshold) |
| `interaction_prerequisite` | `content/_schema/monologue-pool.schema.json` | schema | New prerequisite type |
| `KnowledgeEventType` variants | `server/src/knowledge/events.rs` | enum variants | 5 new event types |
| Interaction FactId format | Convention | `interaction.{slug}_{NNN}` | Fact category for interactions |
### Breaking changes
One field addition to an existing struct:
```diff
pub struct FactKnowledge {
pub confidence: KnowledgeConfidence,
pub source: KnowledgeSource,
pub state: KnowledgeState,
pub acquired_tick: u64,
+ pub metadata: Option<FactMetadata>,
}
```
This is an additive change. Existing facts have `metadata: None`. All existing queries continue to work unchanged. The only code change required beyond adding the field is to add `metadata: None` to existing `FactKnowledge` construction sites.