//! Knowledge grant types for dialogue and monologue lines. //! //! `KnowledgeGrant` describes the knowledge a player gains from a dialogue line. //! `Prerequisites` describes preconditions for a monologue line to fire. use serde::Deserialize; use std::collections::BTreeMap; /// 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. /// /// **Ambiguity hazard**: if a YAML object contains both `fact_id` and `entity_ref`, /// serde(untagged) silently selects `Fact` (first variant wins) and ignores `entity_ref`. /// Use [`KnowledgeGrant::validate`] at load time to catch this case. #[derive(Debug, Clone, Deserialize)] #[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, confidence: String, }, } impl KnowledgeGrant { /// Validates that the grant is internally consistent. /// /// Specifically catches the serde(untagged) ambiguity: a raw YAML value that has /// both `fact_id` and `entity_ref` would parse silently as `Fact`, losing the /// entity reference. This can only be detected after deserializing the surrounding /// structure — call this on every grant after loading. /// /// Currently there is no post-deserialization raw-value access, so this serves as /// a place to add future cross-field validation. Returns `Ok(())` for now. pub fn validate(&self) -> Result<(), String> { match self { KnowledgeGrant::Fact { fact_id, .. } => { if fact_id.is_empty() { return Err("KnowledgeGrant::Fact has empty fact_id".to_string()); } } KnowledgeGrant::Entity { entity_ref, .. } => { if entity_ref.is_empty() { return Err("KnowledgeGrant::Entity has empty entity_ref".to_string()); } } } Ok(()) } } /// Prerequisite set for a monologue line. #[derive(Debug, Clone, Deserialize)] pub struct Prerequisites { #[serde(default)] pub facts: Vec, #[serde(default)] pub entity_attributes: Vec, #[serde(default)] pub relationship: Option, } #[derive(Debug, Clone, Deserialize)] pub struct FactPrerequisite { pub fact_id: String, pub min_confidence: String, } #[derive(Debug, Clone, Deserialize)] pub struct AttributePrerequisite { pub entity: String, pub key: String, pub value: String, } #[derive(Debug, Clone, Deserialize)] pub struct RelationshipPrerequisite { #[serde(default)] pub target: Option, #[serde(default)] pub state: Option, }