feat(simulation): knowledge grant schema, events, and contradiction detection (#545, #546, #547)

KnowledgeGrant untagged enum (Fact + Entity variants), ContentEntityRegistry
resource, KnowledgeGranted event processing, ContradictionClaim struct with
600-tick window detection in observe_entity. Wires knowledge_grant field in
dialogue line selection. Implements D-079, D-083. Closes Q-026.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 12:14:53 +01:00
co-authored by Claude Opus 4.6
parent 381526ff74
commit 3d636fd4bb
13 changed files with 1121 additions and 12 deletions
+6
View File
@@ -20,6 +20,8 @@ use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use std::path::PathBuf;
use crate::knowledge::ContentEntityRegistry;
/// Configuration for the content loader.
/// Set the content root path before adding ContentPlugin.
#[derive(Resource, Debug, Clone)]
@@ -52,6 +54,10 @@ impl Plugin for ContentPlugin {
app.insert_resource(ContentConfig::default());
}
// ContentEntityRegistry is required by spawn_npc (D-079).
// Init here so ContentPlugin works standalone without KnowledgePlugin.
app.init_resource::<ContentEntityRegistry>();
app.add_systems(Startup, load_and_spawn_content);
app.add_systems(PostUpdate, hot_reload::hot_reload_content);
+9
View File
@@ -22,6 +22,7 @@ use std::collections::BTreeMap;
use crate::content::loader::{ContentStore, DistrictContent};
use crate::content::types;
use crate::knowledge::content_registry::ContentEntityRegistry;
use crate::knowledge::graph::KnowledgeGraph;
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
use crate::knowledge::types::{
@@ -213,6 +214,12 @@ fn spawn_npc(world: &mut World, profile: &types::NpcProfile, result: &mut SpawnR
ContentSlug(profile.canonical_id.clone()),
));
// Register in ContentEntityRegistry so KnowledgeGrant::Entity can resolve entity_ref strings
// (D-079: ContentEntityRegistry populated at NPC spawn time)
world
.resource_mut::<ContentEntityRegistry>()
.register(profile.canonical_id.clone(), stable_id);
result
.npc_ids
.insert(profile.canonical_id.clone(), stable_id);
@@ -296,6 +303,7 @@ fn resolve_information(
source: KnowledgeSource::Background,
state: KnowledgeState::Active,
acquired_tick: 0,
disclosure_blocked: false,
},
)
})
@@ -648,6 +656,7 @@ mod tests {
fn create_test_world() -> World {
let mut world = World::new();
world.init_resource::<EntityRegistry>();
world.init_resource::<ContentEntityRegistry>();
world
}
+23 -3
View File
@@ -491,10 +491,30 @@ pub struct DialogueLine {
pub knowledge_grant: Option<KnowledgeGrant>,
}
/// Knowledge grant attached to a dialogue line (D-079).
///
/// Untagged enum — serde tries each variant in order:
/// `Fact` matches YAML with `fact_id` field.
/// `Entity` matches YAML with `entity_ref` field.
/// `Compound` variant deferred to Sprint 18.
#[derive(Debug, Clone, Deserialize)]
pub struct KnowledgeGrant {
pub fact_id: String,
pub confidence: String,
#[serde(untagged)]
pub enum KnowledgeGrant {
/// Grant knowledge of a non-entity fact.
/// Format: fact_id "category.topic", confidence string.
Fact {
fact_id: String,
confidence: String,
},
/// Grant knowledge of an entity (creates EntityKnowledge entry in observer's KG).
/// Required for contradiction detection: testimony must create ToldBy EntityKnowledge
/// so a subsequent DirectObservation can detect a discrepancy (D-079, D-083).
Entity {
entity_ref: String,
#[serde(default)]
attributes: BTreeMap<String, String>,
confidence: String,
},
}
// ---------------------------------------------------------------------------