Skip to main content

weavatrix_memory/projection/memory/
mod.rs

1mod apply;
2pub(crate) mod index;
3mod parts;
4pub(crate) mod state;
5
6use crate::{
7    domain::{Evidence, MemoryFact, MemoryNode, MemoryView, MemoryViewRef},
8    error::{MemoryError, Result},
9    id::{EntityId, FactId},
10    time::Timestamp,
11};
12use serde::{Deserialize, Serialize};
13use state::{NodeHistory, NodeRevision, Retraction, Supersession};
14use std::collections::HashSet;
15
16pub use state::MemoryProjection;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub struct ProjectionClock {
20    pub valid_at: Timestamp,
21    pub known_at: Timestamp,
22}
23
24impl ProjectionClock {
25    #[must_use]
26    pub const fn new(valid_at: Timestamp, known_at: Timestamp) -> Self {
27        Self { valid_at, known_at }
28    }
29}
30
31impl MemoryProjection {
32    #[must_use]
33    pub const fn last_global_position(&self) -> Option<u64> {
34        self.last_global_position
35    }
36
37    #[must_use]
38    pub fn superseded_by(&self, fact: &FactId) -> Option<&FactId> {
39        self.supersessions
40            .get(fact)
41            .map(|change| &change.replacement)
42    }
43
44    pub(crate) fn visible_node(&self, id: &EntityId, known_at: Timestamp) -> Option<&MemoryNode> {
45        self.node_lookup
46            .get(id)
47            .and_then(|index| visible_revision(&self.nodes[*index], known_at))
48    }
49
50    pub(crate) fn fact(&self, id: &FactId) -> Option<&MemoryFact> {
51        self.fact_lookup.get(id).map(|index| &self.facts[*index])
52    }
53
54    pub(crate) fn all_facts(&self) -> &[MemoryFact] {
55        &self.facts
56    }
57
58    pub(crate) fn incident_fact_ids(&self, id: &EntityId) -> impl Iterator<Item = &FactId> + '_ {
59        self.node_lookup
60            .get(id)
61            .into_iter()
62            .flat_map(|index| {
63                let stable = &self.incident_facts
64                    [self.incident_offsets[*index]..self.incident_offsets[*index + 1]];
65                stable
66                    .iter()
67                    .chain(self.incident_delta.get(index).into_iter().flatten())
68            })
69            .map(|index| &self.facts[*index].id)
70    }
71
72    #[must_use]
73    pub fn view(&self, clock: ProjectionClock) -> MemoryView {
74        self.view_ref(clock).into_owned()
75    }
76
77    /// Borrows a temporal view without cloning node, fact, or evidence payloads.
78    ///
79    /// This is the preferred boundary for lexical and vector index providers
80    /// that only need to read the current evidence projection.
81    #[must_use]
82    pub fn view_ref(&self, clock: ProjectionClock) -> MemoryViewRef<'_> {
83        let nodes = self
84            .nodes
85            .iter()
86            .filter_map(|revisions| visible_revision(revisions, clock.known_at))
87            .collect::<Vec<_>>();
88        let visible_ids = (nodes.len() != self.nodes.len()).then(|| {
89            nodes
90                .iter()
91                .map(|node| node.id.clone())
92                .collect::<HashSet<_>>()
93        });
94        let facts = self
95            .facts
96            .iter()
97            .filter(|fact| {
98                visible_ids.as_ref().is_none_or(|visible| {
99                    visible.contains(&fact.source) && visible.contains(&fact.target)
100                }) && self.fact_is_active(fact, clock)
101            })
102            .collect();
103        MemoryViewRef { nodes, facts }
104    }
105
106    pub(crate) fn fact_is_active(&self, fact: &MemoryFact, clock: ProjectionClock) -> bool {
107        if fact.recorded_at > clock.known_at || fact.valid_from > clock.valid_at {
108            return false;
109        }
110        if fact
111            .valid_until
112            .is_some_and(|until| clock.valid_at >= until)
113        {
114            return false;
115        }
116        if self.supersessions.get(&fact.id).is_some_and(|change| {
117            change.recorded_at <= clock.known_at && change.valid_from <= clock.valid_at
118        }) {
119            return false;
120        }
121        !self.retractions.get(&fact.id).is_some_and(|change| {
122            change.recorded_at <= clock.known_at && change.valid_until <= clock.valid_at
123        })
124    }
125
126    fn insert_node(&mut self, revision: NodeRevision) -> Result<()> {
127        revision.node.validate()?;
128        if let Some(index) = self.node_lookup.get(&revision.node.id).copied() {
129            self.nodes[index].later.push(revision);
130        } else {
131            let index = self.nodes.len();
132            self.node_lookup.insert(revision.node.id.clone(), index);
133            self.nodes.push(NodeHistory::new(revision));
134            let offset = *self.incident_offsets.last().unwrap_or(&0);
135            self.incident_offsets.push(offset);
136        }
137        Ok(())
138    }
139
140    fn insert_fact(&mut self, fact: MemoryFact) -> Result<()> {
141        fact.validate()?;
142        let source = self.require_entity(&fact.source)?;
143        let target = self.require_entity(&fact.target)?;
144        if self.fact_lookup.contains_key(&fact.id) {
145            return Err(MemoryError::ConflictingFact {
146                id: fact.id.to_string(),
147            });
148        }
149        if let Some(prior) = &fact.supersedes {
150            self.apply_supersession(prior, &fact)?;
151        }
152        let index = self.facts.len();
153        self.fact_lookup.insert(fact.id.clone(), index);
154        self.incident_delta.entry(source).or_default().push(index);
155        if target != source {
156            self.incident_delta.entry(target).or_default().push(index);
157        }
158        self.facts.push(fact);
159        Ok(())
160    }
161
162    fn apply_supersession(&mut self, prior: &FactId, fact: &MemoryFact) -> Result<()> {
163        if !self.fact_lookup.contains_key(prior) {
164            return Err(MemoryError::MissingFact {
165                id: prior.to_string(),
166            });
167        }
168        if self.supersessions.contains_key(prior) {
169            return Err(MemoryError::ConflictingFact {
170                id: prior.to_string(),
171            });
172        }
173        self.supersessions.insert(
174            prior.clone(),
175            Supersession {
176                replacement: fact.id.clone(),
177                valid_from: fact.valid_from,
178                recorded_at: fact.recorded_at,
179            },
180        );
181        Ok(())
182    }
183
184    fn require_entity(&self, id: &EntityId) -> Result<usize> {
185        self.node_lookup
186            .get(id)
187            .copied()
188            .ok_or_else(|| MemoryError::MissingEntity { id: id.to_string() })
189    }
190
191    fn apply_retraction(
192        &mut self,
193        recorded_at: Timestamp,
194        fact_id: &FactId,
195        valid_until: Timestamp,
196        evidence: &[Evidence],
197    ) -> Result<()> {
198        let fact = self.fact(fact_id).ok_or_else(|| MemoryError::MissingFact {
199            id: fact_id.to_string(),
200        })?;
201        if valid_until <= fact.valid_from {
202            return Err(MemoryError::InvalidValue {
203                field: "retraction.valid_until",
204                reason: "must be later than the fact valid_from",
205            });
206        }
207        if evidence.is_empty() {
208            return Err(MemoryError::InvalidValue {
209                field: "retraction.evidence",
210                reason: "at least one evidence item is required",
211            });
212        }
213        evidence.iter().try_for_each(Evidence::validate)?;
214        self.retractions.insert(
215            fact_id.clone(),
216            Retraction {
217                valid_until,
218                recorded_at,
219            },
220        );
221        Ok(())
222    }
223}
224
225fn visible_revision(history: &NodeHistory, known_at: Timestamp) -> Option<&MemoryNode> {
226    core::iter::once(&history.first)
227        .chain(&history.later)
228        .filter(|revision| revision.recorded_at <= known_at)
229        .max_by_key(|revision| (revision.recorded_at, revision.position))
230        .map(|revision| &revision.node)
231}