Skip to main content

weavatrix_memory/domain/
fact.rs

1use super::{Confidence, Evidence};
2use crate::{
3    error::{MemoryError, Result},
4    id::{AgentId, EntityId, FactId, SessionId},
5    time::Timestamp,
6};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct MemoryFact {
11    pub id: FactId,
12    pub source: EntityId,
13    pub relation: String,
14    pub target: EntityId,
15    pub valid_from: Timestamp,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub valid_until: Option<Timestamp>,
18    pub observed_at: Timestamp,
19    pub recorded_at: Timestamp,
20    pub agent_id: AgentId,
21    pub session_id: SessionId,
22    pub confidence: Confidence,
23    pub evidence: Vec<Evidence>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub supersedes: Option<FactId>,
26}
27
28impl MemoryFact {
29    /// Creates an evidence-carrying temporal relation.
30    ///
31    /// # Errors
32    ///
33    /// Rejects invalid relation text or missing evidence.
34    #[allow(clippy::too_many_arguments)]
35    pub fn new(
36        id: FactId,
37        source: EntityId,
38        relation: impl Into<String>,
39        target: EntityId,
40        valid_from: Timestamp,
41        recorded_at: Timestamp,
42        agent_id: AgentId,
43        session_id: SessionId,
44        evidence: Evidence,
45    ) -> Result<Self> {
46        let fact = Self {
47            id,
48            source,
49            relation: relation.into(),
50            target,
51            valid_from,
52            valid_until: None,
53            observed_at: recorded_at,
54            recorded_at,
55            agent_id,
56            session_id,
57            confidence: Confidence::CERTAIN,
58            evidence: vec![evidence],
59            supersedes: None,
60        };
61        fact.validate()?;
62        Ok(fact)
63    }
64
65    /// Sets the exclusive end of the fact's valid interval.
66    ///
67    /// # Errors
68    ///
69    /// Rejects an end that is not later than `valid_from`.
70    pub fn valid_until(mut self, value: Timestamp) -> Result<Self> {
71        self.valid_until = Some(value);
72        self.validate()?;
73        Ok(self)
74    }
75
76    #[must_use]
77    pub const fn observed_at(mut self, value: Timestamp) -> Self {
78        self.observed_at = value;
79        self
80    }
81
82    #[must_use]
83    pub const fn with_confidence(mut self, value: Confidence) -> Self {
84        self.confidence = value;
85        self
86    }
87
88    /// Links this fact to the historical fact it replaces.
89    ///
90    /// # Errors
91    ///
92    /// Rejects self-supersession.
93    pub fn supersedes(mut self, fact: FactId) -> Result<Self> {
94        if fact == self.id {
95            return Err(MemoryError::InvalidValue {
96                field: "fact.supersedes",
97                reason: "a fact cannot supersede itself",
98            });
99        }
100        self.supersedes = Some(fact);
101        Ok(self)
102    }
103
104    #[must_use]
105    pub fn with_evidence(mut self, evidence: Evidence) -> Self {
106        self.evidence.push(evidence);
107        self
108    }
109
110    pub(crate) fn validate(&self) -> Result<()> {
111        super::validate_text("fact.relation", &self.relation)?;
112        if self.observed_at > self.recorded_at {
113            return Err(MemoryError::InvalidValue {
114                field: "fact.observed_at",
115                reason: "must not be later than recorded_at",
116            });
117        }
118        if self
119            .valid_until
120            .is_some_and(|until| until <= self.valid_from)
121        {
122            return Err(MemoryError::InvalidValue {
123                field: "fact.valid_until",
124                reason: "must be later than valid_from",
125            });
126        }
127        if self.evidence.is_empty() {
128            return Err(MemoryError::InvalidValue {
129                field: "fact.evidence",
130                reason: "at least one evidence item is required",
131            });
132        }
133        self.evidence.iter().try_for_each(Evidence::validate)
134    }
135}