Skip to main content

csm_memory/
singularity_ext.rs

1//! Extension methods for Singularity (extracted to satisfy LOC gate)
2
3use crate::singularity::Singularity;
4use csm_core_lib::error::{MemoryError, Result};
5use csm_core_lib::hyperdim::HVec10240;
6use tracing::instrument;
7
8impl Singularity {
9    /// Bundle multiple concepts into a single hypervector.
10    #[instrument(skip(self, ns), fields(ids_count = ids.len()))]
11    pub fn bundle_concepts_strict(&self, ns: &str, ids: &[String]) -> Result<HVec10240> {
12        let ns_state = self
13            .get_namespace(ns)
14            .ok_or_else(|| MemoryError::NotFound {
15                entity: "Namespace".to_string(),
16                id: ns.to_string(),
17            })?;
18        let mut vectors = Vec::with_capacity(ids.len());
19        for id in ids {
20            match ns_state.concepts.get(id) {
21                Some(concept) => vectors.push(concept.vector),
22                None => {
23                    return Err(MemoryError::NotFound {
24                        entity: "Concept".to_string(),
25                        id: id.clone(),
26                    });
27                }
28            }
29        }
30
31        if vectors.is_empty() {
32            return Err(MemoryError::InvalidInput {
33                field: "ids".to_string(),
34                reason: "Empty concept list for bundling".to_string(),
35            });
36        }
37
38        HVec10240::bundle(&vectors)
39    }
40
41    pub fn update_metadata(
42        &mut self,
43        ns: &str,
44        id: &str,
45        metadata: std::collections::HashMap<String, serde_json::Value>,
46    ) -> Result<()> {
47        let ns_state = self.ensure_namespace(ns)?;
48        if let Some(concept) = ns_state.concepts.get_mut(id) {
49            concept.metadata = metadata;
50            concept.modified_at = crate::singularity::unix_now_secs();
51            self.invalidate_cache(ns);
52            Ok(())
53        } else {
54            Err(MemoryError::NotFound {
55                entity: "Concept".to_string(),
56                id: id.to_string(),
57            })
58        }
59    }
60
61    pub fn clear_associations(&mut self, ns: &str, id: &str) -> Result<()> {
62        let ns_state = self.ensure_namespace(ns)?;
63        if let Some(neighbors) = ns_state.associations.get_mut(id) {
64            neighbors.clear();
65        }
66        Ok(())
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
73    use super::*;
74    use crate::singularity::{ConceptBuilder, Singularity, SingularityConfig};
75    use csm_core_lib::error::MemoryError;
76    use std::collections::HashMap;
77
78    const NS: &str = "_default";
79
80    #[test]
81    fn test_bundle_concepts_strict_success() -> csm_core_lib::error::Result<()> {
82        let mut singularity = Singularity::with_config(SingularityConfig::default());
83        let vec1 = HVec10240::random();
84        let vec2 = HVec10240::random();
85
86        let c1 = ConceptBuilder::new("c1").with_vector(vec1).build().unwrap();
87        let c2 = ConceptBuilder::new("c2").with_vector(vec2).build().unwrap();
88
89        singularity.inject(NS, c1)?;
90        singularity.inject(NS, c2)?;
91
92        let result = singularity.bundle_concepts_strict(NS, &["c1".to_string(), "c2".to_string()]);
93        assert!(result.is_ok());
94        Ok(())
95    }
96
97    #[test]
98    fn test_bundle_concepts_strict_missing_id() -> csm_core_lib::error::Result<()> {
99        let mut singularity = Singularity::with_config(SingularityConfig::default());
100        let vec1 = HVec10240::random();
101
102        let c1 = ConceptBuilder::new("c1").with_vector(vec1).build()?;
103        singularity.inject(NS, c1)?;
104
105        let result =
106            singularity.bundle_concepts_strict(NS, &["c1".to_string(), "missing_id".to_string()]);
107
108        match result {
109            Err(MemoryError::NotFound { entity, id }) => {
110                assert_eq!(entity, "Concept");
111                assert_eq!(id, "missing_id");
112            }
113            _ => panic!("Expected NotFound error, got {result:?}"),
114        }
115        Ok(())
116    }
117
118    #[test]
119    fn test_update_metadata_not_found() {
120        let mut sing = Singularity::<HVec10240>::new(SingularityConfig::default());
121        let metadata = HashMap::new();
122
123        let result = sing.update_metadata(NS, "non-existent-id", metadata);
124
125        match result {
126            Err(MemoryError::NotFound { entity, id }) => {
127                assert_eq!(entity, "Concept");
128                assert_eq!(id, "non-existent-id");
129            }
130            _ => panic!("Expected MemoryError::NotFound, got {result:?}"),
131        }
132    }
133
134    #[test]
135    fn test_update_metadata_success() -> csm_core_lib::error::Result<()> {
136        let mut sing = Singularity::<HVec10240>::new(SingularityConfig::default());
137        let concept = ConceptBuilder::new("test-id")
138            .with_metadata("original", serde_json::Value::Bool(true))
139            .build()
140            .expect("Failed to build concept");
141
142        sing.inject(NS, concept)?;
143
144        let mut new_metadata = HashMap::new();
145        new_metadata.insert("updated".to_string(), serde_json::Value::Bool(true));
146
147        let time_before = crate::singularity::unix_now_secs();
148
149        let result = sing.update_metadata(NS, "test-id", new_metadata.clone());
150        assert!(result.is_ok());
151
152        let updated_concept = sing
153            .get(NS, "test-id")
154            .ok_or_else(|| MemoryError::NotFound {
155                entity: "Concept".to_string(),
156                id: "test-id".to_string(),
157            })?;
158        assert_eq!(updated_concept.metadata, new_metadata);
159        assert!(updated_concept.modified_at >= time_before);
160        Ok(())
161    }
162
163    #[test]
164    fn ensure_namespace_bruteforce_ok() {
165        let mut sing = Singularity::<HVec10240>::new(SingularityConfig::default());
166        assert!(sing.ensure_namespace(NS).is_ok());
167        assert!(sing.get_namespace(NS).is_some());
168    }
169
170    #[cfg(feature = "ann-hnsw")]
171    #[test]
172    fn inject_invalid_hnsw_returns_invalid_input_not_panic() {
173        use crate::index::IndexBackend;
174        let mut sing = Singularity::<HVec10240>::with_config_and_backend(
175            SingularityConfig::default(),
176            IndexBackend::Hnsw {
177                m: 0,
178                ef_construction: 200,
179                ef_search: 50,
180            },
181        );
182        let concept = ConceptBuilder::new("bad-backend")
183            .build()
184            .expect("concept build");
185        match sing.inject(NS, concept) {
186            Err(MemoryError::InvalidInput { field, .. }) => assert_eq!(field, "m"),
187            other => panic!("expected InvalidInput, got {other:?}"),
188        }
189        match sing.ensure_namespace(NS) {
190            Err(MemoryError::InvalidInput { field, .. }) => assert_eq!(field, "m"),
191            other => panic!("expected InvalidInput from ensure_namespace, got {other:?}"),
192        }
193    }
194
195    #[cfg(feature = "ann-lsh")]
196    #[test]
197    fn inject_invalid_lsh_returns_invalid_input_not_panic() {
198        use crate::index::IndexBackend;
199        let mut sing = Singularity::<HVec10240>::with_config_and_backend(
200            SingularityConfig::default(),
201            IndexBackend::Lsh {
202                num_tables: 0,
203                hash_bits: 8,
204            },
205        );
206        let concept = ConceptBuilder::new("bad-lsh")
207            .build()
208            .expect("concept build");
209        match sing.inject(NS, concept) {
210            Err(MemoryError::InvalidInput { field, .. }) => assert_eq!(field, "num_tables"),
211            other => panic!("expected InvalidInput, got {other:?}"),
212        }
213    }
214}