Skip to main content

holographic_memory/
lib.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4#![deny(clippy::all)]
5
6#[cfg(feature = "node-api")]
7use napi::bindgen_prelude::*;
8#[cfg(feature = "node-api")]
9use napi_derive::napi;
10#[cfg(feature = "node-api")]
11use std::sync::Arc;
12#[cfg(feature = "node-api")]
13use tracing::info_span;
14
15pub mod core;
16
17// Python bindings (pyo3). The `#[pymodule]` entry point is compiled into the
18// cdylib when the `python` feature is on; built into wheels by maturin.
19#[cfg(feature = "python")]
20mod python;
21
22// Node surface of the local embedder.
23#[cfg(all(feature = "node-api", feature = "local-embed"))]
24mod node_embed;
25
26pub use crate::core::documents::{
27    chunk_document, DocumentChunk, DocumentInput, DocumentRecord, DocumentResult, SearchOptions,
28};
29pub use crate::core::entangled::EntangledHVec;
30pub use crate::core::error::HmsError;
31#[cfg(not(target_arch = "wasm32"))]
32pub use crate::core::memory_api::{Memory, MemoryConfig};
33pub use crate::core::types::{
34    ConceptCandidate, HardwareCapabilities, IndexStatus, MemorizeBatchItem, QueryExplanation,
35    RetrievalResult, StorageHealth, TextMetrics,
36};
37pub use crate::core::EmbeddingSpace;
38#[cfg(not(target_arch = "wasm32"))]
39pub use crate::core::HmsCore;
40
41#[cfg(feature = "provenance")]
42pub use crate::core::provenance::types::{
43    DeletionRecord, ProvenanceRecord, StoreManifest, VerificationDetail, VerificationResult,
44};
45#[cfg(feature = "provenance")]
46pub use crate::core::provenance::TripleProvenanceParams;
47
48#[cfg(feature = "node-api")]
49#[napi]
50// codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
51pub struct HolographicMemorySystem {
52    core: Arc<HmsCore>,
53    memory: parking_lot::RwLock<Arc<crate::core::memory_api::Memory>>,
54}
55
56#[cfg(feature = "node-api")]
57fn napi_err(e: anyhow::Error) -> napi::Error {
58    napi::Error::from_reason(e.to_string())
59}
60
61/// Memory API errors carry their class in a `[code]` prefix; `memory.js` turns it into
62/// `error.code`.
63#[cfg(feature = "node-api")]
64fn memory_err(e: anyhow::Error) -> napi::Error {
65    use crate::core::memory_api::MemoryError;
66    let code = match e.downcast_ref::<MemoryError>() {
67        Some(MemoryError::Invalid(_)) => "invalid_request",
68        Some(MemoryError::NotFound(_)) => "not_found",
69        Some(MemoryError::Conflict(_)) => "conflict",
70        None => "internal",
71    };
72    napi::Error::from_reason(format!("[{code}] {e}"))
73}
74
75#[cfg(feature = "node-api")]
76async fn run_async<T: Send + 'static, F: FnOnce() -> anyhow::Result<T> + Send + 'static>(
77    f: F,
78) -> Result<T> {
79    tokio::task::spawn_blocking(f)
80        .await
81        .map_err(|e| napi::Error::from_reason(e.to_string()))?
82        .map_err(napi_err)
83}
84
85#[cfg(feature = "node-api")]
86#[napi(object)]
87pub struct HmsConfigJs {
88    pub embedding_model: Option<String>,
89    pub embedding_revision: Option<String>,
90    pub embedding_dimensions: Option<u32>,
91    pub nsg_max_degree: Option<u32>,
92    pub nsg_ef_construction: Option<u32>,
93    pub nsg_auto_threshold: Option<u32>,
94    pub ivf_enabled: Option<bool>,
95    pub ivf_n_clusters: Option<u32>,
96    pub ivf_n_landmarks: Option<u32>,
97    pub ivf_d_reduced: Option<u32>,
98    pub ivf_n_probe: Option<u32>,
99    pub ivf_auto_threshold: Option<u32>,
100    pub shard_enabled: Option<bool>,
101    pub shard_count: Option<u32>,
102    pub shard_auto_threshold: Option<u32>,
103    pub shard_target_size: Option<u32>,
104    pub component_similarity_threshold: Option<f64>,
105    pub component_max_neighbors: Option<u32>,
106    pub concept_similarity_threshold: Option<f64>,
107    pub concept_min_cluster_size: Option<u32>,
108    pub diffusion_steps: Option<u32>,
109    pub diffusion_sigma_max: Option<f64>,
110    pub diffusion_sigma_min: Option<f64>,
111    pub diffusion_step_size: Option<f64>,
112    pub diffusion_n_langevin: Option<u32>,
113    pub signing_enabled: Option<bool>,
114    pub signing_key_path: Option<String>,
115    pub encryption_enabled: Option<bool>,
116    pub encryption_passphrase: Option<String>,
117    pub encryption_passphrase_env: Option<String>,
118    pub audit_enabled: Option<bool>,
119    pub dp_enabled: Option<bool>,
120    pub dp_epsilon: Option<f64>,
121    pub meaning_enabled: Option<bool>,
122    pub meaning_beta: Option<f64>,
123    pub meaning_max_fanout: Option<u32>,
124    pub meaning_auto_decompose: Option<bool>,
125    pub meaning_max_hop_depth: Option<u32>,
126    pub cognition_enabled: Option<bool>,
127    pub cognition_interval_secs: Option<u32>,
128    pub cognition_min_pattern_freq: Option<u32>,
129    pub cognition_min_abstraction_members: Option<u32>,
130    pub cognition_min_hypothesis_confidence: Option<f64>,
131}
132
133#[cfg(feature = "node-api")]
134impl HmsConfigJs {
135    fn into_config(self) -> crate::core::config::HmsConfig {
136        use crate::core::config::*;
137        let mut cfg = HmsConfig::default();
138        if self.embedding_model.is_some()
139            || self.embedding_revision.is_some()
140            || self.embedding_dimensions.is_some()
141        {
142            cfg.embedding_space = Some(EmbeddingSpace {
143                model: self.embedding_model.unwrap_or_default(),
144                revision: self.embedding_revision.unwrap_or_default(),
145                dimensions: self.embedding_dimensions.unwrap_or_default() as usize,
146                normalization: "l2".into(),
147                metric: "cosine".into(),
148            });
149        }
150        if let Some(v) = self.nsg_max_degree {
151            cfg.nsg.max_degree = v as usize;
152        }
153        if let Some(v) = self.nsg_ef_construction {
154            cfg.nsg.ef_construction = v as usize;
155        }
156        if let Some(v) = self.nsg_auto_threshold {
157            cfg.nsg.auto_threshold = v as usize;
158        }
159        if let Some(v) = self.ivf_enabled {
160            cfg.ivf.enabled = v;
161        }
162        if let Some(v) = self.ivf_n_clusters {
163            cfg.ivf.n_clusters = v as usize;
164        }
165        if let Some(v) = self.ivf_n_landmarks {
166            cfg.ivf.n_landmarks = v as usize;
167        }
168        if let Some(v) = self.ivf_d_reduced {
169            cfg.ivf.d_reduced = v as usize;
170        }
171        if let Some(v) = self.ivf_n_probe {
172            cfg.ivf.n_probe = v as usize;
173        }
174        if let Some(v) = self.ivf_auto_threshold {
175            cfg.ivf.auto_threshold = v as usize;
176        }
177        if let Some(v) = self.shard_enabled {
178            cfg.shard.enabled = v;
179        }
180        if let Some(v) = self.shard_count {
181            cfg.shard.shard_count = v as usize;
182        }
183        if let Some(v) = self.shard_auto_threshold {
184            cfg.shard.auto_threshold = v as usize;
185        }
186        if let Some(v) = self.shard_target_size {
187            cfg.shard.target_shard_size = v as usize;
188        }
189        if let Some(v) = self.component_similarity_threshold {
190            cfg.query.component_similarity_threshold = v;
191        }
192        if let Some(v) = self.component_max_neighbors {
193            cfg.query.component_max_neighbors = v;
194        }
195        if let Some(v) = self.concept_similarity_threshold {
196            cfg.concepts.similarity_threshold = v;
197        }
198        if let Some(v) = self.concept_min_cluster_size {
199            cfg.concepts.min_cluster_size = v as usize;
200        }
201        if let Some(v) = self.diffusion_steps {
202            cfg.diffusion.steps = v as usize;
203        }
204        if let Some(v) = self.diffusion_sigma_max {
205            cfg.diffusion.sigma_max = v;
206        }
207        if let Some(v) = self.diffusion_sigma_min {
208            cfg.diffusion.sigma_min = v;
209        }
210        if let Some(v) = self.diffusion_step_size {
211            cfg.diffusion.step_size = v;
212        }
213        if let Some(v) = self.diffusion_n_langevin {
214            cfg.diffusion.n_langevin = v as usize;
215        }
216        if let Some(v) = self.signing_enabled {
217            cfg.security.signing_enabled = v;
218        }
219        if let Some(v) = self.signing_key_path {
220            cfg.security.key_path = Some(v);
221        }
222        if let Some(v) = self.encryption_enabled {
223            cfg.security.encryption_enabled = v;
224        }
225        if let Some(v) = self.encryption_passphrase {
226            cfg.security.encryption_passphrase = Some(v);
227        }
228        if let Some(v) = self.encryption_passphrase_env {
229            cfg.security.encryption_passphrase_env = Some(v);
230        }
231        if let Some(v) = self.audit_enabled {
232            cfg.security.audit_enabled = v;
233        }
234        if let Some(v) = self.dp_enabled {
235            cfg.privacy.dp_enabled = v;
236        }
237        if let Some(v) = self.dp_epsilon {
238            cfg.privacy.epsilon = v;
239        }
240        if let Some(v) = self.meaning_enabled {
241            cfg.meaning.enabled = v;
242        }
243        if let Some(v) = self.meaning_beta {
244            cfg.meaning.beta = v;
245        }
246        if let Some(v) = self.meaning_max_fanout {
247            cfg.meaning.algebraic_max_fanout = v as usize;
248        }
249        if let Some(v) = self.meaning_auto_decompose {
250            cfg.meaning.auto_decompose = v;
251        }
252        if let Some(v) = self.meaning_max_hop_depth {
253            cfg.meaning.max_hop_depth = v as usize;
254        }
255        if let Some(v) = self.cognition_enabled {
256            cfg.cognition.enabled = v;
257        }
258        if let Some(v) = self.cognition_interval_secs {
259            cfg.cognition.interval_secs = v as u64;
260        }
261        if let Some(v) = self.cognition_min_pattern_freq {
262            cfg.cognition.min_pattern_freq = v as usize;
263        }
264        if let Some(v) = self.cognition_min_abstraction_members {
265            cfg.cognition.min_abstraction_members = v as usize;
266        }
267        if let Some(v) = self.cognition_min_hypothesis_confidence {
268            cfg.cognition.min_hypothesis_confidence = v;
269        }
270        cfg
271    }
272}
273
274#[cfg(feature = "node-api")]
275#[napi]
276// codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
277impl HolographicMemorySystem {
278    #[napi(constructor)]
279    pub fn new(
280        dimensions: u32,
281        storage_path: Option<String>,
282        config: Option<HmsConfigJs>,
283    ) -> Result<Self> {
284        let cfg = config.map(|c| c.into_config());
285        let core = HmsCore::new(dimensions, storage_path, cfg).map_err(napi_err)?;
286        let core = Arc::new(core);
287        Ok(Self {
288            memory: parking_lot::RwLock::new(Arc::new(crate::core::memory_api::Memory::new(
289                core.clone(),
290            ))),
291            core,
292        })
293    }
294
295    /// Replaces the memory API's configuration (policies and defaults; see
296    /// docs/memory-api). Pass nothing to restore the defaults.
297    #[napi]
298    pub fn configure_memory(&self, config: Option<serde_json::Value>) -> Result<()> {
299        let config: crate::core::memory_api::MemoryConfig = match config {
300            Some(value) => serde_json::from_value(value)
301                .map_err(|e| napi::Error::from_reason(format!("invalid memory config: {e}")))?,
302            None => Default::default(),
303        };
304        let memory = crate::core::memory_api::Memory::with_config(self.core.clone(), config)
305            .map_err(napi_err)?;
306        *self.memory.write() = Arc::new(memory);
307        Ok(())
308    }
309
310    /// One memory API operation as JSON in and JSON out: `remember`, `recall`, `explain`,
311    /// `forget`, `get`, `history`, `applyForgetting` or `consolidate`. The typed wrapper is
312    /// `Memory` in `memory.js`.
313    #[napi]
314    pub async fn memory_call(
315        &self,
316        op: String,
317        request: serde_json::Value,
318    ) -> Result<serde_json::Value> {
319        let memory = self.memory.read().clone();
320        tokio::task::spawn_blocking(move || memory.call(&op, request))
321            .await
322            .map_err(|e| napi::Error::from_reason(e.to_string()))?
323            .map_err(memory_err)
324    }
325
326    #[napi]
327    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
328    pub async fn memorize_document(&self, input: DocumentInput) -> Result<u32> {
329        let core = self.core.clone();
330        run_async(move || core.memorize_document(input)).await
331    }
332
333    #[napi]
334    pub async fn memorize_documents(&self, inputs: Vec<DocumentInput>) -> Result<Vec<u32>> {
335        let core = self.core.clone();
336        run_async(move || core.memorize_documents(inputs)).await
337    }
338
339    #[napi]
340    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
341    pub async fn chunk_document(&self, input: DocumentInput) -> Result<Vec<DocumentChunk>> {
342        run_async(move || chunk_document(&input)).await
343    }
344
345    #[napi]
346    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
347    pub async fn search_documents(
348        &self,
349        text: String,
350        options: Option<SearchOptions>,
351    ) -> Result<Vec<DocumentResult>> {
352        let core = self.core.clone();
353        run_async(move || core.search_documents(&text, &options.unwrap_or_default())).await
354    }
355
356    #[napi]
357    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
358    pub async fn delete_document(&self, id: String) -> Result<bool> {
359        let core = self.core.clone();
360        run_async(move || core.delete_document(&id)).await
361    }
362
363    #[napi]
364    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
365    pub fn security_status(&self) -> serde_json::Value {
366        self.core.security_status()
367    }
368
369    #[napi(getter)]
370    pub fn vector_count(&self) -> u32 {
371        self.core.vector_count() as u32
372    }
373
374    #[napi(getter)]
375    pub fn nsg_trained(&self) -> bool {
376        self.core.nsg_trained()
377    }
378
379    #[napi(getter)]
380    pub fn ivf_trained(&self) -> bool {
381        self.core.ivf_trained()
382    }
383
384    #[napi(getter)]
385    pub fn dimensions(&self) -> u32 {
386        self.core.dimensions() as u32
387    }
388
389    #[napi]
390    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
391    pub fn index_status(&self) -> IndexStatus {
392        self.core.index_status()
393    }
394
395    #[napi]
396    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
397    pub fn storage_health(&self) -> StorageHealth {
398        self.core.storage_health()
399    }
400
401    #[napi]
402    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
403    pub fn hardware_capabilities(&self) -> HardwareCapabilities {
404        crate::core::hardware::capabilities()
405    }
406
407    #[napi]
408    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
409    pub fn explain_query(&self, text: String, k: u32) -> QueryExplanation {
410        let query = self.core.encode_text(&text);
411        self.core.explain_query(&query, k)
412    }
413
414    #[napi]
415    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
416    pub async fn maintain_indices(&self) -> Result<IndexStatus> {
417        let core = self.core.clone();
418        run_async(move || core.maintain_indices()).await
419    }
420
421    #[napi]
422    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
423    pub async fn flush(&self) -> Result<()> {
424        let core = self.core.clone();
425        run_async(move || core.flush()).await
426    }
427
428    #[napi]
429    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
430    pub async fn analyze_text(&self, text: String) -> Result<TextMetrics> {
431        let core = self.core.clone();
432        run_async(move || Ok(core.analyze_text(&text))).await
433    }
434
435    #[napi]
436    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
437    pub async fn calculate_readability(&self, metrics: TextMetrics) -> Result<f64> {
438        let core = self.core.clone();
439        run_async(move || Ok(core.calculate_readability(&metrics))).await
440    }
441
442    #[napi]
443    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
444    pub async fn memorize_text(
445        &self,
446        id: String,
447        text: String,
448        trace_id: Option<String>,
449    ) -> Result<()> {
450        let core = self.core.clone();
451        run_async(move || {
452            let _span =
453                info_span!("memorize_text", id = %id, trace_id = trace_id.as_deref().unwrap_or(""))
454                    .entered();
455            let vec = core.encode_text(&text);
456            core.memorize(id, vec)
457        })
458        .await
459    }
460
461    /// Zero-copy text ingestion from a Node.js Buffer. Avoids the UTF-8 copy
462    /// that occurs with String parameters by reading bytes in-place.
463    #[napi]
464    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
465    pub async fn memorize_text_buffer(
466        &self,
467        id: String,
468        text: Buffer,
469        trace_id: Option<String>,
470    ) -> Result<()> {
471        let core = self.core.clone();
472        let text_str = std::str::from_utf8(&text)
473            .map_err(|e| napi::Error::from_reason(format!("Invalid UTF-8: {}", e)))?
474            .to_owned();
475        run_async(move || {
476            let _span = info_span!("memorize_text_buffer", id = %id, trace_id = trace_id.as_deref().unwrap_or("")).entered();
477            let vec = core.encode_text(&text_str);
478            core.memorize(id, vec)
479        })
480        .await
481    }
482
483    /// Batch memorize multiple id/text pairs in a single native call.
484    /// Uses rayon for parallel encoding, then inserts sequentially.
485    #[napi]
486    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
487    pub async fn memorize_batch(
488        &self,
489        items: Vec<MemorizeBatchItem>,
490        trace_id: Option<String>,
491    ) -> Result<()> {
492        let core = self.core.clone();
493        run_async(move || {
494            let _span = info_span!(
495                "memorize_batch",
496                count = items.len(),
497                trace_id = trace_id.as_deref().unwrap_or("")
498            )
499            .entered();
500            core.memorize_batch(&items)
501        })
502        .await
503    }
504
505    /// Ingest a UTF-8 file (up to 8 MiB) as a chunked document with source offsets.
506    /// Retrieve passages with searchDocuments and remove them with deleteDocument.
507    #[napi]
508    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
509    pub async fn memorize_file(&self, id: String, file_path: String) -> Result<()> {
510        let core = self.core.clone();
511        run_async(move || {
512            use std::io::Read;
513            let file = std::fs::File::open(&file_path)
514                .map_err(|e| anyhow::anyhow!("Failed to open {}: {}", file_path, e))?;
515            anyhow::ensure!(
516                file.metadata()?.is_file(),
517                "filePath must name a regular file"
518            );
519            let mut bytes = Vec::new();
520            file.take(core::documents::MAX_DOCUMENT_BYTES as u64 + 1)
521                .read_to_end(&mut bytes)?;
522            anyhow::ensure!(
523                bytes.len() <= core::documents::MAX_DOCUMENT_BYTES,
524                "file exceeds 8 MiB"
525            );
526            let text = String::from_utf8(bytes)?;
527            core.memorize_document(DocumentInput {
528                id,
529                text,
530                source_uri: Some(file_path),
531                version: None,
532                metadata: None,
533                chunk_words: None,
534                overlap_words: None,
535                store_text: None,
536                title_weight: None,
537                embeddings: None,
538            })?;
539            Ok(())
540        })
541        .await
542    }
543
544    #[napi]
545    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
546    pub async fn memorize_vector(&self, id: String, vector: Float32Array) -> Result<()> {
547        let core = self.core.clone();
548        let dense: Vec<f32> = vector.to_vec();
549        run_async(move || core.memorize_vector(id, &dense)).await
550    }
551
552    #[napi]
553    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
554    pub async fn memorize_scalar(&self, id: String, value: f64, min: f64, max: f64) -> Result<()> {
555        let core = self.core.clone();
556        run_async(move || core.memorize_scalar(id, value, min, max)).await
557    }
558
559    #[napi]
560    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
561    pub async fn query(
562        &self,
563        text: String,
564        k: u32,
565        trace_id: Option<String>,
566    ) -> Result<Vec<RetrievalResult>> {
567        let core = self.core.clone();
568        run_async(move || {
569            let _span =
570                info_span!("query", k = k, trace_id = trace_id.as_deref().unwrap_or("")).entered();
571            let q_vec = core.encode_text(&text);
572            let results = core.query(&q_vec, k);
573            Ok(results)
574        })
575        .await
576    }
577
578    /// Converts float32→sparse EntangledHVec on JS thread, then queries on background.
579    #[napi]
580    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
581    pub async fn query_vector(&self, vector: Float32Array, k: u32) -> Result<Vec<RetrievalResult>> {
582        let core = self.core.clone();
583        let dense = vector.to_vec();
584        run_async(move || core.query_vector(&dense, k)).await
585    }
586
587    #[napi]
588    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
589    pub async fn query_scalar(
590        &self,
591        value: f64,
592        min: f64,
593        max: f64,
594        k: u32,
595    ) -> Result<Vec<RetrievalResult>> {
596        let core = self.core.clone();
597        run_async(move || {
598            let q_vec = EntangledHVec::from_scalar(value, min, max, core.dimensions());
599            let results = core.query(&q_vec, k);
600            Ok(results)
601        })
602        .await
603    }
604
605    /// Process multiple text queries in parallel, returning results for each.
606    #[napi]
607    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
608    pub async fn query_batch(
609        &self,
610        texts: Vec<String>,
611        k: u32,
612    ) -> Result<Vec<Vec<RetrievalResult>>> {
613        let core = self.core.clone();
614        run_async(move || {
615            let queries: Vec<EntangledHVec> = texts.iter().map(|t| core.encode_text(t)).collect();
616            Ok(core.query_batch(&queries, k))
617        })
618        .await
619    }
620
621    /// Process multiple float32 vector queries in parallel.
622    #[napi]
623    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
624    pub async fn query_vector_batch(
625        &self,
626        vectors: Vec<Float32Array>,
627        k: u32,
628    ) -> Result<Vec<Vec<RetrievalResult>>> {
629        let core = self.core.clone();
630        let dense: Vec<Vec<f32>> = vectors.iter().map(|v| v.to_vec()).collect();
631        run_async(move || {
632            anyhow::ensure!(dense.len() <= 4096, "query batch exceeds 4096 vectors");
633            use rayon::prelude::*;
634            dense.par_iter().map(|v| core.query_vector(v, k)).collect()
635        })
636        .await
637    }
638
639    #[napi]
640    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
641    pub async fn analyze_components(&self, text: String) -> Result<Vec<RetrievalResult>> {
642        let core = self.core.clone();
643        run_async(move || {
644            let vec = core.encode_text(&text);
645            let results = core.analyze_components(&vec);
646            Ok(results)
647        })
648        .await
649    }
650
651    #[napi]
652    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
653    pub async fn factorize_diffusion(
654        &self,
655        product_text: String,
656        domains: Vec<Vec<String>>,
657        max_iter: u32,
658    ) -> Result<Vec<Option<String>>> {
659        let core = self.core.clone();
660        run_async(move || {
661            let vec = core.encode_text(&product_text);
662            let domain_vecs: Vec<Vec<EntangledHVec>> = domains
663                .iter()
664                .map(|d| d.iter().map(|s| core.encode_text(s)).collect())
665                .collect();
666            let results = core.factorize_diffusion(&vec, &domain_vecs, max_iter as usize);
667
668            // Map EntangledHVec results back to IDs from the domain strings
669            let mapped = results
670                .into_iter()
671                .enumerate()
672                .map(|(i, opt_vec)| {
673                    opt_vec.and_then(|evec| {
674                        domains[i]
675                            .iter()
676                            .zip(domain_vecs[i].iter())
677                            .min_by_key(|(_, enc)| evec.hamming(enc))
678                            .map(|(s, _)| s.clone())
679                    })
680                })
681                .collect();
682            Ok(mapped)
683        })
684        .await
685    }
686
687    #[napi]
688    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
689    pub async fn memorize_triplet(
690        &self,
691        id: String,
692        head: String,
693        relation: String,
694        tail: String,
695    ) -> Result<()> {
696        let core = self.core.clone();
697        run_async(move || core.memorize_triplet(id, head, relation, tail)).await
698    }
699
700    #[napi]
701    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
702    pub async fn query_triplet(
703        &self,
704        head: String,
705        relation: String,
706        k: u32,
707    ) -> Result<Vec<RetrievalResult>> {
708        let core = self.core.clone();
709        run_async(move || core.query_triplet(head, relation, k)).await
710    }
711
712    /// Finds an analogy: A is to B as C is to ?.
713    ///
714    /// NOTE: Currently uses character trigram encoding, which has limited semantic
715    /// understanding. Complex semantic analogies may require higher-level word
716    /// embeddings (slated for future upgrade).
717    #[napi]
718    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
719    pub async fn find_analogy(
720        &self,
721        a: String,
722        b: String,
723        c: String,
724        k: Option<u32>,
725        trace_id: Option<String>,
726    ) -> Result<Vec<RetrievalResult>> {
727        let core = self.core.clone();
728        run_async(move || {
729            let _span =
730                info_span!("find_analogy", trace_id = trace_id.as_deref().unwrap_or("")).entered();
731            let results = core.find_analogy(&a, &b, &c, k.unwrap_or(5));
732            Ok(results)
733        })
734        .await
735    }
736
737    #[napi]
738    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
739    pub async fn synthesize_concepts(&self) -> Result<Vec<ConceptCandidate>> {
740        let core = self.core.clone();
741        run_async(move || {
742            let results = core.synthesize_concepts();
743            Ok(results)
744        })
745        .await
746    }
747
748    #[napi]
749    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
750    pub async fn memorize_sequence(&self, id: String, sequence: Vec<String>) -> Result<()> {
751        let core = self.core.clone();
752        run_async(move || core.memorize_sequence(id, &sequence)).await
753    }
754
755    #[napi]
756    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
757    pub async fn train_nsg(&self) -> Result<()> {
758        let core = self.core.clone();
759        run_async(move || core.train_nsg()).await
760    }
761
762    #[napi]
763    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
764    pub async fn train_ivf(&self) -> Result<()> {
765        let core = self.core.clone();
766        run_async(move || core.train_ivf()).await
767    }
768
769    #[napi]
770    pub async fn train_document_index(&self) -> Result<()> {
771        let core = self.core.clone();
772        run_async(move || core.train_document_index()).await
773    }
774
775    #[napi(getter)]
776    pub fn document_index_trained(&self) -> bool {
777        self.core.document_index_trained()
778    }
779
780    #[napi]
781    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
782    pub async fn query_sequence(
783        &self,
784        partial: Vec<String>,
785        k: u32,
786    ) -> Result<Vec<RetrievalResult>> {
787        let core = self.core.clone();
788        run_async(move || core.query_sequence(&partial, k)).await
789    }
790
791    #[napi]
792    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
793    pub async fn delete(&self, id: String) -> Result<bool> {
794        let core = self.core.clone();
795        run_async(move || core.delete(&id)).await
796    }
797
798    #[napi]
799    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
800    pub async fn compact(&self) -> Result<()> {
801        let core = self.core.clone();
802        run_async(move || core.compact()).await
803    }
804
805    #[napi]
806    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
807    pub async fn audit_since(&self, timestamp_ms: f64) -> Result<Vec<AuditEntryJs>> {
808        let core = self.core.clone();
809        let ts = timestamp_ms as u64;
810        run_async(move || {
811            let entries = core.audit_since(ts)?;
812            Ok(entries
813                .into_iter()
814                .map(|e| AuditEntryJs {
815                    timestamp_ms: e.timestamp_ms as f64,
816                    op: match e.op {
817                        crate::core::audit::AuditOp::Memorize => "memorize".to_string(),
818                        crate::core::audit::AuditOp::Delete => "delete".to_string(),
819                        crate::core::audit::AuditOp::Compact => "compact".to_string(),
820                    },
821                    id_hash: e.id_hash.iter().map(|b| format!("{:02x}", b)).collect(),
822                    signed: e.signature != [0u8; 64],
823                })
824                .collect())
825        })
826        .await
827    }
828
829    /// Bundle multiple text items into a single hypervector.
830    /// Respects the PrivacyConfig: when dp_enabled, uses epsilon-DP noise.
831    #[napi]
832    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
833    pub async fn bundle_texts(&self, texts: Vec<String>) -> Result<Vec<u32>> {
834        let core = self.core.clone();
835        run_async(move || {
836            let vecs: Vec<EntangledHVec> = texts.iter().map(|t| core.encode_text(t)).collect();
837            let bundled = core.bundle(&vecs);
838            Ok(bundled.indices().to_vec())
839        })
840        .await
841    }
842
843    // === Meaning Memory API ===
844
845    #[napi]
846    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
847    pub async fn memorize_meaning(&self, id: String, text: String) -> Result<()> {
848        let core = self.core.clone();
849        run_async(move || core.memorize_meaning(&id, &text)).await
850    }
851
852    #[napi]
853    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
854    pub async fn structural_query(
855        &self,
856        known_subjects: Vec<String>,
857        known_relations: Vec<String>,
858        target_role: String,
859    ) -> Result<Vec<StructuralResultJs>> {
860        let core = self.core.clone();
861        run_async(move || {
862            let known_vecs: Vec<EntangledHVec> = known_subjects
863                .iter()
864                .chain(known_relations.iter())
865                .map(|t| core.encode_text(t))
866                .collect();
867            let mut bindings: Vec<(&str, &EntangledHVec)> = Vec::new();
868            for (i, v) in known_vecs.iter().enumerate() {
869                if i < known_subjects.len() {
870                    bindings.push(("subject", v));
871                } else {
872                    bindings.push(("relation", v));
873                }
874            }
875            let results = core.structural_query(&bindings, &target_role);
876            Ok(results
877                .into_iter()
878                .map(|r| StructuralResultJs {
879                    entity_id: r.entity_id,
880                    confidence: r.confidence,
881                    path: format!("{:?}", r.path),
882                })
883                .collect())
884        })
885        .await
886    }
887
888    #[napi]
889    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
890    pub async fn multi_hop_query(
891        &self,
892        start_entity: String,
893        relations: Vec<String>,
894    ) -> Result<Vec<MultiHopResultJs>> {
895        let core = self.core.clone();
896        run_async(move || {
897            let rel_refs: Vec<&str> = relations.iter().map(|s| s.as_str()).collect();
898            let results = core.multi_hop(&start_entity, &rel_refs);
899            Ok(results
900                .into_iter()
901                .map(|r| MultiHopResultJs {
902                    entity_id: r.entity_id,
903                    confidence: r.confidence,
904                    method: format!("{:?}", r.method),
905                })
906                .collect())
907        })
908        .await
909    }
910
911    #[napi]
912    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
913    pub async fn meaning_cleanup(&self, text: String) -> Result<Option<CleanupResultJs>> {
914        let core = self.core.clone();
915        run_async(move || {
916            let vec = core.encode_text(&text);
917            Ok(core
918                .meaning_cleanup(&vec)
919                .map(|(id, confidence)| CleanupResultJs { id, confidence }))
920        })
921        .await
922    }
923
924    #[napi]
925    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
926    pub async fn cleanup_vector(&self, vector: Float32Array) -> Result<Option<CleanupResultJs>> {
927        let core = self.core.clone();
928        let q_vec = EntangledHVec::from_dense(&vector, core.dimensions());
929        run_async(move || {
930            Ok(core
931                .meaning_cleanup(&q_vec)
932                .map(|(id, confidence)| CleanupResultJs { id, confidence }))
933        })
934        .await
935    }
936
937    #[napi]
938    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
939    pub fn declare_composition_rule(
940        &self,
941        name: String,
942        input_relations: Vec<String>,
943        output_relation: String,
944    ) -> Result<()> {
945        self.core
946            .declare_rule(&name, input_relations, output_relation)
947            .map_err(napi_err)
948    }
949
950    #[napi(getter)]
951    pub fn meaning_enabled(&self) -> bool {
952        self.core.meaning_enabled()
953    }
954
955    // === Cognition API ===
956
957    #[napi]
958    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
959    pub fn start_cognition(&self) -> Result<()> {
960        self.core.start_cognition().map_err(napi_err)
961    }
962
963    #[napi]
964    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
965    pub fn stop_cognition(&self) {
966        self.core.stop_cognition();
967    }
968
969    #[napi(getter)]
970    pub fn cognition_running(&self) -> bool {
971        self.core.cognition_running()
972    }
973
974    #[napi(getter)]
975    pub fn cognition_cycle_count(&self) -> u32 {
976        self.core.cognition_cycle_count() as u32
977    }
978
979    #[napi(getter)]
980    pub fn cognition_insight_count(&self) -> u32 {
981        self.core.cognition_insight_count() as u32
982    }
983
984    #[napi(getter)]
985    pub fn cognition_enabled(&self) -> bool {
986        self.core.cognition_enabled()
987    }
988
989    #[napi]
990    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
991    pub fn run_cognition_once(&self) -> u32 {
992        self.core.run_cognition_once().len() as u32
993    }
994
995    #[napi]
996    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
997    pub fn govern_memory(&self) -> GovernanceReportJs {
998        let report = self.core.govern_memory();
999        GovernanceReportJs {
1000            composites_merged: report.composites_merged as u32,
1001            composites_forgotten: report.composites_forgotten as u32,
1002            atoms_forgotten: report.atoms_forgotten as u32,
1003            idf_refreshed: report.idf_refreshed,
1004            atoms_refined: report.refinement.atoms_refined as u32,
1005        }
1006    }
1007
1008    // === Agency API ===
1009
1010    #[napi]
1011    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1012    pub fn add_goal(
1013        &self,
1014        name: String,
1015        description: String,
1016        relevance: f64,
1017        urgency: f64,
1018        cost: f64,
1019    ) -> Option<u32> {
1020        self.core
1021            .add_goal(&name, &description, relevance, urgency, cost)
1022            .map(|id| id as u32)
1023    }
1024
1025    #[napi]
1026    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1027    pub fn deactivate_goal(&self, name: String) -> bool {
1028        self.core.deactivate_goal(&name)
1029    }
1030
1031    #[napi]
1032    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1033    pub fn active_goals(&self) -> Vec<GoalJs> {
1034        self.core
1035            .active_goals()
1036            .into_iter()
1037            .map(|(name, utility)| GoalJs { name, utility })
1038            .collect()
1039    }
1040
1041    #[napi]
1042    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1043    pub fn plan_goal(
1044        &self,
1045        goal: String,
1046        causal_relations: Vec<String>,
1047        max_depth: Option<u32>,
1048    ) -> PlanJs {
1049        let rel_refs: Vec<&str> = causal_relations.iter().map(|s| s.as_str()).collect();
1050        let plan = self
1051            .core
1052            .plan_goal(&goal, &rel_refs, max_depth.unwrap_or(10) as usize);
1053        PlanJs {
1054            goal: plan.goal,
1055            actions: plan
1056                .actions
1057                .into_iter()
1058                .map(|a| PlannedActionJs {
1059                    subject: a.subject,
1060                    relation: a.relation,
1061                    object: a.object,
1062                    depth: a.depth as u32,
1063                    cost: a.cost,
1064                })
1065                .collect(),
1066            complete: plan.complete,
1067            total_cost: plan.total_cost,
1068        }
1069    }
1070
1071    #[napi]
1072    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1073    pub fn generate_questions(&self) -> Vec<QuestionJs> {
1074        self.core
1075            .generate_questions()
1076            .into_iter()
1077            .map(|q| QuestionJs {
1078                text: q.text,
1079                goal_relevance: q.goal_relevance,
1080            })
1081            .collect()
1082    }
1083
1084    #[napi]
1085    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1086    pub fn propose_rule(
1087        &self,
1088        name: String,
1089        input_relations: Vec<String>,
1090        output_relation: String,
1091        reason: String,
1092    ) -> Option<u32> {
1093        self.core
1094            .propose_rule(&name, input_relations, &output_relation, &reason)
1095            .map(|id| id as u32)
1096    }
1097
1098    #[napi]
1099    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1100    pub fn approve_proposal(&self, id: u32) -> bool {
1101        self.core.approve_proposal(id as usize)
1102    }
1103
1104    #[napi]
1105    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1106    pub fn reject_proposal(&self, id: u32) -> bool {
1107        self.core.reject_proposal(id as usize)
1108    }
1109
1110    #[napi(getter)]
1111    pub fn pending_proposals(&self) -> u32 {
1112        self.core.pending_proposals() as u32
1113    }
1114
1115    #[napi(getter)]
1116    pub fn goal_count(&self) -> u32 {
1117        self.core.goal_count() as u32
1118    }
1119
1120    #[napi(getter)]
1121    pub fn active_goal_count(&self) -> u32 {
1122        self.core.active_goal_count() as u32
1123    }
1124
1125    // === Graph API ===
1126
1127    #[napi]
1128    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1129    pub async fn add_relation(
1130        &self,
1131        source_id: String,
1132        relation_type: String,
1133        target_id: String,
1134        properties: Option<String>,
1135        valid_from: Option<f64>,
1136        valid_to: Option<f64>,
1137    ) -> Result<()> {
1138        let core = self.core.clone();
1139        run_async(move || {
1140            let rel = crate::core::types::Relation {
1141                source_id,
1142                relation_type,
1143                target_id,
1144                properties,
1145                valid_from: valid_from.unwrap_or(0.0),
1146                valid_to: valid_to.unwrap_or(0.0),
1147            };
1148            core.add_relation(&rel)
1149        })
1150        .await
1151    }
1152
1153    #[napi]
1154    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1155    pub async fn remove_relation(
1156        &self,
1157        source_id: String,
1158        relation_type: String,
1159        target_id: String,
1160    ) -> Result<bool> {
1161        let core = self.core.clone();
1162        run_async(move || core.remove_relation(&source_id, &relation_type, &target_id)).await
1163    }
1164
1165    #[napi]
1166    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1167    pub fn declare_relation_type(
1168        &self,
1169        name: String,
1170        transitive: Option<bool>,
1171        symmetric: Option<bool>,
1172    ) -> Result<()> {
1173        self.core
1174            .declare_relation_type(crate::core::types::RelationType {
1175                name,
1176                transitive: transitive.unwrap_or(false),
1177                symmetric: symmetric.unwrap_or(false),
1178            })
1179            .map_err(napi_err)
1180    }
1181
1182    #[napi]
1183    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1184    pub async fn traverse(
1185        &self,
1186        start_id: String,
1187        relation_type: Option<String>,
1188        max_depth: Option<u32>,
1189        at_time: Option<f64>,
1190    ) -> Result<Vec<crate::core::types::GraphPath>> {
1191        let core = self.core.clone();
1192        run_async(move || {
1193            Ok(core.traverse(
1194                &start_id,
1195                relation_type.as_deref(),
1196                max_depth.unwrap_or(3),
1197                at_time.unwrap_or(0.0),
1198            ))
1199        })
1200        .await
1201    }
1202
1203    #[napi]
1204    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1205    pub async fn outgoing_relations(
1206        &self,
1207        source_id: String,
1208        relation_type: Option<String>,
1209        at_time: Option<f64>,
1210    ) -> Result<Vec<crate::core::types::Relation>> {
1211        let core = self.core.clone();
1212        run_async(move || {
1213            Ok(core.outgoing_relations(
1214                &source_id,
1215                relation_type.as_deref(),
1216                at_time.unwrap_or(0.0),
1217            ))
1218        })
1219        .await
1220    }
1221
1222    #[napi]
1223    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1224    pub async fn incoming_relations(
1225        &self,
1226        target_id: String,
1227        relation_type: Option<String>,
1228        at_time: Option<f64>,
1229    ) -> Result<Vec<crate::core::types::Relation>> {
1230        let core = self.core.clone();
1231        run_async(move || {
1232            Ok(core.incoming_relations(
1233                &target_id,
1234                relation_type.as_deref(),
1235                at_time.unwrap_or(0.0),
1236            ))
1237        })
1238        .await
1239    }
1240
1241    #[napi(getter)]
1242    pub fn relation_count(&self) -> u32 {
1243        self.core.relation_count() as u32
1244    }
1245
1246    #[napi]
1247    // codeql[rust/access-invalid-pointer] False positive: NAPI macro generated FFI bindings
1248    pub async fn federated_query(
1249        &self,
1250        peer_paths: Vec<String>,
1251        text: String,
1252        k: u32,
1253    ) -> Result<Vec<RetrievalResult>> {
1254        let core = self.core.clone();
1255        run_async(move || {
1256            let q_vec = core.encode_text(&text);
1257            core.federated_query(&peer_paths, &q_vec, k)
1258        })
1259        .await
1260    }
1261}
1262
1263#[cfg(feature = "node-api")]
1264#[napi(object)]
1265pub struct AuditEntryJs {
1266    pub timestamp_ms: f64,
1267    pub op: String,
1268    pub id_hash: String,
1269    pub signed: bool,
1270}
1271
1272#[cfg(feature = "node-api")]
1273#[napi(object)]
1274pub struct StructuralResultJs {
1275    pub entity_id: String,
1276    pub confidence: f64,
1277    pub path: String,
1278}
1279
1280#[cfg(feature = "node-api")]
1281#[napi(object)]
1282pub struct MultiHopResultJs {
1283    pub entity_id: String,
1284    pub confidence: f64,
1285    pub method: String,
1286}
1287
1288#[cfg(feature = "node-api")]
1289#[napi(object)]
1290pub struct CleanupResultJs {
1291    pub id: String,
1292    pub confidence: f64,
1293}
1294
1295#[cfg(feature = "node-api")]
1296#[napi(object)]
1297pub struct GovernanceReportJs {
1298    pub composites_merged: u32,
1299    pub composites_forgotten: u32,
1300    pub atoms_forgotten: u32,
1301    pub idf_refreshed: bool,
1302    pub atoms_refined: u32,
1303}
1304
1305#[cfg(feature = "node-api")]
1306#[napi(object)]
1307pub struct GoalJs {
1308    pub name: String,
1309    pub utility: f64,
1310}
1311
1312#[cfg(feature = "node-api")]
1313#[napi(object)]
1314pub struct PlannedActionJs {
1315    pub subject: String,
1316    pub relation: String,
1317    pub object: String,
1318    pub depth: u32,
1319    pub cost: f64,
1320}
1321
1322#[cfg(feature = "node-api")]
1323#[napi(object)]
1324pub struct PlanJs {
1325    pub goal: String,
1326    pub actions: Vec<PlannedActionJs>,
1327    pub complete: bool,
1328    pub total_cost: f64,
1329}
1330
1331#[cfg(feature = "node-api")]
1332#[napi(object)]
1333pub struct QuestionJs {
1334    pub text: String,
1335    pub goal_relevance: f64,
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340    use crate::core::engine::HmsCore;
1341    use crate::core::entangled::EntangledHVec;
1342
1343    #[test]
1344    fn test_determinism() {
1345        let dir = tempfile::tempdir().unwrap();
1346        let hms = HmsCore::new(1000, Some(dir.path().display().to_string()), None).unwrap();
1347        let v1 = hms.encode_text("hello world");
1348        let v2 = hms.encode_text("hello world");
1349        assert!((v1.similarity(&v2) - 1.0).abs() < 0.0001);
1350    }
1351
1352    #[test]
1353    fn test_store_rejects_concurrent_engines_and_reopens_after_drop() {
1354        let dir = tempfile::tempdir().unwrap();
1355        let path = dir.path().display().to_string();
1356        let first = HmsCore::new(1000, Some(path.clone()), None).unwrap();
1357        let error = HmsCore::new(1000, Some(path.clone()), None)
1358            .err()
1359            .expect("a second writable engine must be rejected");
1360        assert!(error.to_string().contains("already open"));
1361        drop(first);
1362        HmsCore::new(1000, Some(path), None).expect("store lock must release after teardown");
1363    }
1364
1365    #[test]
1366    fn test_from_dense_produces_sparse() {
1367        let dense: Vec<f32> = (0..128).map(|i| (i as f32 - 64.0) / 64.0).collect();
1368        let e = EntangledHVec::from_dense(&dense, 1000);
1369        assert_eq!(e.dim, 1000);
1370        // Should have ~dim/256 active indices
1371        let expected = 1000 / 256;
1372        assert_eq!(e.indices.len(), expected);
1373    }
1374
1375    // === Batch Memorization ===
1376
1377    #[test]
1378    fn test_batch_memorize() {
1379        let dir = tempfile::tempdir().unwrap();
1380        let hms =
1381            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1382
1383        // Simulate what memorize_batch does: parallel encode, sequential insert
1384        let items = vec![
1385            ("b1", "first batch item"),
1386            ("b2", "second batch item"),
1387            ("b3", "third batch item"),
1388        ];
1389        for (id, text) in &items {
1390            let vec = hms.encode_text(text);
1391            hms.memorize(id.to_string(), vec).unwrap();
1392        }
1393        assert_eq!(hms.vector_count(), 3);
1394
1395        let q = hms.encode_text("first batch item");
1396        let results = hms.query(&q, 3);
1397        assert!(!results.is_empty());
1398    }
1399
1400    // === Custom Config ===
1401
1402    #[test]
1403    fn test_custom_concept_config() {
1404        let dir = tempfile::tempdir().unwrap();
1405        let mut config = crate::core::config::HmsConfig::default();
1406        config.concepts.similarity_threshold = 0.5;
1407        config.concepts.min_cluster_size = 5;
1408
1409        let hms = HmsCore::new(
1410            10_000,
1411            Some(dir.path().to_string_lossy().to_string()),
1412            Some(config),
1413        )
1414        .unwrap();
1415
1416        // With high threshold and min size, clusters are harder to form
1417        for i in 0..10 {
1418            let vec = hms.encode_text(&format!("config test document {}", i));
1419            hms.memorize(format!("cfg_{}", i), vec).unwrap();
1420        }
1421
1422        let concepts = hms.synthesize_concepts();
1423        // With strict thresholds, fewer or no concepts should form
1424        // (this validates the config is actually used)
1425        for c in &concepts {
1426            assert!(
1427                c.member_count >= 5,
1428                "Min cluster size should be 5, got {}",
1429                c.member_count
1430            );
1431        }
1432    }
1433
1434    // === Diffusion factorizer ===
1435
1436    #[test]
1437    fn test_factorize_returns_factors() {
1438        let dir = tempfile::tempdir().unwrap();
1439        let hms =
1440            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1441
1442        // Memorize some domain items
1443        let colors = vec!["red", "blue", "green"];
1444        let shapes = vec!["circle", "square", "triangle"];
1445        for c in &colors {
1446            let v = hms.encode_text(c);
1447            hms.memorize(c.to_string(), v).unwrap();
1448        }
1449        for s in &shapes {
1450            let v = hms.encode_text(s);
1451            hms.memorize(s.to_string(), v).unwrap();
1452        }
1453
1454        // Create a composite: red * circle
1455        let red_vec = hms.encode_text("red");
1456        let circle_vec = hms.encode_text("circle");
1457        let product = red_vec.bind(&circle_vec);
1458
1459        let domains = vec![
1460            colors
1461                .iter()
1462                .map(|s| hms.encode_text(s))
1463                .collect::<Vec<EntangledHVec>>(),
1464            shapes
1465                .iter()
1466                .map(|s| hms.encode_text(s))
1467                .collect::<Vec<EntangledHVec>>(),
1468        ];
1469
1470        let results = hms.factorize_diffusion(&product, &domains, 20);
1471        assert_eq!(results.len(), 2);
1472    }
1473
1474    #[test]
1475    fn test_concept_synthesis() {
1476        let dir = tempfile::tempdir().unwrap();
1477        let hms = HmsCore::new(1000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1478
1479        // Create 20 slightly-varied versions of a "base" concept
1480        // Use the same seed with small variations to create tight clusters
1481        let base = hms.encode_text("base concept");
1482        for i in 0..20 {
1483            // Create variants by permuting base slightly
1484            let variant = if i == 0 {
1485                base.clone()
1486            } else {
1487                // Bind with a "small" perturbation (most indices shared)
1488                let perturb = EntangledHVec::new_deterministic(1000, 10000 + i);
1489                // Bundle many copies of base with one perturbation to stay close
1490                EntangledHVec::bundle(&[
1491                    base.clone(),
1492                    base.clone(),
1493                    base.clone(),
1494                    base.clone(),
1495                    perturb,
1496                ])
1497            };
1498            hms.memorize(format!("var_{}", i), variant).unwrap();
1499        }
1500
1501        let concepts = hms.synthesize_concepts();
1502        // We expect at least one synthesized concept representing the cluster
1503        assert!(
1504            !concepts.is_empty(),
1505            "Should synthesize at least one concept"
1506        );
1507        assert!(
1508            concepts[0].coherence > 0.7,
1509            "Synthesized concept should have high coherence, got {}",
1510            concepts[0].coherence
1511        );
1512    }
1513
1514    #[test]
1515    fn test_concept_refinement_stability() {
1516        let dir = tempfile::tempdir().unwrap();
1517        let mut config = crate::core::config::HmsConfig::default();
1518        config.concepts.similarity_threshold = 0.3;
1519        config.concepts.min_cluster_size = 3;
1520        let hms = HmsCore::new(
1521            1000,
1522            Some(dir.path().to_string_lossy().to_string()),
1523            Some(config),
1524        )
1525        .unwrap();
1526
1527        // Create two distinct clusters so refinement has work to do
1528        let base_a = hms.encode_text("alpha cluster topic");
1529        let base_b = hms.encode_text("beta different subject");
1530        for i in 0..10 {
1531            let perturb = EntangledHVec::new_deterministic(1000, 20000 + i);
1532            let va = EntangledHVec::bundle(&[
1533                base_a.clone(),
1534                base_a.clone(),
1535                base_a.clone(),
1536                base_a.clone(),
1537                perturb.clone(),
1538            ]);
1539            hms.memorize(format!("a_{}", i), va).unwrap();
1540
1541            let perturb_b = EntangledHVec::new_deterministic(1000, 30000 + i);
1542            let vb = EntangledHVec::bundle(&[
1543                base_b.clone(),
1544                base_b.clone(),
1545                base_b.clone(),
1546                base_b.clone(),
1547                perturb_b,
1548            ]);
1549            hms.memorize(format!("b_{}", i), vb).unwrap();
1550        }
1551
1552        let concepts = hms.synthesize_concepts();
1553        assert!(
1554            !concepts.is_empty(),
1555            "Should synthesize at least one concept"
1556        );
1557        // With tight clusters, refinement should converge (stable = true)
1558        for c in &concepts {
1559            assert!(
1560                c.stable,
1561                "Concept with {} members should be stable after refinement",
1562                c.member_count
1563            );
1564            assert!(
1565                c.coherence > 0.5,
1566                "Coherence should be decent after refinement, got {}",
1567                c.coherence
1568            );
1569        }
1570    }
1571
1572    // === NSG Integration Tests ===
1573
1574    #[test]
1575    fn test_nsg_train_and_query() {
1576        let dir = tempfile::tempdir().unwrap();
1577        let mut config = crate::core::config::HmsConfig::default();
1578        config.nsg.max_degree = 8;
1579
1580        config.nsg.ef_construction = 16;
1581
1582        let hms = HmsCore::new(
1583            1000,
1584            Some(dir.path().to_string_lossy().to_string()),
1585            Some(config),
1586        )
1587        .unwrap();
1588
1589        for i in 0..50 {
1590            let vec = hms.encode_text(&format!("nsg document {}", i));
1591            hms.memorize(format!("nsg_{}", i), vec).unwrap();
1592        }
1593
1594        assert!(!hms.nsg_trained());
1595        hms.train_nsg().unwrap();
1596        assert!(hms.nsg_trained());
1597
1598        let q = hms.encode_text("nsg document 0");
1599        let results = hms.query(&q, 5);
1600        assert!(!results.is_empty(), "NSG query should return results");
1601    }
1602
1603    #[test]
1604    fn test_nsg_maintenance_at_threshold() {
1605        let dir = tempfile::tempdir().unwrap();
1606        let mut config = crate::core::config::HmsConfig::default();
1607        config.nsg.auto_threshold = 30;
1608        config.nsg.max_degree = 8;
1609
1610        config.nsg.ef_construction = 16;
1611
1612        let hms = HmsCore::new(
1613            1000,
1614            Some(dir.path().to_string_lossy().to_string()),
1615            Some(config),
1616        )
1617        .unwrap();
1618
1619        for i in 0..29 {
1620            let vec = hms.encode_text(&format!("auto nsg {}", i));
1621            hms.memorize(format!("ansg_{}", i), vec).unwrap();
1622        }
1623        assert!(!hms.nsg_trained());
1624
1625        let vec = hms.encode_text("auto nsg 29");
1626        hms.memorize("ansg_29".to_string(), vec).unwrap();
1627        assert!(!hms.nsg_trained(), "ingestion must not synchronously train");
1628        assert!(hms.index_status().nsg_training_recommended);
1629        hms.maintain_indices().unwrap();
1630        assert!(hms.nsg_trained());
1631    }
1632
1633    // === Phase 4 Integration Tests ===
1634
1635    #[test]
1636    fn test_adaptive_routing_e2e() {
1637        let dir = tempfile::tempdir().unwrap();
1638        let mut config = crate::core::config::HmsConfig::default();
1639        config.nsg.max_degree = 8;
1640
1641        config.nsg.ef_construction = 16;
1642
1643        let hms = HmsCore::new(
1644            1000,
1645            Some(dir.path().to_string_lossy().to_string()),
1646            Some(config),
1647        )
1648        .unwrap();
1649
1650        for i in 0..50 {
1651            let vec = hms.encode_text(&format!("routing item {}", i));
1652            hms.memorize(format!("rt_{}", i), vec).unwrap();
1653        }
1654
1655        // Train NSG — queries should now route through NSG
1656        hms.train_nsg().unwrap();
1657        assert!(hms.nsg_trained());
1658
1659        let q = hms.encode_text("routing item 0");
1660        let results = hms.query(&q, 5);
1661        assert!(
1662            !results.is_empty(),
1663            "Adaptive routing should return results via NSG"
1664        );
1665    }
1666
1667    // === Knowledge Graph Tests ===
1668
1669    #[test]
1670    fn test_triplet_memorize_and_query() {
1671        let dir = tempfile::tempdir().unwrap();
1672        let hms =
1673            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1674
1675        hms.memorize_triplet(
1676            "paris_capital".to_string(),
1677            "Paris".to_string(),
1678            "is_capital_of".to_string(),
1679            "France".to_string(),
1680        )
1681        .unwrap();
1682        hms.memorize_triplet(
1683            "berlin_capital".to_string(),
1684            "Berlin".to_string(),
1685            "is_capital_of".to_string(),
1686            "Germany".to_string(),
1687        )
1688        .unwrap();
1689        hms.memorize_triplet(
1690            "tokyo_capital".to_string(),
1691            "Tokyo".to_string(),
1692            "is_capital_of".to_string(),
1693            "Japan".to_string(),
1694        )
1695        .unwrap();
1696
1697        let results = hms
1698            .query_triplet("Paris".to_string(), "is_capital_of".to_string(), 3)
1699            .unwrap();
1700        assert!(!results.is_empty(), "Triplet query should return results");
1701        assert!(
1702            results.iter().any(|r| r.id == "paris_capital"),
1703            "Paris triplet should appear in top-3 results"
1704        );
1705    }
1706
1707    // === Sequence Tests ===
1708
1709    #[test]
1710    fn test_sequence_memorize_and_query() {
1711        let dir = tempfile::tempdir().unwrap();
1712        let hms =
1713            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1714
1715        hms.memorize_sequence(
1716            "recipe_1".to_string(),
1717            &[
1718                "preheat oven".to_string(),
1719                "mix ingredients".to_string(),
1720                "pour into pan".to_string(),
1721                "bake for thirty minutes".to_string(),
1722            ],
1723        )
1724        .unwrap();
1725        hms.memorize_sequence(
1726            "recipe_2".to_string(),
1727            &[
1728                "boil water".to_string(),
1729                "add pasta".to_string(),
1730                "drain and serve".to_string(),
1731            ],
1732        )
1733        .unwrap();
1734
1735        // Query with a partial sequence match
1736        let q = hms.encode_text("preheat oven").permute(0);
1737        let results = hms.query(&q, 2);
1738        assert!(!results.is_empty(), "Sequence query should return results");
1739    }
1740
1741    // === Scalar Query Tests ===
1742
1743    #[test]
1744    fn test_scalar_query_ordering() {
1745        let dir = tempfile::tempdir().unwrap();
1746        let hms =
1747            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1748
1749        for i in 0..20 {
1750            let val = i as f64 * 5.0;
1751            hms.memorize_scalar(format!("temp_{}", i), val, 0.0, 100.0)
1752                .unwrap();
1753        }
1754
1755        // Query near value 50 — should return items closest to 50
1756        let q = EntangledHVec::from_scalar(50.0, 0.0, 100.0, 10_000);
1757        let results = hms.query(&q, 5);
1758        assert!(!results.is_empty(), "Scalar query should return results");
1759
1760        // Top results should cluster around value 50 (idx 10)
1761        let top_idx: usize = results[0]
1762            .id
1763            .strip_prefix("temp_")
1764            .unwrap()
1765            .parse()
1766            .unwrap();
1767        assert!(
1768            (5..=15).contains(&top_idx),
1769            "Top scalar result should be near value 50 (idx 10), got idx {}",
1770            top_idx
1771        );
1772    }
1773
1774    // === Component Analysis Tests ===
1775
1776    #[test]
1777    fn test_analyze_components() {
1778        let dir = tempfile::tempdir().unwrap();
1779        let hms =
1780            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1781
1782        for i in 0..30 {
1783            let vec = hms.encode_text(&format!("component analysis document {}", i));
1784            hms.memorize(format!("ca_{}", i), vec).unwrap();
1785        }
1786
1787        let vec = hms.encode_text("component analysis document 0");
1788        let results = hms.analyze_components(&vec);
1789        assert!(
1790            !results.is_empty(),
1791            "analyze_components should return results"
1792        );
1793        assert!(
1794            results.iter().all(|r| r.similarity > 0.05),
1795            "All results should exceed similarity threshold"
1796        );
1797    }
1798
1799    // === Delete Tests ===
1800
1801    #[test]
1802    fn test_delete_existing() {
1803        let dir = tempfile::tempdir().unwrap();
1804        let hms =
1805            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1806
1807        let vec = hms.encode_text("hello");
1808        hms.memorize("hello".to_string(), vec).unwrap();
1809        assert_eq!(hms.vector_count(), 1);
1810
1811        assert!(hms.delete("hello").unwrap());
1812        assert_eq!(hms.vector_count(), 0);
1813
1814        let q = hms.encode_text("hello");
1815        let results = hms.query(&q, 5);
1816        assert!(
1817            results.is_empty(),
1818            "Deleted vector should not appear in results"
1819        );
1820    }
1821
1822    #[test]
1823    fn test_delete_nonexistent() {
1824        let dir = tempfile::tempdir().unwrap();
1825        let hms = HmsCore::new(10_000, Some(dir.path().display().to_string()), None).unwrap();
1826        assert!(!hms.delete("no_such_id").unwrap());
1827    }
1828
1829    #[test]
1830    fn test_basic_persistence() {
1831        let dir = tempfile::tempdir().unwrap();
1832        let path = dir.path().to_string_lossy().to_string();
1833
1834        {
1835            let hms = HmsCore::new(10_000, Some(path.clone()), None).unwrap();
1836            let v = hms.encode_text("persist me");
1837            hms.memorize("p1".to_string(), v).unwrap();
1838            assert_eq!(hms.vector_count(), 1);
1839        }
1840
1841        let hms = HmsCore::new(10_000, Some(path), None).unwrap();
1842        assert_eq!(hms.vector_count(), 1);
1843    }
1844
1845    #[test]
1846    fn test_delete_persistence() {
1847        let dir = tempfile::tempdir().unwrap();
1848        let path = dir.path().to_string_lossy().to_string();
1849
1850        {
1851            let hms = HmsCore::new(10_000, Some(path.clone()), None).unwrap();
1852            let v1 = hms.encode_text("keep me");
1853            let v2 = hms.encode_text("delete me");
1854            hms.memorize("keep".to_string(), v1).unwrap();
1855            hms.memorize("del".to_string(), v2).unwrap();
1856            assert_eq!(hms.vector_count(), 2);
1857            hms.delete("del").unwrap();
1858            assert_eq!(hms.vector_count(), 1);
1859        }
1860
1861        let hms = HmsCore::new(10_000, Some(path), None).unwrap();
1862        assert_eq!(hms.vector_count(), 1);
1863        let q = hms.encode_text("keep me");
1864        let results = hms.query(&q, 5);
1865        assert!(!results.is_empty(), "Kept vector should survive restart");
1866    }
1867
1868    #[test]
1869    fn test_delete_and_rememorize() {
1870        let dir = tempfile::tempdir().unwrap();
1871        let hms =
1872            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1873
1874        let v1 = hms.encode_text("version 1");
1875        hms.memorize("doc".to_string(), v1).unwrap();
1876        hms.delete("doc").unwrap();
1877
1878        let v2 = hms.encode_text("version 2");
1879        hms.memorize("doc".to_string(), v2.clone()).unwrap();
1880        assert_eq!(hms.vector_count(), 1);
1881
1882        let q = hms.encode_text("version 2");
1883        let results = hms.query(&q, 1);
1884        assert_eq!(results[0].id, "doc");
1885    }
1886
1887    // === Compact Tests ===
1888
1889    #[test]
1890    fn test_compact_basic() {
1891        let dir = tempfile::tempdir().unwrap();
1892        let path = dir.path().to_string_lossy().to_string();
1893
1894        let hms = HmsCore::new(10_000, Some(path.clone()), None).unwrap();
1895        for i in 0..50 {
1896            let vec = hms.encode_text(&format!("item {}", i));
1897            hms.memorize(format!("id_{}", i), vec).unwrap();
1898        }
1899        // Delete half
1900        for i in 0..25 {
1901            hms.delete(&format!("id_{}", i)).unwrap();
1902        }
1903        assert_eq!(hms.vector_count(), 25);
1904
1905        hms.compact().unwrap();
1906
1907        // Verify all live vectors still queryable
1908        let q = hms.encode_text("item 30");
1909        let results = hms.query(&q, 5);
1910        assert!(!results.is_empty(), "Should find results after compaction");
1911        assert_eq!(hms.vector_count(), 25);
1912    }
1913
1914    #[test]
1915    fn test_compact_persistence() {
1916        let dir = tempfile::tempdir().unwrap();
1917        let path = dir.path().to_string_lossy().to_string();
1918
1919        {
1920            let hms = HmsCore::new(10_000, Some(path.clone()), None).unwrap();
1921            for i in 0..20 {
1922                let vec = hms.encode_text(&format!("doc {}", i));
1923                hms.memorize(format!("d_{}", i), vec).unwrap();
1924            }
1925            for i in 0..10 {
1926                hms.delete(&format!("d_{}", i)).unwrap();
1927            }
1928            hms.compact().unwrap();
1929        }
1930
1931        // Re-open from compacted arena
1932        let hms = HmsCore::new(10_000, Some(path), None).unwrap();
1933        assert_eq!(hms.vector_count(), 10);
1934    }
1935
1936    // === Shard Tests ===
1937
1938    #[test]
1939    fn test_multi_shard_insert_query() {
1940        let dir = tempfile::tempdir().unwrap();
1941        let mut config = crate::core::config::HmsConfig::default();
1942        config.shard.enabled = true;
1943        config.shard.shard_count = 4;
1944
1945        let hms = HmsCore::new(
1946            10_000,
1947            Some(dir.path().to_string_lossy().to_string()),
1948            Some(config),
1949        )
1950        .unwrap();
1951
1952        for i in 0..100 {
1953            let vec = hms.encode_text(&format!("shard document {}", i));
1954            hms.memorize(format!("sd_{}", i), vec).unwrap();
1955        }
1956
1957        assert_eq!(hms.vector_count(), 100);
1958
1959        let q = hms.encode_text("shard document 0");
1960        let results = hms.query(&q, 5);
1961        assert!(
1962            !results.is_empty(),
1963            "Multi-shard query should return results"
1964        );
1965    }
1966
1967    #[test]
1968    fn test_multi_shard_delete() {
1969        let dir = tempfile::tempdir().unwrap();
1970        let mut config = crate::core::config::HmsConfig::default();
1971        config.shard.enabled = true;
1972        config.shard.shard_count = 4;
1973
1974        let hms = HmsCore::new(
1975            10_000,
1976            Some(dir.path().to_string_lossy().to_string()),
1977            Some(config),
1978        )
1979        .unwrap();
1980
1981        for i in 0..20 {
1982            let vec = hms.encode_text(&format!("item {}", i));
1983            hms.memorize(format!("m_{}", i), vec).unwrap();
1984        }
1985        assert_eq!(hms.vector_count(), 20);
1986
1987        for i in 0..10 {
1988            assert!(hms.delete(&format!("m_{}", i)).unwrap());
1989        }
1990        assert_eq!(hms.vector_count(), 10);
1991    }
1992
1993    #[test]
1994    fn test_auto_shard_trigger() {
1995        let dir = tempfile::tempdir().unwrap();
1996        let mut config = crate::core::config::HmsConfig::default();
1997        config.shard.enabled = true;
1998        config.shard.shard_count = 0; // auto
1999        config.shard.auto_threshold = 50;
2000        config.shard.target_shard_size = 25;
2001
2002        let hms = HmsCore::new(
2003            10_000,
2004            Some(dir.path().to_string_lossy().to_string()),
2005            Some(config),
2006        )
2007        .unwrap();
2008
2009        for i in 0..50 {
2010            let vec = hms.encode_text(&format!("auto shard {}", i));
2011            hms.memorize(format!("as_{}", i), vec).unwrap();
2012        }
2013
2014        assert_eq!(hms.vector_count(), 50);
2015
2016        hms.maintain_indices().unwrap();
2017        let metadata: serde_json::Value =
2018            serde_json::from_slice(&std::fs::read(dir.path().join("shard_meta.json")).unwrap())
2019                .unwrap();
2020        assert_eq!(metadata["shard_count"], 2);
2021
2022        // Verify queries still work after auto-sharding
2023        let q = hms.encode_text("auto shard 0");
2024        let results = hms.query(&q, 5);
2025        assert!(!results.is_empty(), "Should find results after auto-shard");
2026    }
2027
2028    // === Audit Integration Tests ===
2029
2030    #[test]
2031    fn test_audit_records_operations() {
2032        let dir = tempfile::tempdir().unwrap();
2033        let mut config = crate::core::config::HmsConfig::default();
2034        config.security.audit_enabled = true;
2035
2036        let hms = HmsCore::new(
2037            10_000,
2038            Some(dir.path().to_string_lossy().to_string()),
2039            Some(config),
2040        )
2041        .unwrap();
2042
2043        let v = hms.encode_text("audit test");
2044        hms.memorize("aud_1".to_string(), v).unwrap();
2045        hms.delete("aud_1").unwrap();
2046
2047        let entries = hms.audit_since(0).unwrap();
2048        assert_eq!(entries.len(), 2);
2049        assert_eq!(entries[0].op, crate::core::audit::AuditOp::Memorize);
2050        assert_eq!(entries[1].op, crate::core::audit::AuditOp::Delete);
2051    }
2052
2053    #[test]
2054    fn test_audit_disabled_returns_empty() {
2055        let dir = tempfile::tempdir().unwrap();
2056        let hms = HmsCore::new(10_000, Some(dir.path().display().to_string()), None).unwrap();
2057        let entries = hms.audit_since(0).unwrap();
2058        assert!(entries.is_empty());
2059    }
2060
2061    #[test]
2062    fn test_audit_compact_recorded() {
2063        let dir = tempfile::tempdir().unwrap();
2064        let mut config = crate::core::config::HmsConfig::default();
2065        config.security.audit_enabled = true;
2066
2067        let hms = HmsCore::new(
2068            10_000,
2069            Some(dir.path().to_string_lossy().to_string()),
2070            Some(config),
2071        )
2072        .unwrap();
2073
2074        let v = hms.encode_text("compact audit");
2075        hms.memorize("ca_1".to_string(), v).unwrap();
2076        hms.compact().unwrap();
2077
2078        let entries = hms.audit_since(0).unwrap();
2079        assert!(entries
2080            .iter()
2081            .any(|e| e.op == crate::core::audit::AuditOp::Compact));
2082    }
2083
2084    // === Graph Integration Tests ===
2085
2086    #[test]
2087    fn test_graph_add_and_traverse() {
2088        let dir = tempfile::tempdir().unwrap();
2089        let hms =
2090            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
2091
2092        // Memorize some nodes
2093        for city in &["paris", "france", "europe"] {
2094            let v = hms.encode_text(city);
2095            hms.memorize(city.to_string(), v).unwrap();
2096        }
2097
2098        hms.add_relation(&crate::core::types::Relation {
2099            source_id: "paris".into(),
2100            relation_type: "is_in".into(),
2101            target_id: "france".into(),
2102            properties: None,
2103            valid_from: 0.0,
2104            valid_to: 0.0,
2105        })
2106        .unwrap();
2107        hms.add_relation(&crate::core::types::Relation {
2108            source_id: "france".into(),
2109            relation_type: "is_in".into(),
2110            target_id: "europe".into(),
2111            properties: None,
2112            valid_from: 0.0,
2113            valid_to: 0.0,
2114        })
2115        .unwrap();
2116
2117        assert_eq!(hms.relation_count(), 2);
2118
2119        let paths = hms.traverse("paris", Some("is_in"), 3, 0.0);
2120        assert!(!paths.is_empty());
2121        let targets: Vec<&str> = paths
2122            .iter()
2123            .flat_map(|p| p.hops.iter().map(|h| h.node_id.as_str()))
2124            .collect();
2125        assert!(targets.contains(&"france"));
2126        assert!(targets.contains(&"europe"));
2127    }
2128
2129    #[test]
2130    fn test_graph_transitive_inference() {
2131        let dir = tempfile::tempdir().unwrap();
2132        let hms =
2133            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
2134
2135        for name in &["a", "b", "c"] {
2136            let v = hms.encode_text(name);
2137            hms.memorize(name.to_string(), v).unwrap();
2138        }
2139
2140        hms.declare_relation_type(crate::core::types::RelationType {
2141            name: "contains".into(),
2142            transitive: true,
2143            symmetric: false,
2144        })
2145        .unwrap();
2146
2147        hms.add_relation(&crate::core::types::Relation {
2148            source_id: "a".into(),
2149            relation_type: "contains".into(),
2150            target_id: "b".into(),
2151            properties: None,
2152            valid_from: 0.0,
2153            valid_to: 0.0,
2154        })
2155        .unwrap();
2156        hms.add_relation(&crate::core::types::Relation {
2157            source_id: "b".into(),
2158            relation_type: "contains".into(),
2159            target_id: "c".into(),
2160            properties: None,
2161            valid_from: 0.0,
2162            valid_to: 0.0,
2163        })
2164        .unwrap();
2165
2166        let paths = hms.traverse("a", Some("contains"), 3, 0.0);
2167        // Should have inferred single-hop a->c
2168        let inferred = paths
2169            .iter()
2170            .find(|p| p.hops.len() == 1 && p.hops[0].node_id == "c");
2171        assert!(inferred.is_some(), "Should infer transitive a->c");
2172    }
2173
2174    #[test]
2175    fn test_graph_persistence() {
2176        let dir = tempfile::tempdir().unwrap();
2177        let path = dir.path().to_string_lossy().to_string();
2178
2179        {
2180            let hms = HmsCore::new(10_000, Some(path.clone()), None).unwrap();
2181            let v = hms.encode_text("node_a");
2182            hms.memorize("node_a".to_string(), v).unwrap();
2183            hms.add_relation(&crate::core::types::Relation {
2184                source_id: "node_a".into(),
2185                relation_type: "links_to".into(),
2186                target_id: "node_b".into(),
2187                properties: None,
2188                valid_from: 0.0,
2189                valid_to: 0.0,
2190            })
2191            .unwrap();
2192        }
2193
2194        let hms = HmsCore::new(10_000, Some(path), None).unwrap();
2195        assert_eq!(hms.relation_count(), 1);
2196        let out = hms.outgoing_relations("node_a", None, 0.0);
2197        assert_eq!(out.len(), 1);
2198        assert_eq!(out[0].target_id, "node_b");
2199    }
2200
2201    #[test]
2202    fn test_graph_temporal_query() {
2203        let dir = tempfile::tempdir().unwrap();
2204        let hms =
2205            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
2206
2207        hms.add_relation(&crate::core::types::Relation {
2208            source_id: "alice".into(),
2209            relation_type: "works_at".into(),
2210            target_id: "acme".into(),
2211            properties: None,
2212            valid_from: 1000.0,
2213            valid_to: 2000.0,
2214        })
2215        .unwrap();
2216        hms.add_relation(&crate::core::types::Relation {
2217            source_id: "alice".into(),
2218            relation_type: "works_at".into(),
2219            target_id: "globex".into(),
2220            properties: None,
2221            valid_from: 2001.0,
2222            valid_to: 0.0,
2223        })
2224        .unwrap();
2225
2226        let at_1500 = hms.outgoing_relations("alice", Some("works_at"), 1500.0);
2227        assert_eq!(at_1500.len(), 1);
2228        assert_eq!(at_1500[0].target_id, "acme");
2229
2230        let at_3000 = hms.outgoing_relations("alice", Some("works_at"), 3000.0);
2231        assert_eq!(at_3000.len(), 1);
2232        assert_eq!(at_3000[0].target_id, "globex");
2233    }
2234
2235    #[test]
2236    fn test_federated_query() {
2237        let dir1 = tempfile::tempdir().unwrap();
2238        let dir2 = tempfile::tempdir().unwrap();
2239        let path1 = dir1.path().to_string_lossy().to_string();
2240        let path2 = dir2.path().to_string_lossy().to_string();
2241
2242        // Populate two separate instances
2243        {
2244            let hms1 = HmsCore::new(10_000, Some(path1.clone()), None).unwrap();
2245            let v = hms1.encode_text("federated doc alpha");
2246            hms1.memorize("alpha".to_string(), v).unwrap();
2247        }
2248        {
2249            let hms2 = HmsCore::new(10_000, Some(path2.clone()), None).unwrap();
2250            let v = hms2.encode_text("federated doc beta");
2251            hms2.memorize("beta".to_string(), v).unwrap();
2252        }
2253
2254        // Query from instance 1, federating with instance 2
2255        let hms1 = HmsCore::new(10_000, Some(path1), None).unwrap();
2256        let q = hms1.encode_text("federated doc");
2257        let results = hms1.federated_query(&[path2], &q, 5).unwrap();
2258        let ids: Vec<&str> = results.iter().map(|r| r.id.as_str()).collect();
2259        assert!(ids.contains(&"alpha"), "Should find local result");
2260        assert!(ids.contains(&"beta"), "Should find federated result");
2261    }
2262
2263    #[test]
2264    fn test_hopfield_query_e2e() {
2265        let dir = tempfile::tempdir().unwrap();
2266        let hms =
2267            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
2268
2269        let topics = [
2270            "quantum physics experiments in laboratories",
2271            "medieval history of european kingdoms",
2272            "tropical marine biology and coral reefs",
2273            "classical piano compositions by beethoven",
2274            "volcanic geology and tectonic plate movements",
2275            "abstract algebra and group theory proofs",
2276            "impressionist painting techniques with oils",
2277            "arctic wildlife migration patterns observed",
2278            "distributed computing consensus algorithms",
2279            "ancient roman architecture and aqueducts",
2280        ];
2281        for (i, topic) in topics.iter().enumerate() {
2282            let v = hms.encode_text(topic);
2283            hms.memorize(format!("doc_{}", i), v).unwrap();
2284        }
2285
2286        let q = hms.encode_text("volcanic geology and tectonic plate movements");
2287        let results = hms.query_hopfield(&q, 10);
2288        assert!(!results.is_empty(), "Hopfield query should return results");
2289        let top3_ids: Vec<&str> = results.iter().take(3).map(|r| r.id.as_str()).collect();
2290        assert!(
2291            top3_ids.contains(&"doc_4"),
2292            "Exact match should be in top-3 Hopfield results, got {:?}",
2293            top3_ids
2294        );
2295    }
2296
2297    #[test]
2298    fn test_graph_compact_preserves_relations() {
2299        let dir = tempfile::tempdir().unwrap();
2300        let hms =
2301            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
2302
2303        let v = hms.encode_text("compactable");
2304        hms.memorize("node1".to_string(), v).unwrap();
2305        hms.add_relation(&crate::core::types::Relation {
2306            source_id: "node1".into(),
2307            relation_type: "related".into(),
2308            target_id: "node2".into(),
2309            properties: None,
2310            valid_from: 0.0,
2311            valid_to: 0.0,
2312        })
2313        .unwrap();
2314
2315        hms.compact().unwrap();
2316        assert_eq!(hms.relation_count(), 1);
2317        assert_eq!(hms.vector_count(), 1);
2318    }
2319
2320    // === Analogy Tests ===
2321
2322    #[test]
2323    fn test_find_analogy() {
2324        let dir = tempfile::tempdir().unwrap();
2325        let hms =
2326            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
2327
2328        for word in &[
2329            "walking", "talking", "running", "walked", "talked", "runner",
2330        ] {
2331            let v = hms.encode_text(word);
2332            hms.memorize(word.to_string(), v).unwrap();
2333        }
2334
2335        let results = hms.find_analogy("walking", "walked", "talking", 5);
2336        assert!(!results.is_empty(), "Analogy should return results");
2337    }
2338
2339    // === IVF Persistence ===
2340
2341    #[test]
2342    fn test_ivf_index_persistence() {
2343        let dir = tempfile::tempdir().unwrap();
2344        let path = dir.path().to_string_lossy().to_string();
2345
2346        {
2347            let mut config = crate::core::config::HmsConfig::default();
2348            config.ivf.n_clusters = 8;
2349            config.ivf.n_landmarks = 64;
2350            config.ivf.d_reduced = 16;
2351            config.ivf.n_probe = 8;
2352
2353            let hms = HmsCore::new(10_000, Some(path.clone()), Some(config)).unwrap();
2354            for i in 0..200 {
2355                let v = hms.encode_text(&format!("ivf persist {}", i));
2356                hms.memorize(format!("ip_{}", i), v).unwrap();
2357            }
2358            hms.train_ivf().unwrap();
2359            assert!(hms.ivf_trained());
2360        }
2361
2362        let mut config = crate::core::config::HmsConfig::default();
2363        config.ivf.n_clusters = 8;
2364        config.ivf.n_landmarks = 64;
2365        config.ivf.d_reduced = 16;
2366        config.ivf.n_probe = 8;
2367        let hms = HmsCore::new(10_000, Some(path), Some(config)).unwrap();
2368        assert!(
2369            hms.ivf_trained(),
2370            "IVF should be loaded from disk on re-open"
2371        );
2372        let q = hms.encode_text("ivf persist 0");
2373        let results = hms.query(&q, 5);
2374        assert!(!results.is_empty(), "IVF query should work after reload");
2375    }
2376
2377    // === Query Batch Tests ===
2378
2379    #[test]
2380    fn test_query_batch() {
2381        let dir = tempfile::tempdir().unwrap();
2382        let hms =
2383            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
2384
2385        for i in 0..20 {
2386            let v = hms.encode_text(&format!("batch query doc {}", i));
2387            hms.memorize(format!("bq_{}", i), v).unwrap();
2388        }
2389
2390        let queries: Vec<EntangledHVec> = (0..3)
2391            .map(|i| hms.encode_text(&format!("batch query doc {}", i)))
2392            .collect();
2393        let batch_results = hms.query_batch(&queries, 3);
2394        assert_eq!(
2395            batch_results.len(),
2396            3,
2397            "Should return one result set per query"
2398        );
2399        for results in &batch_results {
2400            assert!(!results.is_empty(), "Each query should return results");
2401        }
2402    }
2403
2404    // === Memorize Vector Tests ===
2405
2406    #[test]
2407    fn test_memorize_vector_through_core() {
2408        let dir = tempfile::tempdir().unwrap();
2409        let hms =
2410            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
2411
2412        let dense: Vec<f32> = (0..128).map(|i| (i as f32 - 64.0) / 64.0).collect();
2413        hms.memorize_vector("dense_1".to_string(), &dense).unwrap();
2414        assert_eq!(hms.vector_count(), 1);
2415
2416        let q = EntangledHVec::from_dense(&dense, 10_000);
2417        let results = hms.query(&q, 1);
2418        assert_eq!(results[0].id, "dense_1");
2419    }
2420
2421    // === Readability Integration ===
2422
2423    #[test]
2424    fn test_readability_through_core() {
2425        let dir = tempfile::tempdir().unwrap();
2426        let hms = HmsCore::new(10_000, Some(dir.path().display().to_string()), None).unwrap();
2427        let metrics = hms.analyze_text("The cat sat on the mat.");
2428        assert!(metrics.word_count > 0);
2429        let score = hms.calculate_readability(&metrics);
2430        assert!(
2431            score > 50.0,
2432            "Simple sentence should be highly readable (got {:.1})",
2433            score
2434        );
2435    }
2436
2437    // === Index Persistence Tests ===
2438
2439    #[test]
2440    fn test_nsg_index_persistence() {
2441        let dir = tempfile::tempdir().unwrap();
2442        let path = dir.path().to_string_lossy().to_string();
2443
2444        {
2445            let mut config = crate::core::config::HmsConfig::default();
2446            config.nsg.max_degree = 8;
2447            config.nsg.ef_construction = 16;
2448
2449            let hms = HmsCore::new(10_000, Some(path.clone()), Some(config)).unwrap();
2450            for i in 0..50 {
2451                let v = hms.encode_text(&format!("persist nsg {}", i));
2452                hms.memorize(format!("pn_{}", i), v).unwrap();
2453            }
2454            hms.train_nsg().unwrap();
2455            assert!(hms.nsg_trained());
2456        }
2457
2458        // Re-open and verify NSG was reloaded
2459        let mut config = crate::core::config::HmsConfig::default();
2460        config.nsg.max_degree = 8;
2461        config.nsg.ef_construction = 16;
2462        let hms = HmsCore::new(10_000, Some(path), Some(config)).unwrap();
2463        assert!(
2464            hms.nsg_trained(),
2465            "NSG should be loaded from disk on re-open"
2466        );
2467        let q = hms.encode_text("persist nsg 0");
2468        let results = hms.query(&q, 5);
2469        assert!(!results.is_empty(), "NSG query should work after reload");
2470    }
2471
2472    // === NSG Accuracy ===
2473
2474    #[test]
2475    fn test_nsg_accuracy_matches_brute_force() {
2476        let dir = tempfile::tempdir().unwrap();
2477        let mut config = crate::core::config::HmsConfig::default();
2478        config.nsg.max_degree = 8;
2479        config.nsg.ef_construction = 16;
2480
2481        let hms = HmsCore::new(
2482            10_000,
2483            Some(dir.path().to_string_lossy().to_string()),
2484            Some(config),
2485        )
2486        .unwrap();
2487
2488        for i in 0..50 {
2489            let v = hms.encode_text(&format!("accuracy test {}", i));
2490            hms.memorize(format!("acc_{}", i), v).unwrap();
2491        }
2492
2493        // Query before training (brute force)
2494        let q = hms.encode_text("accuracy test 0");
2495        let brute_results = hms.query(&q, 1);
2496
2497        hms.train_nsg().unwrap();
2498
2499        // Query after training (NSG)
2500        let nsg_results = hms.query(&q, 1);
2501
2502        assert_eq!(
2503            brute_results[0].id, nsg_results[0].id,
2504            "NSG top-1 should match brute-force top-1"
2505        );
2506    }
2507
2508    // === Multi-Shard Merge Verification ===
2509
2510    #[test]
2511    fn test_multi_shard_results_from_multiple_shards() {
2512        let dir = tempfile::tempdir().unwrap();
2513        let mut config = crate::core::config::HmsConfig::default();
2514        config.shard.enabled = true;
2515        config.shard.shard_count = 4;
2516
2517        let hms = HmsCore::new(
2518            10_000,
2519            Some(dir.path().to_string_lossy().to_string()),
2520            Some(config),
2521        )
2522        .unwrap();
2523
2524        for i in 0..100 {
2525            let v = hms.encode_text(&format!("multi verify {}", i));
2526            hms.memorize(format!("mv_{}", i), v).unwrap();
2527        }
2528
2529        let q = hms.encode_text("multi verify");
2530        let results = hms.query(&q, 20);
2531        assert!(
2532            results.len() >= 10,
2533            "Should return many results from across shards"
2534        );
2535
2536        // Verify results come from at least 2 different shards
2537        use fxhash::FxHasher;
2538        use std::hash::Hasher;
2539        let mut shard_ids: std::collections::HashSet<usize> = std::collections::HashSet::new();
2540        for r in &results {
2541            let mut hasher = FxHasher::default();
2542            hasher.write(r.id.as_bytes());
2543            shard_ids.insert((hasher.finish() as usize) % 4);
2544        }
2545        assert!(
2546            shard_ids.len() >= 2,
2547            "Results should come from multiple shards (got {} distinct shards)",
2548            shard_ids.len()
2549        );
2550    }
2551
2552    // === Compact Multi-Shard ===
2553
2554    #[test]
2555    fn test_compact_multi_shard() {
2556        let dir = tempfile::tempdir().unwrap();
2557        let mut config = crate::core::config::HmsConfig::default();
2558        config.shard.enabled = true;
2559        config.shard.shard_count = 2;
2560
2561        let hms = HmsCore::new(
2562            10_000,
2563            Some(dir.path().to_string_lossy().to_string()),
2564            Some(config),
2565        )
2566        .unwrap();
2567
2568        for i in 0..30 {
2569            let v = hms.encode_text(&format!("compact shard {}", i));
2570            hms.memorize(format!("cs_{}", i), v).unwrap();
2571        }
2572        for i in 0..15 {
2573            hms.delete(&format!("cs_{}", i)).unwrap();
2574        }
2575        assert_eq!(hms.vector_count(), 15);
2576
2577        hms.compact().unwrap();
2578        assert_eq!(hms.vector_count(), 15);
2579
2580        let q = hms.encode_text("compact shard 20");
2581        let results = hms.query(&q, 5);
2582        assert!(
2583            !results.is_empty(),
2584            "Should find results after multi-shard compact"
2585        );
2586    }
2587
2588    // === IVF Integration Tests ===
2589
2590    #[test]
2591    fn test_manual_ivf_train_and_query() {
2592        let dir = tempfile::tempdir().unwrap();
2593        let mut config = crate::core::config::HmsConfig::default();
2594        config.ivf.n_clusters = 8;
2595        config.ivf.n_landmarks = 64;
2596        config.ivf.d_reduced = 16;
2597        config.ivf.n_probe = 8;
2598
2599        let hms = HmsCore::new(
2600            10000,
2601            Some(dir.path().to_string_lossy().to_string()),
2602            Some(config),
2603        )
2604        .unwrap();
2605
2606        for i in 0..200 {
2607            let vec = hms.encode_text(&format!("document number {}", i));
2608            hms.memorize(format!("doc_{}", i), vec).unwrap();
2609        }
2610
2611        assert!(!hms.ivf_trained());
2612        hms.train_ivf().unwrap();
2613        assert!(hms.ivf_trained());
2614
2615        let q = hms.encode_text("document number 0");
2616        let results = hms.query(&q, 10);
2617        assert!(!results.is_empty(), "IVF query should return results");
2618    }
2619
2620    #[test]
2621    fn test_ivf_maintenance_at_threshold() {
2622        let dir = tempfile::tempdir().unwrap();
2623        let mut config = crate::core::config::HmsConfig::default();
2624        config.ivf.enabled = true;
2625        config.ivf.auto_threshold = 50;
2626        config.ivf.n_clusters = 8;
2627        config.ivf.n_landmarks = 64;
2628        config.ivf.d_reduced = 16;
2629        config.ivf.n_probe = 8;
2630
2631        let hms = HmsCore::new(
2632            1000,
2633            Some(dir.path().to_string_lossy().to_string()),
2634            Some(config),
2635        )
2636        .unwrap();
2637
2638        for i in 0..49 {
2639            let vec = hms.encode_text(&format!("auto item {}", i));
2640            hms.memorize(format!("auto_{}", i), vec).unwrap();
2641        }
2642        assert!(!hms.ivf_trained(), "Should not be trained before threshold");
2643
2644        // Reaching the threshold requests maintenance outside ingestion.
2645        let vec = hms.encode_text("auto item 49");
2646        hms.memorize("auto_49".to_string(), vec).unwrap();
2647        assert!(!hms.ivf_trained());
2648        assert!(hms.index_status().ivf_training_recommended);
2649        hms.maintain_indices().unwrap();
2650        assert!(hms.ivf_trained());
2651    }
2652}
2653
2654#[cfg(all(test, feature = "security"))]
2655mod security_integration_tests {
2656    use crate::core::audit::AuditOp;
2657    use crate::core::engine::HmsCore;
2658    use crate::core::security::SigningManager;
2659
2660    #[test]
2661    fn test_signed_audit_entries() {
2662        let dir = tempfile::tempdir().unwrap();
2663        let mut config = crate::core::config::HmsConfig::default();
2664        config.security.signing_enabled = true;
2665        config.security.audit_enabled = true;
2666
2667        let hms = HmsCore::new(
2668            10_000,
2669            Some(dir.path().to_string_lossy().to_string()),
2670            Some(config),
2671        )
2672        .unwrap();
2673
2674        let v = hms.encode_text("signed audit test");
2675        hms.memorize("sig_1".to_string(), v).unwrap();
2676        hms.delete("sig_1").unwrap();
2677
2678        let entries = hms.audit_since(0).unwrap();
2679        assert_eq!(entries.len(), 2);
2680
2681        // All entries should be signed (non-zero signature)
2682        for entry in &entries {
2683            assert_ne!(entry.signature, [0u8; 64], "Entry should be signed");
2684        }
2685
2686        // Verify signatures using the same key
2687        let key_path = dir.path().join("hms_signing.key");
2688        let mgr = SigningManager::new(&key_path).unwrap();
2689        for entry in &entries {
2690            let mut signable = [0u8; 41];
2691            signable[0..8].copy_from_slice(&entry.timestamp_ms.to_le_bytes());
2692            signable[8] = entry.op as u8;
2693            signable[9..41].copy_from_slice(&entry.id_hash);
2694            mgr.verify(&signable, &entry.signature)
2695                .expect("Signature verification failed");
2696        }
2697    }
2698
2699    #[test]
2700    fn test_signed_compact_audit() {
2701        let dir = tempfile::tempdir().unwrap();
2702        let mut config = crate::core::config::HmsConfig::default();
2703        config.security.signing_enabled = true;
2704        config.security.audit_enabled = true;
2705
2706        let hms = HmsCore::new(
2707            10_000,
2708            Some(dir.path().to_string_lossy().to_string()),
2709            Some(config),
2710        )
2711        .unwrap();
2712
2713        let v = hms.encode_text("compact signed");
2714        hms.memorize("cs_1".to_string(), v).unwrap();
2715        hms.compact().unwrap();
2716
2717        let entries = hms.audit_since(0).unwrap();
2718        let compact_entry = entries.iter().find(|e| e.op == AuditOp::Compact).unwrap();
2719        assert_ne!(compact_entry.signature, [0u8; 64]);
2720
2721        let key_path = dir.path().join("hms_signing.key");
2722        let mgr = SigningManager::new(&key_path).unwrap();
2723        let mut signable = [0u8; 41];
2724        signable[0..8].copy_from_slice(&compact_entry.timestamp_ms.to_le_bytes());
2725        signable[8] = compact_entry.op as u8;
2726        signable[9..41].copy_from_slice(&compact_entry.id_hash);
2727        mgr.verify(&signable, &compact_entry.signature)
2728            .expect("Compact signature verification failed");
2729    }
2730
2731    #[test]
2732    fn test_encrypted_arena_roundtrip() {
2733        let dir = tempfile::tempdir().unwrap();
2734        let path = dir.path().to_string_lossy().to_string();
2735
2736        {
2737            let mut config = crate::core::config::HmsConfig::default();
2738            config.security.encryption_enabled = true;
2739            config.security.encryption_passphrase = Some("test-passphrase-123".to_string());
2740
2741            let hms = HmsCore::new(10_000, Some(path.clone()), Some(config)).unwrap();
2742            let v = hms.encode_text("encrypted data");
2743            hms.memorize("enc_1".to_string(), v).unwrap();
2744            let v2 = hms.encode_text("more encrypted data");
2745            hms.memorize("enc_2".to_string(), v2).unwrap();
2746            assert_eq!(hms.vector_count(), 2);
2747        }
2748
2749        // Reopen with same passphrase — should decrypt and recover
2750        let mut config = crate::core::config::HmsConfig::default();
2751        config.security.encryption_enabled = true;
2752        config.security.encryption_passphrase = Some("test-passphrase-123".to_string());
2753
2754        let hms = HmsCore::new(10_000, Some(path), Some(config)).unwrap();
2755        assert_eq!(hms.vector_count(), 2);
2756
2757        let q = hms.encode_text("encrypted data");
2758        let results = hms.query(&q, 1);
2759        assert_eq!(results[0].id, "enc_1");
2760    }
2761
2762    #[test]
2763    fn test_encrypted_compact_roundtrip() {
2764        let dir = tempfile::tempdir().unwrap();
2765        let path = dir.path().to_string_lossy().to_string();
2766
2767        let mut config = crate::core::config::HmsConfig::default();
2768        config.security.encryption_enabled = true;
2769        config.security.encryption_passphrase = Some("compact-test".to_string());
2770
2771        let hms = HmsCore::new(10_000, Some(path.clone()), Some(config.clone())).unwrap();
2772        for i in 0..10 {
2773            let v = hms.encode_text(&format!("encrypted compact {}", i));
2774            hms.memorize(format!("ec_{}", i), v).unwrap();
2775        }
2776        for i in 0..5 {
2777            hms.delete(&format!("ec_{}", i)).unwrap();
2778        }
2779        hms.compact().unwrap();
2780        assert_eq!(hms.vector_count(), 5);
2781
2782        // Reopen after compaction
2783        drop(hms);
2784        let hms = HmsCore::new(10_000, Some(path), Some(config)).unwrap();
2785        assert_eq!(hms.vector_count(), 5);
2786    }
2787}
2788
2789#[cfg(test)]
2790mod meaning_tests {
2791    use crate::core::config::HmsConfig;
2792    use crate::core::engine::HmsCore;
2793
2794    fn meaning_hms(dim: u32) -> HmsCore {
2795        let dir = tempfile::tempdir().unwrap();
2796        let mut config = HmsConfig::default();
2797        config.meaning.enabled = true;
2798        config.meaning.auto_decompose = true;
2799        config.meaning.beta = 24.0;
2800        HmsCore::new(
2801            dim,
2802            Some(dir.path().to_string_lossy().to_string()),
2803            Some(config),
2804        )
2805        .unwrap()
2806    }
2807
2808    #[test]
2809    fn test_basin_certification() {
2810        let hms = meaning_hms(16384);
2811        for i in 0..1000u64 {
2812            let v = crate::core::entangled::EntangledHVec::new_deterministic(16384, i);
2813            hms.memorize(format!("atom_{}", i), v).unwrap();
2814        }
2815
2816        let mut recovered = 0;
2817        for probe_seed in [42u64, 100, 250, 500, 750, 999] {
2818            let original =
2819                crate::core::entangled::EntangledHVec::new_deterministic(16384, probe_seed);
2820            let mut noisy_indices = original.indices().to_vec();
2821            let mut rng = rand::rng();
2822            use rand::RngExt;
2823            for idx in noisy_indices.iter_mut().take(16) {
2824                *idx = rng.random_range(0..16384u32);
2825            }
2826            noisy_indices.sort_unstable();
2827            noisy_indices.dedup();
2828            let noisy = crate::core::entangled::EntangledHVec::from_indices(noisy_indices, 16384);
2829
2830            if let Some((id, _conf)) = hms.meaning_cleanup(&noisy) {
2831                if id == format!("atom_{}", probe_seed) {
2832                    recovered += 1;
2833                }
2834            }
2835        }
2836        assert!(
2837            recovered >= 5,
2838            "Should recover at least 5/6 atoms from 25% noise, got {}",
2839            recovered
2840        );
2841    }
2842
2843    #[test]
2844    fn test_structural_query_e2e() {
2845        let hms = meaning_hms(16384);
2846        hms.memorize_meaning("doc1", "Paris is a city").unwrap();
2847        let s = hms.encode_text("Paris");
2848        let r = hms.encode_text("is_a");
2849        let results = hms.structural_query(&[("subject", &s), ("relation", &r)], "object");
2850        assert_eq!(results.first().map(|r| r.entity_id.as_str()), Some("city"));
2851    }
2852
2853    #[test]
2854    fn test_multi_hop_chained() {
2855        let hms = meaning_hms(16384);
2856        hms.memorize_triplet("t1".into(), "john".into(), "father".into(), "mark".into())
2857            .unwrap();
2858        hms.memorize_triplet("t2".into(), "mark".into(), "father".into(), "bob".into())
2859            .unwrap();
2860        assert_eq!(
2861            hms.multi_hop("john", &["father", "father"])[0].entity_id,
2862            "bob"
2863        );
2864    }
2865
2866    #[test]
2867    fn test_meaning_decompose_e2e() {
2868        let hms = meaning_hms(16384);
2869        hms.memorize_meaning("doc1", "Paris is a city").unwrap();
2870        assert_eq!(hms.meaning_triple_count(), 1);
2871        assert_eq!(hms.meaning_composite_count(), 1);
2872    }
2873
2874    #[test]
2875    fn test_backward_compat_meaning_disabled() {
2876        let dir = tempfile::tempdir().unwrap();
2877        let hms =
2878            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
2879        assert!(!hms.meaning_enabled());
2880        let v = hms.encode_text("test");
2881        hms.memorize("t1".to_string(), v).unwrap();
2882        assert_eq!(hms.vector_count(), 1);
2883        assert!(hms.structural_query(&[], "object").is_empty());
2884        assert!(hms.multi_hop("t1", &["r"]).is_empty());
2885    }
2886
2887    #[test]
2888    fn test_role_inversion() {
2889        use crate::core::admission::AdmissionControl;
2890        use crate::core::atom_memory::AtomMemory;
2891        use crate::core::composite_memory::CompositeMemory;
2892        use crate::core::engine::structural::{fuzzy_structural_query, MeaningContext};
2893        use crate::core::role::RoleRegistry;
2894        use crate::core::triple_store::TripleStore;
2895
2896        let dim = 16384;
2897        let atom_mem = AtomMemory::new(dim, 3.0);
2898        let comp_mem = CompositeMemory::new(dim, 3.0);
2899        let triple_store = TripleStore::new();
2900        let roles = RoleRegistry::new(dim);
2901        let admission = AdmissionControl::new(40);
2902
2903        let triples = vec![
2904            ("alice", "loves", "bob"),
2905            ("paris", "capital_of", "france"),
2906            ("earth", "orbits", "sun"),
2907        ];
2908
2909        for (s, r, o) in &triples {
2910            let (_, sv) = atom_mem.get_or_insert(s);
2911            let (_, rv) = atom_mem.get_or_insert(r);
2912            let (_, ov) = atom_mem.get_or_insert(o);
2913            let comp = roles.compose_triple(&sv, &rv, &ov);
2914            let cid = format!("{}:{}:{}", s, r, o);
2915            comp_mem.insert(cid.clone(), comp);
2916            triple_store.add(s, r, o, &cid);
2917        }
2918
2919        let ctx = MeaningContext {
2920            atom_memory: &atom_mem,
2921            composite_memory: &comp_mem,
2922            triple_store: &triple_store,
2923            roles: &roles,
2924            admission: &admission,
2925            beta: 24.0,
2926            k: 64,
2927            max_iter: 3,
2928        };
2929
2930        for (s, r, o) in &triples {
2931            let sv = atom_mem.get(s).unwrap();
2932            let rv = atom_mem.get(r).unwrap();
2933            let ov = atom_mem.get(o).unwrap();
2934
2935            // Recover object from (subject, relation)
2936            let r1 = fuzzy_structural_query(&ctx, &[("subject", &sv), ("relation", &rv)], "object");
2937            assert!(
2938                !r1.is_empty() && r1[0].entity_id == *o,
2939                "Failed to recover object '{}' from ({}, {})",
2940                o,
2941                s,
2942                r
2943            );
2944
2945            // Recover subject from (relation, object)
2946            let r2 = fuzzy_structural_query(&ctx, &[("relation", &rv), ("object", &ov)], "subject");
2947            assert!(
2948                !r2.is_empty() && r2[0].entity_id == *s,
2949                "Failed to recover subject '{}' from ({}, {})",
2950                s,
2951                r,
2952                o
2953            );
2954
2955            // Recover relation from (subject, object)
2956            let r3 = fuzzy_structural_query(&ctx, &[("subject", &sv), ("object", &ov)], "relation");
2957            assert!(
2958                !r3.is_empty() && r3[0].entity_id == *r,
2959                "Failed to recover relation '{}' from ({}, {})",
2960                r,
2961                s,
2962                o
2963            );
2964        }
2965    }
2966
2967    #[test]
2968    fn test_fan_out_sweep() {
2969        use crate::core::admission::AdmissionControl;
2970        use crate::core::atom_memory::AtomMemory;
2971        use crate::core::composite_memory::CompositeMemory;
2972        use crate::core::engine::structural::{
2973            fuzzy_structural_query, MeaningContext, StructuralPath,
2974        };
2975        use crate::core::role::RoleRegistry;
2976        use crate::core::triple_store::TripleStore;
2977
2978        let dim = 16384;
2979        let atom_mem = AtomMemory::new(dim, 3.0);
2980        let comp_mem = CompositeMemory::new(dim, 3.0);
2981        let triple_store = TripleStore::new();
2982        let roles = RoleRegistry::new(dim);
2983        let fanout_limit = 10;
2984        let admission = AdmissionControl::new(fanout_limit);
2985
2986        let (_, r_vec) = atom_mem.get_or_insert("related_to");
2987
2988        // Create triples with increasing fan-out
2989        for i in 0..20u64 {
2990            let s_name = format!("entity_{}", i);
2991            let o_name = format!("target_{}", i);
2992            let (_, sv) = atom_mem.get_or_insert(&s_name);
2993            let (_, ov) = atom_mem.get_or_insert(&o_name);
2994            let comp = roles.compose_triple(&sv, &r_vec, &ov);
2995            let cid = format!("c_{}", i);
2996            comp_mem.insert(cid.clone(), comp);
2997            triple_store.add(&s_name, "related_to", &o_name, &cid);
2998        }
2999
3000        let ctx = MeaningContext {
3001            atom_memory: &atom_mem,
3002            composite_memory: &comp_mem,
3003            triple_store: &triple_store,
3004            roles: &roles,
3005            admission: &admission,
3006            beta: 24.0,
3007            k: 64,
3008            max_iter: 3,
3009        };
3010
3011        // Query for one specific entity
3012        let sv = atom_mem.get("entity_5").unwrap();
3013        let results =
3014            fuzzy_structural_query(&ctx, &[("subject", &sv), ("relation", &r_vec)], "object");
3015        assert!(!results.is_empty(), "Should find at least one result");
3016
3017        // At high fan-out, should use materialized path
3018        if results.len() > fanout_limit {
3019            assert!(
3020                results
3021                    .iter()
3022                    .any(|r| r.path == StructuralPath::Materialized),
3023                "High fan-out should trigger materialized path"
3024            );
3025        }
3026    }
3027
3028    #[test]
3029    fn test_poisoning_resistance() {
3030        use crate::core::atom_memory::AtomMemory;
3031        use crate::core::entangled::EntangledHVec;
3032
3033        let dim = 16384;
3034        let atom_mem = AtomMemory::new(dim, 3.0);
3035
3036        // Insert 50 normal atoms
3037        for i in 0..50u64 {
3038            atom_mem.get_or_insert(&format!("normal_{}", i));
3039        }
3040
3041        // Create 5 "poisoned" atoms that share a fixed set of decoy indices
3042        let mut decoy_indices: Vec<u32> = (100..148).collect();
3043        decoy_indices.sort_unstable();
3044        for i in 0..5u64 {
3045            let mut indices = decoy_indices.clone();
3046            // Add a few unique indices so they're not identical
3047            indices.push(200 + i as u32);
3048            indices.push(300 + i as u32);
3049            indices.sort_unstable();
3050            indices.dedup();
3051            let vec = EntangledHVec::from_indices(indices, dim);
3052            atom_mem.insert_with_vec(&format!("decoy_{}", i), &vec);
3053        }
3054
3055        // Query with a probe similar to the decoy pattern
3056        let probe = EntangledHVec::from_indices(decoy_indices, dim);
3057        let result = atom_mem.cleanup(&probe, 24.0, 64, 3);
3058
3059        // IDF clipping should prevent the shared decoy indices from
3060        // dominating; the result should be one of the decoy atoms,
3061        // NOT a normal atom that happens to share a few indices
3062        if result.found {
3063            assert!(
3064                result.id.starts_with("decoy_"),
3065                "Cleanup should recover a decoy atom, not a normal one (got {})",
3066                result.id
3067            );
3068        }
3069    }
3070
3071    #[test]
3072    fn test_concurrent_access() {
3073        use std::sync::Arc;
3074        use std::thread;
3075
3076        let dim = 16384;
3077        let hms = Arc::new(meaning_hms(dim as u32));
3078
3079        let mut handles = Vec::new();
3080
3081        // 4 writer threads
3082        for t in 0..4 {
3083            let hms = Arc::clone(&hms);
3084            handles.push(thread::spawn(move || {
3085                for i in 0..100 {
3086                    let id = format!("w{}_{}", t, i);
3087                    let v = crate::core::entangled::EntangledHVec::new_deterministic(
3088                        dim,
3089                        (t * 1000 + i) as u64,
3090                    );
3091                    hms.memorize(id, v).unwrap();
3092                }
3093            }));
3094        }
3095
3096        // 2 reader threads
3097        for _ in 0..2 {
3098            let hms = Arc::clone(&hms);
3099            handles.push(thread::spawn(move || {
3100                for i in 0..100 {
3101                    let probe =
3102                        crate::core::entangled::EntangledHVec::new_deterministic(dim, i as u64);
3103                    let _ = hms.meaning_cleanup(&probe);
3104                }
3105            }));
3106        }
3107
3108        // All threads must complete without deadlock (timeout is implicit via test runner)
3109        for h in handles {
3110            h.join().expect("Thread panicked");
3111        }
3112
3113        assert!(hms.vector_count() >= 400, "All writes should complete");
3114    }
3115
3116    #[test]
3117    fn test_persistence_round_trip() {
3118        use crate::core::entangled::EntangledHVec;
3119
3120        let dir = tempfile::tempdir().unwrap();
3121        let path = dir.path().to_string_lossy().to_string();
3122
3123        let atoms = ["alpha", "beta", "gamma"];
3124        let original_vecs: Vec<(String, EntangledHVec)>;
3125
3126        // Phase 1: store data
3127        let atom_count;
3128        let triple_count;
3129        let vector_count;
3130        {
3131            let hms = meaning_hms_at(&path, 16384);
3132            // Use sentences the decomposer can parse: "X is Y" pattern
3133            hms.memorize_meaning("d1", "Alpha is a concept").unwrap();
3134            hms.memorize_meaning("d2", "Beta is a concept").unwrap();
3135            hms.memorize_meaning("d3", "Gamma is a concept").unwrap();
3136
3137            atom_count = hms.meaning_atom_count();
3138            triple_count = hms.meaning_triple_count();
3139            vector_count = hms.vector_count();
3140            assert!(atom_count > 0, "Should have atoms");
3141            assert!(triple_count > 0, "Should have triples");
3142
3143            original_vecs = atoms
3144                .iter()
3145                .map(|a| {
3146                    let v = hms.encode_text(a);
3147                    (a.to_string(), v)
3148                })
3149                .collect();
3150        }
3151        // HmsCore dropped, all in-memory state gone
3152
3153        // Phase 2: reload from arena
3154        {
3155            let hms = meaning_hms_at(&path, 16384);
3156            assert!(
3157                hms.meaning_atom_count() >= atom_count,
3158                "Atom count should survive reload: before={}, after={}",
3159                atom_count,
3160                hms.meaning_atom_count()
3161            );
3162            assert!(
3163                hms.meaning_triple_count() >= triple_count,
3164                "Triple count should survive reload: before={}, after={}",
3165                triple_count,
3166                hms.meaning_triple_count()
3167            );
3168            assert_eq!(
3169                hms.vector_count(),
3170                vector_count,
3171                "Vector count should survive reload"
3172            );
3173
3174            // Verify encoding is deterministic
3175            for (name, original) in &original_vecs {
3176                let reloaded = hms.encode_text(name);
3177                assert_eq!(
3178                    reloaded.indices(),
3179                    original.indices(),
3180                    "Encoding should be deterministic for {}",
3181                    name
3182                );
3183            }
3184        }
3185    }
3186
3187    fn meaning_hms_at(path: &str, dim: u32) -> HmsCore {
3188        let mut config = HmsConfig::default();
3189        config.meaning.enabled = true;
3190        config.meaning.auto_decompose = true;
3191        config.meaning.beta = 24.0;
3192        HmsCore::new(dim, Some(path.to_string()), Some(config)).unwrap()
3193    }
3194}
3195
3196#[cfg(test)]
3197mod lib_proptest;
3198
3199#[cfg(test)]
3200mod ts_export_tests {
3201    use ts_rs::TS;
3202
3203    #[test]
3204    fn export_ts_bindings() {
3205        let cfg = ts_rs::Config::default();
3206        crate::RetrievalResult::export_all(&cfg).unwrap();
3207        crate::ConceptCandidate::export_all(&cfg).unwrap();
3208        crate::TextMetrics::export_all(&cfg).unwrap();
3209        crate::MemorizeBatchItem::export_all(&cfg).unwrap();
3210        crate::QueryExplanation::export_all(&cfg).unwrap();
3211        crate::IndexStatus::export_all(&cfg).unwrap();
3212        crate::StorageHealth::export_all(&cfg).unwrap();
3213        crate::HardwareCapabilities::export_all(&cfg).unwrap();
3214        crate::HmsError::export_all(&cfg).unwrap();
3215    }
3216
3217    #[test]
3218    fn export_json_schemas() {
3219        use schemars::schema_for;
3220        let dir = std::path::Path::new("schemas");
3221        std::fs::create_dir_all(dir).unwrap();
3222
3223        let schemas: Vec<(&str, schemars::Schema)> = vec![
3224            ("RetrievalResult", schema_for!(crate::RetrievalResult)),
3225            ("ConceptCandidate", schema_for!(crate::ConceptCandidate)),
3226            ("TextMetrics", schema_for!(crate::TextMetrics)),
3227            ("MemorizeBatchItem", schema_for!(crate::MemorizeBatchItem)),
3228            ("HmsError", schema_for!(crate::HmsError)),
3229            ("QueryExplanation", schema_for!(crate::QueryExplanation)),
3230            ("IndexStatus", schema_for!(crate::IndexStatus)),
3231            ("StorageHealth", schema_for!(crate::StorageHealth)),
3232            (
3233                "HardwareCapabilities",
3234                schema_for!(crate::HardwareCapabilities),
3235            ),
3236        ];
3237
3238        for (name, schema) in schemas {
3239            let json = serde_json::to_string_pretty(&schema).unwrap();
3240            std::fs::write(dir.join(format!("{}.json", name)), json).unwrap();
3241        }
3242    }
3243}