Skip to main content

cerememory_index/
lib.rs

1//! Hippocampal Coordinator — cross-store record registry and global association graph.
2//!
3//! Maps record_id → StoreType and maintains a global association index
4//! that spans all stores. Phase 1 keeps everything in-memory, rebuilt
5//! from stores on startup.
6//!
7//! Phase 2 adds:
8//! - [`text_index::TextIndex`] — Tantivy full-text search across all stores
9//! - [`vector_index::VectorIndex`] — Brute-force cosine similarity search
10
11pub mod hnsw_index;
12pub mod structured_index;
13pub mod text_index;
14pub mod vector_index;
15
16use cerememory_core::error::CerememoryError;
17use cerememory_core::traits::AssociationGraph;
18use cerememory_core::types::*;
19use std::collections::HashMap;
20use tokio::sync::RwLock;
21use uuid::Uuid;
22
23/// Entry in the coordinator's registry.
24#[derive(Debug, Clone)]
25pub struct RegistryEntry {
26    pub store_type: StoreType,
27    pub associations: Vec<Association>,
28}
29
30/// Hippocampal Coordinator: knows where every record lives
31/// and maintains the global association graph.
32pub struct HippocampalCoordinator {
33    registry: RwLock<HashMap<Uuid, RegistryEntry>>,
34}
35
36impl HippocampalCoordinator {
37    pub fn new() -> Self {
38        Self {
39            registry: RwLock::new(HashMap::new()),
40        }
41    }
42
43    /// Register a record in the coordinator.
44    pub async fn register(
45        &self,
46        record_id: Uuid,
47        store_type: StoreType,
48        associations: Vec<Association>,
49    ) {
50        self.registry.write().await.insert(
51            record_id,
52            RegistryEntry {
53                store_type,
54                associations,
55            },
56        );
57    }
58
59    /// Remove a record from the coordinator and clean up inbound associations.
60    pub async fn unregister(&self, record_id: &Uuid) -> bool {
61        let mut reg = self.registry.write().await;
62        let existed = reg.remove(record_id).is_some();
63        // Remove inbound associations from all other records pointing to this one
64        if existed {
65            for entry in reg.values_mut() {
66                entry.associations.retain(|a| a.target_id != *record_id);
67            }
68        }
69        existed
70    }
71
72    /// Update associations for a record.
73    pub async fn update_associations(
74        &self,
75        record_id: &Uuid,
76        associations: Vec<Association>,
77    ) -> Result<(), CerememoryError> {
78        let mut reg = self.registry.write().await;
79        match reg.get_mut(record_id) {
80            Some(entry) => {
81                entry.associations = associations;
82                Ok(())
83            }
84            None => Err(CerememoryError::RecordNotFound(record_id.to_string())),
85        }
86    }
87
88    /// Add a single association to a record.
89    pub async fn add_association(
90        &self,
91        record_id: &Uuid,
92        association: Association,
93    ) -> Result<(), CerememoryError> {
94        let mut reg = self.registry.write().await;
95        match reg.get_mut(record_id) {
96            Some(entry) => {
97                // Prevent duplicate associations to the same target with same type
98                let exists = entry.associations.iter().any(|a| {
99                    a.target_id == association.target_id
100                        && a.association_type == association.association_type
101                });
102                if !exists {
103                    entry.associations.push(association);
104                }
105                Ok(())
106            }
107            None => Err(CerememoryError::RecordNotFound(record_id.to_string())),
108        }
109    }
110
111    /// Get all record IDs in a given store.
112    pub async fn records_in_store(&self, store_type: StoreType) -> Vec<Uuid> {
113        self.registry
114            .read()
115            .await
116            .iter()
117            .filter(|(_, e)| e.store_type == store_type)
118            .map(|(id, _)| *id)
119            .collect()
120    }
121
122    /// Total number of registered records.
123    pub async fn total_records(&self) -> usize {
124        self.registry.read().await.len()
125    }
126
127    /// Count records per store type.
128    pub async fn records_by_store(&self) -> HashMap<StoreType, u32> {
129        let reg = self.registry.read().await;
130        let mut counts = HashMap::new();
131        for entry in reg.values() {
132            *counts.entry(entry.store_type).or_insert(0) += 1;
133        }
134        counts
135    }
136
137    /// Total number of associations across all records.
138    pub async fn total_associations(&self) -> u32 {
139        self.registry
140            .read()
141            .await
142            .values()
143            .map(|e| e.associations.len() as u32)
144            .sum()
145    }
146
147    /// Rebuild the coordinator from a list of records.
148    /// Called on startup to repopulate the in-memory index.
149    pub async fn rebuild(&self, records: Vec<(Uuid, StoreType, Vec<Association>)>) {
150        let mut reg = self.registry.write().await;
151        reg.clear();
152        for (id, store_type, associations) in records {
153            reg.insert(
154                id,
155                RegistryEntry {
156                    store_type,
157                    associations,
158                },
159            );
160        }
161    }
162}
163
164impl Default for HippocampalCoordinator {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170impl AssociationGraph for HippocampalCoordinator {
171    async fn get_associations(
172        &self,
173        record_id: &Uuid,
174    ) -> Result<Vec<Association>, CerememoryError> {
175        let reg = self.registry.read().await;
176        match reg.get(record_id) {
177            Some(entry) => Ok(entry.associations.clone()),
178            None => Ok(Vec::new()),
179        }
180    }
181
182    async fn get_record_store_type(
183        &self,
184        record_id: &Uuid,
185    ) -> Result<Option<StoreType>, CerememoryError> {
186        Ok(self
187            .registry
188            .read()
189            .await
190            .get(record_id)
191            .map(|e| e.store_type))
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use chrono::Utc;
199
200    fn make_association(target: Uuid) -> Association {
201        Association {
202            target_id: target,
203            association_type: AssociationType::Temporal,
204            weight: 0.8,
205            created_at: Utc::now(),
206            last_co_activation: Utc::now(),
207        }
208    }
209
210    #[tokio::test]
211    async fn register_and_lookup() {
212        let coord = HippocampalCoordinator::new();
213        let id = Uuid::now_v7();
214        coord.register(id, StoreType::Episodic, vec![]).await;
215
216        let store = coord.get_record_store_type(&id).await.unwrap();
217        assert_eq!(store, Some(StoreType::Episodic));
218    }
219
220    #[tokio::test]
221    async fn unregister_removes_record() {
222        let coord = HippocampalCoordinator::new();
223        let id = Uuid::now_v7();
224        coord.register(id, StoreType::Semantic, vec![]).await;
225        assert!(coord.unregister(&id).await);
226        assert_eq!(coord.get_record_store_type(&id).await.unwrap(), None);
227    }
228
229    #[tokio::test]
230    async fn get_associations_returns_registered() {
231        let coord = HippocampalCoordinator::new();
232        let id_a = Uuid::now_v7();
233        let id_b = Uuid::now_v7();
234        let assoc = make_association(id_b);
235        coord.register(id_a, StoreType::Episodic, vec![assoc]).await;
236
237        let associations = coord.get_associations(&id_a).await.unwrap();
238        assert_eq!(associations.len(), 1);
239        assert_eq!(associations[0].target_id, id_b);
240    }
241
242    #[tokio::test]
243    async fn cross_store_associations() {
244        let coord = HippocampalCoordinator::new();
245        let ep_id = Uuid::now_v7();
246        let sem_id = Uuid::now_v7();
247
248        let ep_assoc = make_association(sem_id);
249        let sem_assoc = make_association(ep_id);
250
251        coord
252            .register(ep_id, StoreType::Episodic, vec![ep_assoc])
253            .await;
254        coord
255            .register(sem_id, StoreType::Semantic, vec![sem_assoc])
256            .await;
257
258        // Episodic record points to semantic
259        let ep_assocs = coord.get_associations(&ep_id).await.unwrap();
260        assert_eq!(
261            coord
262                .get_record_store_type(&ep_assocs[0].target_id)
263                .await
264                .unwrap(),
265            Some(StoreType::Semantic)
266        );
267
268        // Semantic record points to episodic
269        let sem_assocs = coord.get_associations(&sem_id).await.unwrap();
270        assert_eq!(
271            coord
272                .get_record_store_type(&sem_assocs[0].target_id)
273                .await
274                .unwrap(),
275            Some(StoreType::Episodic)
276        );
277    }
278
279    #[tokio::test]
280    async fn rebuild_from_records() {
281        let coord = HippocampalCoordinator::new();
282        let id1 = Uuid::now_v7();
283        let id2 = Uuid::now_v7();
284
285        coord
286            .rebuild(vec![
287                (id1, StoreType::Episodic, vec![]),
288                (id2, StoreType::Semantic, vec![]),
289            ])
290            .await;
291
292        assert_eq!(coord.total_records().await, 2);
293        let by_store = coord.records_by_store().await;
294        assert_eq!(by_store[&StoreType::Episodic], 1);
295        assert_eq!(by_store[&StoreType::Semantic], 1);
296    }
297
298    #[tokio::test]
299    async fn records_in_store_filter() {
300        let coord = HippocampalCoordinator::new();
301        let e1 = Uuid::now_v7();
302        let e2 = Uuid::now_v7();
303        let s1 = Uuid::now_v7();
304
305        coord.register(e1, StoreType::Episodic, vec![]).await;
306        coord.register(e2, StoreType::Episodic, vec![]).await;
307        coord.register(s1, StoreType::Semantic, vec![]).await;
308
309        let episodic = coord.records_in_store(StoreType::Episodic).await;
310        assert_eq!(episodic.len(), 2);
311    }
312
313    #[tokio::test]
314    async fn add_association_to_existing() {
315        let coord = HippocampalCoordinator::new();
316        let id_a = Uuid::now_v7();
317        let id_b = Uuid::now_v7();
318        coord.register(id_a, StoreType::Episodic, vec![]).await;
319
320        coord
321            .add_association(&id_a, make_association(id_b))
322            .await
323            .unwrap();
324
325        let assocs = coord.get_associations(&id_a).await.unwrap();
326        assert_eq!(assocs.len(), 1);
327    }
328
329    #[tokio::test]
330    async fn total_associations_counts_all() {
331        let coord = HippocampalCoordinator::new();
332        let a = Uuid::now_v7();
333        let b = Uuid::now_v7();
334        let c = Uuid::now_v7();
335
336        coord
337            .register(a, StoreType::Episodic, vec![make_association(b)])
338            .await;
339        coord
340            .register(
341                b,
342                StoreType::Semantic,
343                vec![make_association(a), make_association(c)],
344            )
345            .await;
346        coord.register(c, StoreType::Semantic, vec![]).await;
347
348        assert_eq!(coord.total_associations().await, 3);
349    }
350}