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;
16pub use crate::core::entangled::EntangledHVec;
17pub use crate::core::error::HmsError;
18pub use crate::core::types::{ConceptCandidate, MemorizeBatchItem, RetrievalResult, TextMetrics};
19pub use crate::core::HmsCore;
20
21#[cfg(feature = "node-api")]
22#[napi]
23pub struct HolographicMemorySystem {
24    core: Arc<HmsCore>,
25}
26
27#[cfg(feature = "node-api")]
28fn napi_err(e: anyhow::Error) -> napi::Error {
29    napi::Error::from_reason(e.to_string())
30}
31
32#[cfg(feature = "node-api")]
33async fn run_async<T: Send + 'static, F: FnOnce() -> anyhow::Result<T> + Send + 'static>(
34    f: F,
35) -> Result<T> {
36    tokio::task::spawn_blocking(f)
37        .await
38        .map_err(|e| napi::Error::from_reason(e.to_string()))?
39        .map_err(napi_err)
40}
41
42#[cfg(feature = "node-api")]
43#[napi(object)]
44pub struct HmsConfigJs {
45    pub nsg_max_degree: Option<u32>,
46    pub nsg_ef_construction: Option<u32>,
47    pub nsg_auto_threshold: Option<u32>,
48    pub ivf_enabled: Option<bool>,
49    pub ivf_n_clusters: Option<u32>,
50    pub ivf_n_landmarks: Option<u32>,
51    pub ivf_d_reduced: Option<u32>,
52    pub ivf_n_probe: Option<u32>,
53    pub ivf_auto_threshold: Option<u32>,
54    pub shard_enabled: Option<bool>,
55    pub shard_count: Option<u32>,
56    pub shard_auto_threshold: Option<u32>,
57    pub shard_target_size: Option<u32>,
58    pub component_similarity_threshold: Option<f64>,
59    pub component_max_neighbors: Option<u32>,
60    pub concept_similarity_threshold: Option<f64>,
61    pub concept_min_cluster_size: Option<u32>,
62    pub diffusion_steps: Option<u32>,
63    pub diffusion_sigma_max: Option<f64>,
64    pub diffusion_sigma_min: Option<f64>,
65    pub diffusion_step_size: Option<f64>,
66    pub diffusion_n_langevin: Option<u32>,
67    pub signing_enabled: Option<bool>,
68    pub signing_key_path: Option<String>,
69    pub encryption_enabled: Option<bool>,
70    pub encryption_passphrase: Option<String>,
71    pub audit_enabled: Option<bool>,
72    pub dp_enabled: Option<bool>,
73    pub dp_epsilon: Option<f64>,
74    pub meaning_enabled: Option<bool>,
75    pub meaning_beta: Option<f64>,
76    pub meaning_max_fanout: Option<u32>,
77    pub meaning_auto_decompose: Option<bool>,
78    pub meaning_max_hop_depth: Option<u32>,
79    pub cognition_enabled: Option<bool>,
80    pub cognition_interval_secs: Option<u32>,
81    pub cognition_min_pattern_freq: Option<u32>,
82    pub cognition_min_abstraction_members: Option<u32>,
83    pub cognition_min_hypothesis_confidence: Option<f64>,
84}
85
86#[cfg(feature = "node-api")]
87impl HmsConfigJs {
88    fn into_config(self) -> crate::core::config::HmsConfig {
89        use crate::core::config::*;
90        let mut cfg = HmsConfig::default();
91        if let Some(v) = self.nsg_max_degree {
92            cfg.nsg.max_degree = v as usize;
93        }
94        if let Some(v) = self.nsg_ef_construction {
95            cfg.nsg.ef_construction = v as usize;
96        }
97        if let Some(v) = self.nsg_auto_threshold {
98            cfg.nsg.auto_threshold = v as usize;
99        }
100        if let Some(v) = self.ivf_enabled {
101            cfg.ivf.enabled = v;
102        }
103        if let Some(v) = self.ivf_n_clusters {
104            cfg.ivf.n_clusters = v as usize;
105        }
106        if let Some(v) = self.ivf_n_landmarks {
107            cfg.ivf.n_landmarks = v as usize;
108        }
109        if let Some(v) = self.ivf_d_reduced {
110            cfg.ivf.d_reduced = v as usize;
111        }
112        if let Some(v) = self.ivf_n_probe {
113            cfg.ivf.n_probe = v as usize;
114        }
115        if let Some(v) = self.ivf_auto_threshold {
116            cfg.ivf.auto_threshold = v as usize;
117        }
118        if let Some(v) = self.shard_enabled {
119            cfg.shard.enabled = v;
120        }
121        if let Some(v) = self.shard_count {
122            cfg.shard.shard_count = v as usize;
123        }
124        if let Some(v) = self.shard_auto_threshold {
125            cfg.shard.auto_threshold = v as usize;
126        }
127        if let Some(v) = self.shard_target_size {
128            cfg.shard.target_shard_size = v as usize;
129        }
130        if let Some(v) = self.component_similarity_threshold {
131            cfg.query.component_similarity_threshold = v;
132        }
133        if let Some(v) = self.component_max_neighbors {
134            cfg.query.component_max_neighbors = v;
135        }
136        if let Some(v) = self.concept_similarity_threshold {
137            cfg.concepts.similarity_threshold = v;
138        }
139        if let Some(v) = self.concept_min_cluster_size {
140            cfg.concepts.min_cluster_size = v as usize;
141        }
142        if let Some(v) = self.diffusion_steps {
143            cfg.diffusion.steps = v as usize;
144        }
145        if let Some(v) = self.diffusion_sigma_max {
146            cfg.diffusion.sigma_max = v;
147        }
148        if let Some(v) = self.diffusion_sigma_min {
149            cfg.diffusion.sigma_min = v;
150        }
151        if let Some(v) = self.diffusion_step_size {
152            cfg.diffusion.step_size = v;
153        }
154        if let Some(v) = self.diffusion_n_langevin {
155            cfg.diffusion.n_langevin = v as usize;
156        }
157        if let Some(v) = self.signing_enabled {
158            cfg.security.signing_enabled = v;
159        }
160        if let Some(v) = self.signing_key_path {
161            cfg.security.key_path = Some(v);
162        }
163        if let Some(v) = self.encryption_enabled {
164            cfg.security.encryption_enabled = v;
165        }
166        if let Some(v) = self.encryption_passphrase {
167            cfg.security.encryption_passphrase = Some(v);
168        }
169        if let Some(v) = self.audit_enabled {
170            cfg.security.audit_enabled = v;
171        }
172        if let Some(v) = self.dp_enabled {
173            cfg.privacy.dp_enabled = v;
174        }
175        if let Some(v) = self.dp_epsilon {
176            cfg.privacy.epsilon = v;
177        }
178        if let Some(v) = self.meaning_enabled {
179            cfg.meaning.enabled = v;
180        }
181        if let Some(v) = self.meaning_beta {
182            cfg.meaning.beta = v;
183        }
184        if let Some(v) = self.meaning_max_fanout {
185            cfg.meaning.algebraic_max_fanout = v as usize;
186        }
187        if let Some(v) = self.meaning_auto_decompose {
188            cfg.meaning.auto_decompose = v;
189        }
190        if let Some(v) = self.meaning_max_hop_depth {
191            cfg.meaning.max_hop_depth = v as usize;
192        }
193        if let Some(v) = self.cognition_enabled {
194            cfg.cognition.enabled = v;
195        }
196        if let Some(v) = self.cognition_interval_secs {
197            cfg.cognition.interval_secs = v as u64;
198        }
199        if let Some(v) = self.cognition_min_pattern_freq {
200            cfg.cognition.min_pattern_freq = v as usize;
201        }
202        if let Some(v) = self.cognition_min_abstraction_members {
203            cfg.cognition.min_abstraction_members = v as usize;
204        }
205        if let Some(v) = self.cognition_min_hypothesis_confidence {
206            cfg.cognition.min_hypothesis_confidence = v;
207        }
208        cfg
209    }
210}
211
212#[cfg(feature = "node-api")]
213#[napi]
214impl HolographicMemorySystem {
215    #[napi(constructor)]
216    pub fn new(
217        dimensions: u32,
218        storage_path: Option<String>,
219        config: Option<HmsConfigJs>,
220    ) -> Result<Self> {
221        let cfg = config.map(|c| c.into_config());
222        let core = HmsCore::new(dimensions, storage_path, cfg).map_err(napi_err)?;
223        Ok(Self {
224            core: Arc::new(core),
225        })
226    }
227
228    #[napi(getter)]
229    pub fn vector_count(&self) -> u32 {
230        self.core.vector_count() as u32
231    }
232
233    #[napi(getter)]
234    pub fn nsg_trained(&self) -> bool {
235        self.core.nsg_trained()
236    }
237
238    #[napi(getter)]
239    pub fn ivf_trained(&self) -> bool {
240        self.core.ivf_trained()
241    }
242
243    #[napi(getter)]
244    pub fn dimensions(&self) -> u32 {
245        self.core.dimensions() as u32
246    }
247
248    #[napi]
249    pub async fn analyze_text(&self, text: String) -> Result<TextMetrics> {
250        let core = self.core.clone();
251        run_async(move || Ok(core.analyze_text(&text))).await
252    }
253
254    #[napi]
255    pub async fn calculate_readability(&self, metrics: TextMetrics) -> Result<f64> {
256        let core = self.core.clone();
257        run_async(move || Ok(core.calculate_readability(&metrics))).await
258    }
259
260    #[napi]
261    pub async fn memorize_text(
262        &self,
263        id: String,
264        text: String,
265        trace_id: Option<String>,
266    ) -> Result<()> {
267        let core = self.core.clone();
268        run_async(move || {
269            let _span =
270                info_span!("memorize_text", id = %id, trace_id = trace_id.as_deref().unwrap_or(""))
271                    .entered();
272            let vec = core.encode_text(&text);
273            core.memorize(id, vec)
274        })
275        .await
276    }
277
278    /// Zero-copy text ingestion from a Node.js Buffer. Avoids the UTF-8 copy
279    /// that occurs with String parameters by reading bytes in-place.
280    #[napi]
281    pub async fn memorize_text_buffer(
282        &self,
283        id: String,
284        text: Buffer,
285        trace_id: Option<String>,
286    ) -> Result<()> {
287        let core = self.core.clone();
288        let text_str = std::str::from_utf8(&text)
289            .map_err(|e| napi::Error::from_reason(format!("Invalid UTF-8: {}", e)))?
290            .to_owned();
291        run_async(move || {
292            let _span = info_span!("memorize_text_buffer", id = %id, trace_id = trace_id.as_deref().unwrap_or("")).entered();
293            let vec = core.encode_text(&text_str);
294            core.memorize(id, vec)
295        })
296        .await
297    }
298
299    /// Batch memorize multiple id/text pairs in a single native call.
300    /// Uses rayon for parallel encoding, then inserts sequentially.
301    #[napi]
302    pub async fn memorize_batch(
303        &self,
304        items: Vec<MemorizeBatchItem>,
305        trace_id: Option<String>,
306    ) -> Result<()> {
307        let core = self.core.clone();
308        run_async(move || {
309            let _span = info_span!(
310                "memorize_batch",
311                count = items.len(),
312                trace_id = trace_id.as_deref().unwrap_or("")
313            )
314            .entered();
315            use rayon::prelude::*;
316            let encoded: Vec<(String, EntangledHVec)> = items
317                .into_par_iter()
318                .map(|item| {
319                    let vec = core.encode_text(&item.text);
320                    (item.id, vec)
321                })
322                .collect();
323            for (id, vec) in encoded {
324                core.memorize(id, vec)?;
325            }
326            Ok(())
327        })
328        .await
329    }
330
331    /// Read a file directly from disk via memory-mapping and memorize its content.
332    /// Avoids passing file content through the JS string boundary entirely.
333    #[napi]
334    pub async fn memorize_file(&self, id: String, file_path: String) -> Result<()> {
335        let core = self.core.clone();
336        run_async(move || {
337            let file = std::fs::File::open(&file_path)
338                .map_err(|e| anyhow::anyhow!("Failed to open {}: {}", file_path, e))?;
339            let mmap = unsafe { memmap2::Mmap::map(&file) }
340                .map_err(|e| anyhow::anyhow!("Failed to mmap {}: {}", file_path, e))?;
341            let text = std::str::from_utf8(&mmap)
342                .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in {}: {}", file_path, e))?;
343            let vec = core.encode_text(text);
344            core.memorize(id, vec)
345        })
346        .await
347    }
348
349    #[napi]
350    pub async fn memorize_vector(&self, id: String, vector: Float32Array) -> Result<()> {
351        let core = self.core.clone();
352        let dense: Vec<f32> = vector.to_vec();
353        run_async(move || core.memorize_vector(id, &dense)).await
354    }
355
356    #[napi]
357    pub async fn memorize_scalar(&self, id: String, value: f64, min: f64, max: f64) -> Result<()> {
358        let core = self.core.clone();
359        run_async(move || core.memorize_scalar(id, value, min, max)).await
360    }
361
362    #[napi]
363    pub async fn query(
364        &self,
365        text: String,
366        k: u32,
367        trace_id: Option<String>,
368    ) -> Result<Vec<RetrievalResult>> {
369        let core = self.core.clone();
370        run_async(move || {
371            let _span =
372                info_span!("query", k = k, trace_id = trace_id.as_deref().unwrap_or("")).entered();
373            let q_vec = core.encode_text(&text);
374            let results = core.query(&q_vec, k);
375            Ok(results)
376        })
377        .await
378    }
379
380    /// Converts float32→sparse EntangledHVec on JS thread, then queries on background.
381    #[napi]
382    pub async fn query_vector(&self, vector: Float32Array, k: u32) -> Result<Vec<RetrievalResult>> {
383        let core = self.core.clone();
384        let q_vec = EntangledHVec::from_dense(&vector, core.dimensions());
385        run_async(move || {
386            let results = core.query(&q_vec, k);
387            Ok(results)
388        })
389        .await
390    }
391
392    #[napi]
393    pub async fn query_scalar(
394        &self,
395        value: f64,
396        min: f64,
397        max: f64,
398        k: u32,
399    ) -> Result<Vec<RetrievalResult>> {
400        let core = self.core.clone();
401        run_async(move || {
402            let q_vec = EntangledHVec::from_scalar(value, min, max, core.dimensions());
403            let results = core.query(&q_vec, k);
404            Ok(results)
405        })
406        .await
407    }
408
409    /// Process multiple text queries in parallel, returning results for each.
410    #[napi]
411    pub async fn query_batch(
412        &self,
413        texts: Vec<String>,
414        k: u32,
415    ) -> Result<Vec<Vec<RetrievalResult>>> {
416        let core = self.core.clone();
417        run_async(move || {
418            let queries: Vec<EntangledHVec> = texts.iter().map(|t| core.encode_text(t)).collect();
419            Ok(core.query_batch(&queries, k))
420        })
421        .await
422    }
423
424    /// Process multiple float32 vector queries in parallel.
425    #[napi]
426    pub async fn query_vector_batch(
427        &self,
428        vectors: Vec<Float32Array>,
429        k: u32,
430    ) -> Result<Vec<Vec<RetrievalResult>>> {
431        let core = self.core.clone();
432        let queries: Vec<EntangledHVec> = vectors
433            .iter()
434            .map(|v| EntangledHVec::from_dense(v, core.dimensions()))
435            .collect();
436        run_async(move || Ok(core.query_batch(&queries, k))).await
437    }
438
439    #[napi]
440    pub async fn analyze_components(&self, text: String) -> Result<Vec<RetrievalResult>> {
441        let core = self.core.clone();
442        run_async(move || {
443            let vec = core.encode_text(&text);
444            let results = core.analyze_components(&vec);
445            Ok(results)
446        })
447        .await
448    }
449
450    #[napi]
451    pub async fn factorize_diffusion(
452        &self,
453        product_text: String,
454        domains: Vec<Vec<String>>,
455        max_iter: u32,
456    ) -> Result<Vec<Option<String>>> {
457        let core = self.core.clone();
458        run_async(move || {
459            let vec = core.encode_text(&product_text);
460            let domain_vecs: Vec<Vec<EntangledHVec>> = domains
461                .iter()
462                .map(|d| d.iter().map(|s| core.encode_text(s)).collect())
463                .collect();
464            let results = core.factorize_diffusion(&vec, &domain_vecs, max_iter as usize);
465
466            // Map EntangledHVec results back to IDs from the domain strings
467            let mapped = results
468                .into_iter()
469                .enumerate()
470                .map(|(i, opt_vec)| {
471                    opt_vec.and_then(|evec| {
472                        domains[i]
473                            .iter()
474                            .zip(domain_vecs[i].iter())
475                            .min_by_key(|(_, enc)| evec.hamming(enc))
476                            .map(|(s, _)| s.clone())
477                    })
478                })
479                .collect();
480            Ok(mapped)
481        })
482        .await
483    }
484
485    #[napi]
486    pub async fn memorize_triplet(
487        &self,
488        id: String,
489        head: String,
490        relation: String,
491        tail: String,
492    ) -> Result<()> {
493        let core = self.core.clone();
494        run_async(move || core.memorize_triplet(id, head, relation, tail)).await
495    }
496
497    #[napi]
498    pub async fn query_triplet(
499        &self,
500        head: String,
501        relation: String,
502        k: u32,
503    ) -> Result<Vec<RetrievalResult>> {
504        let core = self.core.clone();
505        run_async(move || core.query_triplet(head, relation, k)).await
506    }
507
508    /// Finds an analogy: A is to B as C is to ?.
509    ///
510    /// NOTE: Currently uses character trigram encoding, which has limited semantic
511    /// understanding. Complex semantic analogies may require higher-level word
512    /// embeddings (slated for future upgrade).
513    #[napi]
514    pub async fn find_analogy(
515        &self,
516        a: String,
517        b: String,
518        c: String,
519        k: Option<u32>,
520        trace_id: Option<String>,
521    ) -> Result<Vec<RetrievalResult>> {
522        let core = self.core.clone();
523        run_async(move || {
524            let _span =
525                info_span!("find_analogy", trace_id = trace_id.as_deref().unwrap_or("")).entered();
526            let results = core.find_analogy(&a, &b, &c, k.unwrap_or(5));
527            Ok(results)
528        })
529        .await
530    }
531
532    #[napi]
533    pub async fn synthesize_concepts(&self) -> Result<Vec<ConceptCandidate>> {
534        let core = self.core.clone();
535        run_async(move || {
536            let results = core.synthesize_concepts();
537            Ok(results)
538        })
539        .await
540    }
541
542    #[napi]
543    pub async fn memorize_sequence(&self, id: String, sequence: Vec<String>) -> Result<()> {
544        let core = self.core.clone();
545        run_async(move || core.memorize_sequence(id, &sequence)).await
546    }
547
548    #[napi]
549    pub async fn train_nsg(&self) -> Result<()> {
550        let core = self.core.clone();
551        run_async(move || core.train_nsg()).await
552    }
553
554    #[napi]
555    pub async fn train_ivf(&self) -> Result<()> {
556        let core = self.core.clone();
557        run_async(move || core.train_ivf()).await
558    }
559
560    #[napi]
561    pub async fn query_sequence(
562        &self,
563        partial: Vec<String>,
564        k: u32,
565    ) -> Result<Vec<RetrievalResult>> {
566        let core = self.core.clone();
567        run_async(move || core.query_sequence(&partial, k)).await
568    }
569
570    #[napi]
571    pub async fn delete(&self, id: String) -> Result<bool> {
572        let core = self.core.clone();
573        run_async(move || core.delete(&id)).await
574    }
575
576    #[napi]
577    pub async fn compact(&self) -> Result<()> {
578        let core = self.core.clone();
579        run_async(move || core.compact()).await
580    }
581
582    #[napi]
583    pub async fn audit_since(&self, timestamp_ms: f64) -> Result<Vec<AuditEntryJs>> {
584        let core = self.core.clone();
585        let ts = timestamp_ms as u64;
586        run_async(move || {
587            let entries = core.audit_since(ts)?;
588            Ok(entries
589                .into_iter()
590                .map(|e| AuditEntryJs {
591                    timestamp_ms: e.timestamp_ms as f64,
592                    op: match e.op {
593                        crate::core::audit::AuditOp::Memorize => "memorize".to_string(),
594                        crate::core::audit::AuditOp::Delete => "delete".to_string(),
595                        crate::core::audit::AuditOp::Compact => "compact".to_string(),
596                    },
597                    id_hash: e.id_hash.iter().map(|b| format!("{:02x}", b)).collect(),
598                    signed: e.signature != [0u8; 64],
599                })
600                .collect())
601        })
602        .await
603    }
604
605    /// Bundle multiple text items into a single hypervector.
606    /// Respects the PrivacyConfig: when dp_enabled, uses epsilon-DP noise.
607    #[napi]
608    pub async fn bundle_texts(&self, texts: Vec<String>) -> Result<Vec<u32>> {
609        let core = self.core.clone();
610        run_async(move || {
611            let vecs: Vec<EntangledHVec> = texts.iter().map(|t| core.encode_text(t)).collect();
612            let bundled = core.bundle(&vecs);
613            Ok(bundled.indices().to_vec())
614        })
615        .await
616    }
617
618    // === Meaning Memory API ===
619
620    #[napi]
621    pub async fn memorize_meaning(&self, id: String, text: String) -> Result<()> {
622        let core = self.core.clone();
623        run_async(move || core.memorize_meaning(&id, &text)).await
624    }
625
626    #[napi]
627    pub async fn structural_query(
628        &self,
629        known_subjects: Vec<String>,
630        known_relations: Vec<String>,
631        target_role: String,
632    ) -> Result<Vec<StructuralResultJs>> {
633        let core = self.core.clone();
634        run_async(move || {
635            let known_vecs: Vec<EntangledHVec> = known_subjects
636                .iter()
637                .chain(known_relations.iter())
638                .map(|t| core.encode_text(t))
639                .collect();
640            let mut bindings: Vec<(&str, &EntangledHVec)> = Vec::new();
641            for (i, v) in known_vecs.iter().enumerate() {
642                if i < known_subjects.len() {
643                    bindings.push(("subject", v));
644                } else {
645                    bindings.push(("relation", v));
646                }
647            }
648            let results = core.structural_query(&bindings, &target_role);
649            Ok(results
650                .into_iter()
651                .map(|r| StructuralResultJs {
652                    entity_id: r.entity_id,
653                    confidence: r.confidence,
654                    path: format!("{:?}", r.path),
655                })
656                .collect())
657        })
658        .await
659    }
660
661    #[napi]
662    pub async fn multi_hop_query(
663        &self,
664        start_entity: String,
665        relations: Vec<String>,
666    ) -> Result<Vec<MultiHopResultJs>> {
667        let core = self.core.clone();
668        run_async(move || {
669            let rel_refs: Vec<&str> = relations.iter().map(|s| s.as_str()).collect();
670            let results = core.multi_hop(&start_entity, &rel_refs);
671            Ok(results
672                .into_iter()
673                .map(|r| MultiHopResultJs {
674                    entity_id: r.entity_id,
675                    confidence: r.confidence,
676                    method: format!("{:?}", r.method),
677                })
678                .collect())
679        })
680        .await
681    }
682
683    #[napi]
684    pub async fn meaning_cleanup(&self, text: String) -> Result<Option<CleanupResultJs>> {
685        let core = self.core.clone();
686        run_async(move || {
687            let vec = core.encode_text(&text);
688            Ok(core
689                .meaning_cleanup(&vec)
690                .map(|(id, confidence)| CleanupResultJs { id, confidence }))
691        })
692        .await
693    }
694
695    #[napi]
696    pub fn declare_composition_rule(
697        &self,
698        name: String,
699        input_relations: Vec<String>,
700        output_relation: String,
701    ) {
702        self.core
703            .declare_rule(&name, input_relations, output_relation);
704    }
705
706    #[napi(getter)]
707    pub fn meaning_enabled(&self) -> bool {
708        self.core.meaning_enabled()
709    }
710
711    // === Cognition API ===
712
713    #[napi]
714    pub fn start_cognition(&self) -> Result<()> {
715        self.core.start_cognition().map_err(napi_err)
716    }
717
718    #[napi]
719    pub fn stop_cognition(&self) {
720        self.core.stop_cognition();
721    }
722
723    #[napi(getter)]
724    pub fn cognition_running(&self) -> bool {
725        self.core.cognition_running()
726    }
727
728    #[napi(getter)]
729    pub fn cognition_cycle_count(&self) -> u32 {
730        self.core.cognition_cycle_count() as u32
731    }
732
733    #[napi(getter)]
734    pub fn cognition_insight_count(&self) -> u32 {
735        self.core.cognition_insight_count() as u32
736    }
737
738    #[napi(getter)]
739    pub fn cognition_enabled(&self) -> bool {
740        self.core.cognition_enabled()
741    }
742
743    #[napi]
744    pub fn run_cognition_once(&self) -> u32 {
745        self.core.run_cognition_once().len() as u32
746    }
747
748    #[napi]
749    pub fn govern_memory(&self) -> GovernanceReportJs {
750        let report = self.core.govern_memory();
751        GovernanceReportJs {
752            composites_merged: report.composites_merged as u32,
753            composites_forgotten: report.composites_forgotten as u32,
754            atoms_forgotten: report.atoms_forgotten as u32,
755            idf_refreshed: report.idf_refreshed,
756            atoms_refined: report.refinement.atoms_refined as u32,
757        }
758    }
759
760    // === Graph API ===
761
762    #[napi]
763    pub async fn add_relation(
764        &self,
765        source_id: String,
766        relation_type: String,
767        target_id: String,
768        properties: Option<String>,
769        valid_from: Option<f64>,
770        valid_to: Option<f64>,
771    ) -> Result<()> {
772        let core = self.core.clone();
773        run_async(move || {
774            let rel = crate::core::types::Relation {
775                source_id,
776                relation_type,
777                target_id,
778                properties,
779                valid_from: valid_from.unwrap_or(0.0),
780                valid_to: valid_to.unwrap_or(0.0),
781            };
782            core.add_relation(&rel)
783        })
784        .await
785    }
786
787    #[napi]
788    pub async fn remove_relation(
789        &self,
790        source_id: String,
791        relation_type: String,
792        target_id: String,
793    ) -> Result<bool> {
794        let core = self.core.clone();
795        run_async(move || Ok(core.remove_relation(&source_id, &relation_type, &target_id))).await
796    }
797
798    #[napi]
799    pub fn declare_relation_type(
800        &self,
801        name: String,
802        transitive: Option<bool>,
803        symmetric: Option<bool>,
804    ) {
805        self.core
806            .declare_relation_type(crate::core::types::RelationType {
807                name,
808                transitive: transitive.unwrap_or(false),
809                symmetric: symmetric.unwrap_or(false),
810            });
811    }
812
813    #[napi]
814    pub async fn traverse(
815        &self,
816        start_id: String,
817        relation_type: Option<String>,
818        max_depth: Option<u32>,
819        at_time: Option<f64>,
820    ) -> Result<Vec<crate::core::types::GraphPath>> {
821        let core = self.core.clone();
822        run_async(move || {
823            Ok(core.traverse(
824                &start_id,
825                relation_type.as_deref(),
826                max_depth.unwrap_or(3),
827                at_time.unwrap_or(0.0),
828            ))
829        })
830        .await
831    }
832
833    #[napi]
834    pub async fn outgoing_relations(
835        &self,
836        source_id: String,
837        relation_type: Option<String>,
838        at_time: Option<f64>,
839    ) -> Result<Vec<crate::core::types::Relation>> {
840        let core = self.core.clone();
841        run_async(move || {
842            Ok(core.outgoing_relations(
843                &source_id,
844                relation_type.as_deref(),
845                at_time.unwrap_or(0.0),
846            ))
847        })
848        .await
849    }
850
851    #[napi]
852    pub async fn incoming_relations(
853        &self,
854        target_id: String,
855        relation_type: Option<String>,
856        at_time: Option<f64>,
857    ) -> Result<Vec<crate::core::types::Relation>> {
858        let core = self.core.clone();
859        run_async(move || {
860            Ok(core.incoming_relations(
861                &target_id,
862                relation_type.as_deref(),
863                at_time.unwrap_or(0.0),
864            ))
865        })
866        .await
867    }
868
869    #[napi(getter)]
870    pub fn relation_count(&self) -> u32 {
871        self.core.relation_count() as u32
872    }
873
874    #[napi]
875    pub async fn federated_query(
876        &self,
877        peer_paths: Vec<String>,
878        text: String,
879        k: u32,
880    ) -> Result<Vec<RetrievalResult>> {
881        let core = self.core.clone();
882        run_async(move || {
883            let q_vec = core.encode_text(&text);
884            core.federated_query(&peer_paths, &q_vec, k)
885        })
886        .await
887    }
888}
889
890#[cfg(feature = "node-api")]
891#[napi(object)]
892pub struct AuditEntryJs {
893    pub timestamp_ms: f64,
894    pub op: String,
895    pub id_hash: String,
896    pub signed: bool,
897}
898
899#[cfg(feature = "node-api")]
900#[napi(object)]
901pub struct StructuralResultJs {
902    pub entity_id: String,
903    pub confidence: f64,
904    pub path: String,
905}
906
907#[cfg(feature = "node-api")]
908#[napi(object)]
909pub struct MultiHopResultJs {
910    pub entity_id: String,
911    pub confidence: f64,
912    pub method: String,
913}
914
915#[cfg(feature = "node-api")]
916#[napi(object)]
917pub struct CleanupResultJs {
918    pub id: String,
919    pub confidence: f64,
920}
921
922#[cfg(feature = "node-api")]
923#[napi(object)]
924pub struct GovernanceReportJs {
925    pub composites_merged: u32,
926    pub composites_forgotten: u32,
927    pub atoms_forgotten: u32,
928    pub idf_refreshed: bool,
929    pub atoms_refined: u32,
930}
931
932#[cfg(test)]
933mod tests {
934    use crate::core::engine::HmsCore;
935    use crate::core::entangled::EntangledHVec;
936
937    #[test]
938    fn test_determinism() {
939        let hms = HmsCore::new(1000, None, None).unwrap();
940        let v1 = hms.encode_text("hello world");
941        let v2 = hms.encode_text("hello world");
942        assert!((v1.similarity(&v2) - 1.0).abs() < 0.0001);
943    }
944
945    #[test]
946    fn test_from_dense_produces_sparse() {
947        let dense: Vec<f32> = (0..128).map(|i| (i as f32 - 64.0) / 64.0).collect();
948        let e = EntangledHVec::from_dense(&dense, 1000);
949        assert_eq!(e.dim, 1000);
950        // Should have ~dim/256 active indices
951        let expected = 1000 / 256;
952        assert_eq!(e.indices.len(), expected);
953    }
954
955    // === Batch Memorization ===
956
957    #[test]
958    fn test_batch_memorize() {
959        let dir = tempfile::tempdir().unwrap();
960        let hms =
961            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
962
963        // Simulate what memorize_batch does: parallel encode, sequential insert
964        let items = vec![
965            ("b1", "first batch item"),
966            ("b2", "second batch item"),
967            ("b3", "third batch item"),
968        ];
969        for (id, text) in &items {
970            let vec = hms.encode_text(text);
971            hms.memorize(id.to_string(), vec).unwrap();
972        }
973        assert_eq!(hms.vector_count(), 3);
974
975        let q = hms.encode_text("first batch item");
976        let results = hms.query(&q, 3);
977        assert!(!results.is_empty());
978    }
979
980    // === Custom Config ===
981
982    #[test]
983    fn test_custom_concept_config() {
984        let dir = tempfile::tempdir().unwrap();
985        let mut config = crate::core::config::HmsConfig::default();
986        config.concepts.similarity_threshold = 0.5;
987        config.concepts.min_cluster_size = 5;
988
989        let hms = HmsCore::new(
990            10_000,
991            Some(dir.path().to_string_lossy().to_string()),
992            Some(config),
993        )
994        .unwrap();
995
996        // With high threshold and min size, clusters are harder to form
997        for i in 0..10 {
998            let vec = hms.encode_text(&format!("config test document {}", i));
999            hms.memorize(format!("cfg_{}", i), vec).unwrap();
1000        }
1001
1002        let concepts = hms.synthesize_concepts();
1003        // With strict thresholds, fewer or no concepts should form
1004        // (this validates the config is actually used)
1005        for c in &concepts {
1006            assert!(
1007                c.member_count >= 5,
1008                "Min cluster size should be 5, got {}",
1009                c.member_count
1010            );
1011        }
1012    }
1013
1014    // === Diffusion factorizer ===
1015
1016    #[test]
1017    fn test_factorize_returns_factors() {
1018        let dir = tempfile::tempdir().unwrap();
1019        let hms =
1020            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1021
1022        // Memorize some domain items
1023        let colors = vec!["red", "blue", "green"];
1024        let shapes = vec!["circle", "square", "triangle"];
1025        for c in &colors {
1026            let v = hms.encode_text(c);
1027            hms.memorize(c.to_string(), v).unwrap();
1028        }
1029        for s in &shapes {
1030            let v = hms.encode_text(s);
1031            hms.memorize(s.to_string(), v).unwrap();
1032        }
1033
1034        // Create a composite: red * circle
1035        let red_vec = hms.encode_text("red");
1036        let circle_vec = hms.encode_text("circle");
1037        let product = red_vec.bind(&circle_vec);
1038
1039        let domains = vec![
1040            colors
1041                .iter()
1042                .map(|s| hms.encode_text(s))
1043                .collect::<Vec<EntangledHVec>>(),
1044            shapes
1045                .iter()
1046                .map(|s| hms.encode_text(s))
1047                .collect::<Vec<EntangledHVec>>(),
1048        ];
1049
1050        let results = hms.factorize_diffusion(&product, &domains, 20);
1051        assert_eq!(results.len(), 2);
1052    }
1053
1054    #[test]
1055    fn test_concept_synthesis() {
1056        let dir = tempfile::tempdir().unwrap();
1057        let hms = HmsCore::new(1000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1058
1059        // Create 20 slightly-varied versions of a "base" concept
1060        // Use the same seed with small variations to create tight clusters
1061        let base = hms.encode_text("base concept");
1062        for i in 0..20 {
1063            // Create variants by permuting base slightly
1064            let variant = if i == 0 {
1065                base.clone()
1066            } else {
1067                // Bind with a "small" perturbation (most indices shared)
1068                let perturb = EntangledHVec::new_deterministic(1000, 10000 + i);
1069                // Bundle many copies of base with one perturbation to stay close
1070                EntangledHVec::bundle(&[
1071                    base.clone(),
1072                    base.clone(),
1073                    base.clone(),
1074                    base.clone(),
1075                    perturb,
1076                ])
1077            };
1078            hms.memorize(format!("var_{}", i), variant).unwrap();
1079        }
1080
1081        let concepts = hms.synthesize_concepts();
1082        // We expect at least one synthesized concept representing the cluster
1083        assert!(
1084            !concepts.is_empty(),
1085            "Should synthesize at least one concept"
1086        );
1087        assert!(
1088            concepts[0].coherence > 0.7,
1089            "Synthesized concept should have high coherence, got {}",
1090            concepts[0].coherence
1091        );
1092    }
1093
1094    // === NSG Integration Tests ===
1095
1096    #[test]
1097    fn test_nsg_train_and_query() {
1098        let dir = tempfile::tempdir().unwrap();
1099        let mut config = crate::core::config::HmsConfig::default();
1100        config.nsg.max_degree = 8;
1101
1102        config.nsg.ef_construction = 16;
1103
1104        let hms = HmsCore::new(
1105            1000,
1106            Some(dir.path().to_string_lossy().to_string()),
1107            Some(config),
1108        )
1109        .unwrap();
1110
1111        for i in 0..50 {
1112            let vec = hms.encode_text(&format!("nsg document {}", i));
1113            hms.memorize(format!("nsg_{}", i), vec).unwrap();
1114        }
1115
1116        assert!(!hms.nsg_trained());
1117        hms.train_nsg().unwrap();
1118        assert!(hms.nsg_trained());
1119
1120        let q = hms.encode_text("nsg document 0");
1121        let results = hms.query(&q, 5);
1122        assert!(!results.is_empty(), "NSG query should return results");
1123    }
1124
1125    #[test]
1126    fn test_nsg_auto_train_at_threshold() {
1127        let dir = tempfile::tempdir().unwrap();
1128        let mut config = crate::core::config::HmsConfig::default();
1129        config.nsg.auto_threshold = 30;
1130        config.nsg.max_degree = 8;
1131
1132        config.nsg.ef_construction = 16;
1133
1134        let hms = HmsCore::new(
1135            1000,
1136            Some(dir.path().to_string_lossy().to_string()),
1137            Some(config),
1138        )
1139        .unwrap();
1140
1141        for i in 0..29 {
1142            let vec = hms.encode_text(&format!("auto nsg {}", i));
1143            hms.memorize(format!("ansg_{}", i), vec).unwrap();
1144        }
1145        assert!(!hms.nsg_trained());
1146
1147        let vec = hms.encode_text("auto nsg 29");
1148        hms.memorize("ansg_29".to_string(), vec).unwrap();
1149        assert!(hms.nsg_trained(), "NSG should auto-train at threshold");
1150    }
1151
1152    // === Phase 4 Integration Tests ===
1153
1154    #[test]
1155    fn test_adaptive_routing_e2e() {
1156        let dir = tempfile::tempdir().unwrap();
1157        let mut config = crate::core::config::HmsConfig::default();
1158        config.nsg.max_degree = 8;
1159
1160        config.nsg.ef_construction = 16;
1161
1162        let hms = HmsCore::new(
1163            1000,
1164            Some(dir.path().to_string_lossy().to_string()),
1165            Some(config),
1166        )
1167        .unwrap();
1168
1169        for i in 0..50 {
1170            let vec = hms.encode_text(&format!("routing item {}", i));
1171            hms.memorize(format!("rt_{}", i), vec).unwrap();
1172        }
1173
1174        // Train NSG — queries should now route through NSG
1175        hms.train_nsg().unwrap();
1176        assert!(hms.nsg_trained());
1177
1178        let q = hms.encode_text("routing item 0");
1179        let results = hms.query(&q, 5);
1180        assert!(
1181            !results.is_empty(),
1182            "Adaptive routing should return results via NSG"
1183        );
1184    }
1185
1186    // === Knowledge Graph Tests ===
1187
1188    #[test]
1189    fn test_triplet_memorize_and_query() {
1190        let dir = tempfile::tempdir().unwrap();
1191        let hms =
1192            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1193
1194        hms.memorize_triplet(
1195            "paris_capital".to_string(),
1196            "Paris".to_string(),
1197            "is_capital_of".to_string(),
1198            "France".to_string(),
1199        )
1200        .unwrap();
1201        hms.memorize_triplet(
1202            "berlin_capital".to_string(),
1203            "Berlin".to_string(),
1204            "is_capital_of".to_string(),
1205            "Germany".to_string(),
1206        )
1207        .unwrap();
1208        hms.memorize_triplet(
1209            "tokyo_capital".to_string(),
1210            "Tokyo".to_string(),
1211            "is_capital_of".to_string(),
1212            "Japan".to_string(),
1213        )
1214        .unwrap();
1215
1216        let results = hms
1217            .query_triplet("Paris".to_string(), "is_capital_of".to_string(), 3)
1218            .unwrap();
1219        assert!(!results.is_empty(), "Triplet query should return results");
1220        assert!(
1221            results.iter().any(|r| r.id == "paris_capital"),
1222            "Paris triplet should appear in top-3 results"
1223        );
1224    }
1225
1226    // === Sequence Tests ===
1227
1228    #[test]
1229    fn test_sequence_memorize_and_query() {
1230        let dir = tempfile::tempdir().unwrap();
1231        let hms =
1232            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1233
1234        hms.memorize_sequence(
1235            "recipe_1".to_string(),
1236            &[
1237                "preheat oven".to_string(),
1238                "mix ingredients".to_string(),
1239                "pour into pan".to_string(),
1240                "bake for thirty minutes".to_string(),
1241            ],
1242        )
1243        .unwrap();
1244        hms.memorize_sequence(
1245            "recipe_2".to_string(),
1246            &[
1247                "boil water".to_string(),
1248                "add pasta".to_string(),
1249                "drain and serve".to_string(),
1250            ],
1251        )
1252        .unwrap();
1253
1254        // Query with a partial sequence match
1255        let q = hms.encode_text("preheat oven").permute(0);
1256        let results = hms.query(&q, 2);
1257        assert!(!results.is_empty(), "Sequence query should return results");
1258    }
1259
1260    // === Scalar Query Tests ===
1261
1262    #[test]
1263    fn test_scalar_query_ordering() {
1264        let dir = tempfile::tempdir().unwrap();
1265        let hms =
1266            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1267
1268        for i in 0..20 {
1269            let val = i as f64 * 5.0;
1270            hms.memorize_scalar(format!("temp_{}", i), val, 0.0, 100.0)
1271                .unwrap();
1272        }
1273
1274        // Query near value 50 — should return items closest to 50
1275        let q = EntangledHVec::from_scalar(50.0, 0.0, 100.0, 10_000);
1276        let results = hms.query(&q, 5);
1277        assert!(!results.is_empty(), "Scalar query should return results");
1278
1279        // Top results should cluster around value 50 (idx 10)
1280        let top_idx: usize = results[0]
1281            .id
1282            .strip_prefix("temp_")
1283            .unwrap()
1284            .parse()
1285            .unwrap();
1286        assert!(
1287            (5..=15).contains(&top_idx),
1288            "Top scalar result should be near value 50 (idx 10), got idx {}",
1289            top_idx
1290        );
1291    }
1292
1293    // === Component Analysis Tests ===
1294
1295    #[test]
1296    fn test_analyze_components() {
1297        let dir = tempfile::tempdir().unwrap();
1298        let hms =
1299            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1300
1301        for i in 0..30 {
1302            let vec = hms.encode_text(&format!("component analysis document {}", i));
1303            hms.memorize(format!("ca_{}", i), vec).unwrap();
1304        }
1305
1306        let vec = hms.encode_text("component analysis document 0");
1307        let results = hms.analyze_components(&vec);
1308        assert!(
1309            !results.is_empty(),
1310            "analyze_components should return results"
1311        );
1312        assert!(
1313            results.iter().all(|r| r.similarity > 0.05),
1314            "All results should exceed similarity threshold"
1315        );
1316    }
1317
1318    // === Delete Tests ===
1319
1320    #[test]
1321    fn test_delete_existing() {
1322        let dir = tempfile::tempdir().unwrap();
1323        let hms =
1324            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1325
1326        let vec = hms.encode_text("hello");
1327        hms.memorize("hello".to_string(), vec).unwrap();
1328        assert_eq!(hms.vector_count(), 1);
1329
1330        assert!(hms.delete("hello").unwrap());
1331        assert_eq!(hms.vector_count(), 0);
1332
1333        let q = hms.encode_text("hello");
1334        let results = hms.query(&q, 5);
1335        assert!(
1336            results.is_empty(),
1337            "Deleted vector should not appear in results"
1338        );
1339    }
1340
1341    #[test]
1342    fn test_delete_nonexistent() {
1343        let hms = HmsCore::new(10_000, None, None).unwrap();
1344        assert!(!hms.delete("no_such_id").unwrap());
1345    }
1346
1347    #[test]
1348    fn test_basic_persistence() {
1349        let dir = tempfile::tempdir().unwrap();
1350        let path = dir.path().to_string_lossy().to_string();
1351
1352        {
1353            let hms = HmsCore::new(10_000, Some(path.clone()), None).unwrap();
1354            let v = hms.encode_text("persist me");
1355            hms.memorize("p1".to_string(), v).unwrap();
1356            assert_eq!(hms.vector_count(), 1);
1357        }
1358
1359        let hms = HmsCore::new(10_000, Some(path), None).unwrap();
1360        assert_eq!(hms.vector_count(), 1);
1361    }
1362
1363    #[test]
1364    fn test_delete_persistence() {
1365        let dir = tempfile::tempdir().unwrap();
1366        let path = dir.path().to_string_lossy().to_string();
1367
1368        {
1369            let hms = HmsCore::new(10_000, Some(path.clone()), None).unwrap();
1370            let v1 = hms.encode_text("keep me");
1371            let v2 = hms.encode_text("delete me");
1372            hms.memorize("keep".to_string(), v1).unwrap();
1373            hms.memorize("del".to_string(), v2).unwrap();
1374            assert_eq!(hms.vector_count(), 2);
1375            hms.delete("del").unwrap();
1376            assert_eq!(hms.vector_count(), 1);
1377        }
1378
1379        let hms = HmsCore::new(10_000, Some(path), None).unwrap();
1380        assert_eq!(hms.vector_count(), 1);
1381        let q = hms.encode_text("keep me");
1382        let results = hms.query(&q, 5);
1383        assert!(!results.is_empty(), "Kept vector should survive restart");
1384    }
1385
1386    #[test]
1387    fn test_delete_and_rememorize() {
1388        let dir = tempfile::tempdir().unwrap();
1389        let hms =
1390            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1391
1392        let v1 = hms.encode_text("version 1");
1393        hms.memorize("doc".to_string(), v1).unwrap();
1394        hms.delete("doc").unwrap();
1395
1396        let v2 = hms.encode_text("version 2");
1397        hms.memorize("doc".to_string(), v2.clone()).unwrap();
1398        assert_eq!(hms.vector_count(), 1);
1399
1400        let q = hms.encode_text("version 2");
1401        let results = hms.query(&q, 1);
1402        assert_eq!(results[0].id, "doc");
1403    }
1404
1405    // === Compact Tests ===
1406
1407    #[test]
1408    fn test_compact_basic() {
1409        let dir = tempfile::tempdir().unwrap();
1410        let path = dir.path().to_string_lossy().to_string();
1411
1412        let hms = HmsCore::new(10_000, Some(path.clone()), None).unwrap();
1413        for i in 0..50 {
1414            let vec = hms.encode_text(&format!("item {}", i));
1415            hms.memorize(format!("id_{}", i), vec).unwrap();
1416        }
1417        // Delete half
1418        for i in 0..25 {
1419            hms.delete(&format!("id_{}", i)).unwrap();
1420        }
1421        assert_eq!(hms.vector_count(), 25);
1422
1423        hms.compact().unwrap();
1424
1425        // Verify all live vectors still queryable
1426        let q = hms.encode_text("item 30");
1427        let results = hms.query(&q, 5);
1428        assert!(!results.is_empty(), "Should find results after compaction");
1429        assert_eq!(hms.vector_count(), 25);
1430    }
1431
1432    #[test]
1433    fn test_compact_persistence() {
1434        let dir = tempfile::tempdir().unwrap();
1435        let path = dir.path().to_string_lossy().to_string();
1436
1437        {
1438            let hms = HmsCore::new(10_000, Some(path.clone()), None).unwrap();
1439            for i in 0..20 {
1440                let vec = hms.encode_text(&format!("doc {}", i));
1441                hms.memorize(format!("d_{}", i), vec).unwrap();
1442            }
1443            for i in 0..10 {
1444                hms.delete(&format!("d_{}", i)).unwrap();
1445            }
1446            hms.compact().unwrap();
1447        }
1448
1449        // Re-open from compacted arena
1450        let hms = HmsCore::new(10_000, Some(path), None).unwrap();
1451        assert_eq!(hms.vector_count(), 10);
1452    }
1453
1454    // === Shard Tests ===
1455
1456    #[test]
1457    fn test_multi_shard_insert_query() {
1458        let dir = tempfile::tempdir().unwrap();
1459        let mut config = crate::core::config::HmsConfig::default();
1460        config.shard.enabled = true;
1461        config.shard.shard_count = 4;
1462
1463        let hms = HmsCore::new(
1464            10_000,
1465            Some(dir.path().to_string_lossy().to_string()),
1466            Some(config),
1467        )
1468        .unwrap();
1469
1470        for i in 0..100 {
1471            let vec = hms.encode_text(&format!("shard document {}", i));
1472            hms.memorize(format!("sd_{}", i), vec).unwrap();
1473        }
1474
1475        assert_eq!(hms.vector_count(), 100);
1476
1477        let q = hms.encode_text("shard document 0");
1478        let results = hms.query(&q, 5);
1479        assert!(
1480            !results.is_empty(),
1481            "Multi-shard query should return results"
1482        );
1483    }
1484
1485    #[test]
1486    fn test_multi_shard_delete() {
1487        let dir = tempfile::tempdir().unwrap();
1488        let mut config = crate::core::config::HmsConfig::default();
1489        config.shard.enabled = true;
1490        config.shard.shard_count = 4;
1491
1492        let hms = HmsCore::new(
1493            10_000,
1494            Some(dir.path().to_string_lossy().to_string()),
1495            Some(config),
1496        )
1497        .unwrap();
1498
1499        for i in 0..20 {
1500            let vec = hms.encode_text(&format!("item {}", i));
1501            hms.memorize(format!("m_{}", i), vec).unwrap();
1502        }
1503        assert_eq!(hms.vector_count(), 20);
1504
1505        for i in 0..10 {
1506            assert!(hms.delete(&format!("m_{}", i)).unwrap());
1507        }
1508        assert_eq!(hms.vector_count(), 10);
1509    }
1510
1511    #[test]
1512    fn test_auto_shard_trigger() {
1513        let dir = tempfile::tempdir().unwrap();
1514        let mut config = crate::core::config::HmsConfig::default();
1515        config.shard.enabled = true;
1516        config.shard.shard_count = 0; // auto
1517        config.shard.auto_threshold = 50;
1518        config.shard.target_shard_size = 25;
1519
1520        let hms = HmsCore::new(
1521            10_000,
1522            Some(dir.path().to_string_lossy().to_string()),
1523            Some(config),
1524        )
1525        .unwrap();
1526
1527        for i in 0..50 {
1528            let vec = hms.encode_text(&format!("auto shard {}", i));
1529            hms.memorize(format!("as_{}", i), vec).unwrap();
1530        }
1531
1532        assert_eq!(hms.vector_count(), 50);
1533
1534        // Verify queries still work after auto-sharding
1535        let q = hms.encode_text("auto shard 0");
1536        let results = hms.query(&q, 5);
1537        assert!(!results.is_empty(), "Should find results after auto-shard");
1538    }
1539
1540    // === Audit Integration Tests ===
1541
1542    #[test]
1543    fn test_audit_records_operations() {
1544        let dir = tempfile::tempdir().unwrap();
1545        let mut config = crate::core::config::HmsConfig::default();
1546        config.security.audit_enabled = true;
1547
1548        let hms = HmsCore::new(
1549            10_000,
1550            Some(dir.path().to_string_lossy().to_string()),
1551            Some(config),
1552        )
1553        .unwrap();
1554
1555        let v = hms.encode_text("audit test");
1556        hms.memorize("aud_1".to_string(), v).unwrap();
1557        hms.delete("aud_1").unwrap();
1558
1559        let entries = hms.audit_since(0).unwrap();
1560        assert_eq!(entries.len(), 2);
1561        assert_eq!(entries[0].op, crate::core::audit::AuditOp::Memorize);
1562        assert_eq!(entries[1].op, crate::core::audit::AuditOp::Delete);
1563    }
1564
1565    #[test]
1566    fn test_audit_disabled_returns_empty() {
1567        let hms = HmsCore::new(10_000, None, None).unwrap();
1568        let entries = hms.audit_since(0).unwrap();
1569        assert!(entries.is_empty());
1570    }
1571
1572    #[test]
1573    fn test_audit_compact_recorded() {
1574        let dir = tempfile::tempdir().unwrap();
1575        let mut config = crate::core::config::HmsConfig::default();
1576        config.security.audit_enabled = true;
1577
1578        let hms = HmsCore::new(
1579            10_000,
1580            Some(dir.path().to_string_lossy().to_string()),
1581            Some(config),
1582        )
1583        .unwrap();
1584
1585        let v = hms.encode_text("compact audit");
1586        hms.memorize("ca_1".to_string(), v).unwrap();
1587        hms.compact().unwrap();
1588
1589        let entries = hms.audit_since(0).unwrap();
1590        assert!(entries
1591            .iter()
1592            .any(|e| e.op == crate::core::audit::AuditOp::Compact));
1593    }
1594
1595    // === Graph Integration Tests ===
1596
1597    #[test]
1598    fn test_graph_add_and_traverse() {
1599        let dir = tempfile::tempdir().unwrap();
1600        let hms =
1601            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1602
1603        // Memorize some nodes
1604        for city in &["paris", "france", "europe"] {
1605            let v = hms.encode_text(city);
1606            hms.memorize(city.to_string(), v).unwrap();
1607        }
1608
1609        hms.add_relation(&crate::core::types::Relation {
1610            source_id: "paris".into(),
1611            relation_type: "is_in".into(),
1612            target_id: "france".into(),
1613            properties: None,
1614            valid_from: 0.0,
1615            valid_to: 0.0,
1616        })
1617        .unwrap();
1618        hms.add_relation(&crate::core::types::Relation {
1619            source_id: "france".into(),
1620            relation_type: "is_in".into(),
1621            target_id: "europe".into(),
1622            properties: None,
1623            valid_from: 0.0,
1624            valid_to: 0.0,
1625        })
1626        .unwrap();
1627
1628        assert_eq!(hms.relation_count(), 2);
1629
1630        let paths = hms.traverse("paris", Some("is_in"), 3, 0.0);
1631        assert!(!paths.is_empty());
1632        let targets: Vec<&str> = paths
1633            .iter()
1634            .flat_map(|p| p.hops.iter().map(|h| h.node_id.as_str()))
1635            .collect();
1636        assert!(targets.contains(&"france"));
1637        assert!(targets.contains(&"europe"));
1638    }
1639
1640    #[test]
1641    fn test_graph_transitive_inference() {
1642        let dir = tempfile::tempdir().unwrap();
1643        let hms =
1644            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1645
1646        for name in &["a", "b", "c"] {
1647            let v = hms.encode_text(name);
1648            hms.memorize(name.to_string(), v).unwrap();
1649        }
1650
1651        hms.declare_relation_type(crate::core::types::RelationType {
1652            name: "contains".into(),
1653            transitive: true,
1654            symmetric: false,
1655        });
1656
1657        hms.add_relation(&crate::core::types::Relation {
1658            source_id: "a".into(),
1659            relation_type: "contains".into(),
1660            target_id: "b".into(),
1661            properties: None,
1662            valid_from: 0.0,
1663            valid_to: 0.0,
1664        })
1665        .unwrap();
1666        hms.add_relation(&crate::core::types::Relation {
1667            source_id: "b".into(),
1668            relation_type: "contains".into(),
1669            target_id: "c".into(),
1670            properties: None,
1671            valid_from: 0.0,
1672            valid_to: 0.0,
1673        })
1674        .unwrap();
1675
1676        let paths = hms.traverse("a", Some("contains"), 3, 0.0);
1677        // Should have inferred single-hop a->c
1678        let inferred = paths
1679            .iter()
1680            .find(|p| p.hops.len() == 1 && p.hops[0].node_id == "c");
1681        assert!(inferred.is_some(), "Should infer transitive a->c");
1682    }
1683
1684    #[test]
1685    fn test_graph_persistence() {
1686        let dir = tempfile::tempdir().unwrap();
1687        let path = dir.path().to_string_lossy().to_string();
1688
1689        {
1690            let hms = HmsCore::new(10_000, Some(path.clone()), None).unwrap();
1691            let v = hms.encode_text("node_a");
1692            hms.memorize("node_a".to_string(), v).unwrap();
1693            hms.add_relation(&crate::core::types::Relation {
1694                source_id: "node_a".into(),
1695                relation_type: "links_to".into(),
1696                target_id: "node_b".into(),
1697                properties: None,
1698                valid_from: 0.0,
1699                valid_to: 0.0,
1700            })
1701            .unwrap();
1702        }
1703
1704        let hms = HmsCore::new(10_000, Some(path), None).unwrap();
1705        assert_eq!(hms.relation_count(), 1);
1706        let out = hms.outgoing_relations("node_a", None, 0.0);
1707        assert_eq!(out.len(), 1);
1708        assert_eq!(out[0].target_id, "node_b");
1709    }
1710
1711    #[test]
1712    fn test_graph_temporal_query() {
1713        let dir = tempfile::tempdir().unwrap();
1714        let hms =
1715            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1716
1717        hms.add_relation(&crate::core::types::Relation {
1718            source_id: "alice".into(),
1719            relation_type: "works_at".into(),
1720            target_id: "acme".into(),
1721            properties: None,
1722            valid_from: 1000.0,
1723            valid_to: 2000.0,
1724        })
1725        .unwrap();
1726        hms.add_relation(&crate::core::types::Relation {
1727            source_id: "alice".into(),
1728            relation_type: "works_at".into(),
1729            target_id: "globex".into(),
1730            properties: None,
1731            valid_from: 2001.0,
1732            valid_to: 0.0,
1733        })
1734        .unwrap();
1735
1736        let at_1500 = hms.outgoing_relations("alice", Some("works_at"), 1500.0);
1737        assert_eq!(at_1500.len(), 1);
1738        assert_eq!(at_1500[0].target_id, "acme");
1739
1740        let at_3000 = hms.outgoing_relations("alice", Some("works_at"), 3000.0);
1741        assert_eq!(at_3000.len(), 1);
1742        assert_eq!(at_3000[0].target_id, "globex");
1743    }
1744
1745    #[test]
1746    fn test_federated_query() {
1747        let dir1 = tempfile::tempdir().unwrap();
1748        let dir2 = tempfile::tempdir().unwrap();
1749        let path1 = dir1.path().to_string_lossy().to_string();
1750        let path2 = dir2.path().to_string_lossy().to_string();
1751
1752        // Populate two separate instances
1753        {
1754            let hms1 = HmsCore::new(10_000, Some(path1.clone()), None).unwrap();
1755            let v = hms1.encode_text("federated doc alpha");
1756            hms1.memorize("alpha".to_string(), v).unwrap();
1757        }
1758        {
1759            let hms2 = HmsCore::new(10_000, Some(path2.clone()), None).unwrap();
1760            let v = hms2.encode_text("federated doc beta");
1761            hms2.memorize("beta".to_string(), v).unwrap();
1762        }
1763
1764        // Query from instance 1, federating with instance 2
1765        let hms1 = HmsCore::new(10_000, Some(path1), None).unwrap();
1766        let q = hms1.encode_text("federated doc");
1767        let results = hms1.federated_query(&[path2], &q, 5).unwrap();
1768        let ids: Vec<&str> = results.iter().map(|r| r.id.as_str()).collect();
1769        assert!(ids.contains(&"alpha"), "Should find local result");
1770        assert!(ids.contains(&"beta"), "Should find federated result");
1771    }
1772
1773    #[test]
1774    fn test_graph_compact_preserves_relations() {
1775        let dir = tempfile::tempdir().unwrap();
1776        let hms =
1777            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1778
1779        let v = hms.encode_text("compactable");
1780        hms.memorize("node1".to_string(), v).unwrap();
1781        hms.add_relation(&crate::core::types::Relation {
1782            source_id: "node1".into(),
1783            relation_type: "related".into(),
1784            target_id: "node2".into(),
1785            properties: None,
1786            valid_from: 0.0,
1787            valid_to: 0.0,
1788        })
1789        .unwrap();
1790
1791        hms.compact().unwrap();
1792        assert_eq!(hms.relation_count(), 1);
1793        assert_eq!(hms.vector_count(), 1);
1794    }
1795
1796    // === Analogy Tests ===
1797
1798    #[test]
1799    fn test_find_analogy() {
1800        let dir = tempfile::tempdir().unwrap();
1801        let hms =
1802            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1803
1804        for word in &[
1805            "walking", "talking", "running", "walked", "talked", "runner",
1806        ] {
1807            let v = hms.encode_text(word);
1808            hms.memorize(word.to_string(), v).unwrap();
1809        }
1810
1811        let results = hms.find_analogy("walking", "walked", "talking", 5);
1812        assert!(!results.is_empty(), "Analogy should return results");
1813    }
1814
1815    // === IVF Persistence ===
1816
1817    #[test]
1818    fn test_ivf_index_persistence() {
1819        let dir = tempfile::tempdir().unwrap();
1820        let path = dir.path().to_string_lossy().to_string();
1821
1822        {
1823            let mut config = crate::core::config::HmsConfig::default();
1824            config.ivf.n_clusters = 8;
1825            config.ivf.n_landmarks = 64;
1826            config.ivf.d_reduced = 16;
1827            config.ivf.n_probe = 8;
1828
1829            let hms = HmsCore::new(10_000, Some(path.clone()), Some(config)).unwrap();
1830            for i in 0..200 {
1831                let v = hms.encode_text(&format!("ivf persist {}", i));
1832                hms.memorize(format!("ip_{}", i), v).unwrap();
1833            }
1834            hms.train_ivf().unwrap();
1835            assert!(hms.ivf_trained());
1836        }
1837
1838        let mut config = crate::core::config::HmsConfig::default();
1839        config.ivf.n_clusters = 8;
1840        config.ivf.n_landmarks = 64;
1841        config.ivf.d_reduced = 16;
1842        config.ivf.n_probe = 8;
1843        let hms = HmsCore::new(10_000, Some(path), Some(config)).unwrap();
1844        assert!(
1845            hms.ivf_trained(),
1846            "IVF should be loaded from disk on re-open"
1847        );
1848        let q = hms.encode_text("ivf persist 0");
1849        let results = hms.query(&q, 5);
1850        assert!(!results.is_empty(), "IVF query should work after reload");
1851    }
1852
1853    // === Query Batch Tests ===
1854
1855    #[test]
1856    fn test_query_batch() {
1857        let dir = tempfile::tempdir().unwrap();
1858        let hms =
1859            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1860
1861        for i in 0..20 {
1862            let v = hms.encode_text(&format!("batch query doc {}", i));
1863            hms.memorize(format!("bq_{}", i), v).unwrap();
1864        }
1865
1866        let queries: Vec<EntangledHVec> = (0..3)
1867            .map(|i| hms.encode_text(&format!("batch query doc {}", i)))
1868            .collect();
1869        let batch_results = hms.query_batch(&queries, 3);
1870        assert_eq!(
1871            batch_results.len(),
1872            3,
1873            "Should return one result set per query"
1874        );
1875        for results in &batch_results {
1876            assert!(!results.is_empty(), "Each query should return results");
1877        }
1878    }
1879
1880    // === Memorize Vector Tests ===
1881
1882    #[test]
1883    fn test_memorize_vector_through_core() {
1884        let dir = tempfile::tempdir().unwrap();
1885        let hms =
1886            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1887
1888        let dense: Vec<f32> = (0..128).map(|i| (i as f32 - 64.0) / 64.0).collect();
1889        hms.memorize_vector("dense_1".to_string(), &dense).unwrap();
1890        assert_eq!(hms.vector_count(), 1);
1891
1892        let q = EntangledHVec::from_dense(&dense, 10_000);
1893        let results = hms.query(&q, 1);
1894        assert_eq!(results[0].id, "dense_1");
1895    }
1896
1897    // === Readability Integration ===
1898
1899    #[test]
1900    fn test_readability_through_core() {
1901        let hms = HmsCore::new(10_000, None, None).unwrap();
1902        let metrics = hms.analyze_text("The cat sat on the mat.");
1903        assert!(metrics.word_count > 0);
1904        let score = hms.calculate_readability(&metrics);
1905        assert!(
1906            score > 50.0,
1907            "Simple sentence should be highly readable (got {:.1})",
1908            score
1909        );
1910    }
1911
1912    // === Index Persistence Tests ===
1913
1914    #[test]
1915    fn test_nsg_index_persistence() {
1916        let dir = tempfile::tempdir().unwrap();
1917        let path = dir.path().to_string_lossy().to_string();
1918
1919        {
1920            let mut config = crate::core::config::HmsConfig::default();
1921            config.nsg.max_degree = 8;
1922            config.nsg.ef_construction = 16;
1923
1924            let hms = HmsCore::new(10_000, Some(path.clone()), Some(config)).unwrap();
1925            for i in 0..50 {
1926                let v = hms.encode_text(&format!("persist nsg {}", i));
1927                hms.memorize(format!("pn_{}", i), v).unwrap();
1928            }
1929            hms.train_nsg().unwrap();
1930            assert!(hms.nsg_trained());
1931        }
1932
1933        // Re-open and verify NSG was reloaded
1934        let mut config = crate::core::config::HmsConfig::default();
1935        config.nsg.max_degree = 8;
1936        config.nsg.ef_construction = 16;
1937        let hms = HmsCore::new(10_000, Some(path), Some(config)).unwrap();
1938        assert!(
1939            hms.nsg_trained(),
1940            "NSG should be loaded from disk on re-open"
1941        );
1942        let q = hms.encode_text("persist nsg 0");
1943        let results = hms.query(&q, 5);
1944        assert!(!results.is_empty(), "NSG query should work after reload");
1945    }
1946
1947    // === NSG Accuracy ===
1948
1949    #[test]
1950    fn test_nsg_accuracy_matches_brute_force() {
1951        let dir = tempfile::tempdir().unwrap();
1952        let mut config = crate::core::config::HmsConfig::default();
1953        config.nsg.max_degree = 8;
1954        config.nsg.ef_construction = 16;
1955
1956        let hms = HmsCore::new(
1957            10_000,
1958            Some(dir.path().to_string_lossy().to_string()),
1959            Some(config),
1960        )
1961        .unwrap();
1962
1963        for i in 0..50 {
1964            let v = hms.encode_text(&format!("accuracy test {}", i));
1965            hms.memorize(format!("acc_{}", i), v).unwrap();
1966        }
1967
1968        // Query before training (brute force)
1969        let q = hms.encode_text("accuracy test 0");
1970        let brute_results = hms.query(&q, 1);
1971
1972        hms.train_nsg().unwrap();
1973
1974        // Query after training (NSG)
1975        let nsg_results = hms.query(&q, 1);
1976
1977        assert_eq!(
1978            brute_results[0].id, nsg_results[0].id,
1979            "NSG top-1 should match brute-force top-1"
1980        );
1981    }
1982
1983    // === Multi-Shard Merge Verification ===
1984
1985    #[test]
1986    fn test_multi_shard_results_from_multiple_shards() {
1987        let dir = tempfile::tempdir().unwrap();
1988        let mut config = crate::core::config::HmsConfig::default();
1989        config.shard.enabled = true;
1990        config.shard.shard_count = 4;
1991
1992        let hms = HmsCore::new(
1993            10_000,
1994            Some(dir.path().to_string_lossy().to_string()),
1995            Some(config),
1996        )
1997        .unwrap();
1998
1999        for i in 0..100 {
2000            let v = hms.encode_text(&format!("multi verify {}", i));
2001            hms.memorize(format!("mv_{}", i), v).unwrap();
2002        }
2003
2004        let q = hms.encode_text("multi verify");
2005        let results = hms.query(&q, 20);
2006        assert!(
2007            results.len() >= 10,
2008            "Should return many results from across shards"
2009        );
2010
2011        // Verify results come from at least 2 different shards
2012        use fxhash::FxHasher;
2013        use std::hash::Hasher;
2014        let mut shard_ids: std::collections::HashSet<usize> = std::collections::HashSet::new();
2015        for r in &results {
2016            let mut hasher = FxHasher::default();
2017            hasher.write(r.id.as_bytes());
2018            shard_ids.insert((hasher.finish() as usize) % 4);
2019        }
2020        assert!(
2021            shard_ids.len() >= 2,
2022            "Results should come from multiple shards (got {} distinct shards)",
2023            shard_ids.len()
2024        );
2025    }
2026
2027    // === Compact Multi-Shard ===
2028
2029    #[test]
2030    fn test_compact_multi_shard() {
2031        let dir = tempfile::tempdir().unwrap();
2032        let mut config = crate::core::config::HmsConfig::default();
2033        config.shard.enabled = true;
2034        config.shard.shard_count = 2;
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        for i in 0..30 {
2044            let v = hms.encode_text(&format!("compact shard {}", i));
2045            hms.memorize(format!("cs_{}", i), v).unwrap();
2046        }
2047        for i in 0..15 {
2048            hms.delete(&format!("cs_{}", i)).unwrap();
2049        }
2050        assert_eq!(hms.vector_count(), 15);
2051
2052        hms.compact().unwrap();
2053        assert_eq!(hms.vector_count(), 15);
2054
2055        let q = hms.encode_text("compact shard 20");
2056        let results = hms.query(&q, 5);
2057        assert!(
2058            !results.is_empty(),
2059            "Should find results after multi-shard compact"
2060        );
2061    }
2062
2063    // === IVF Integration Tests ===
2064
2065    #[test]
2066    fn test_manual_ivf_train_and_query() {
2067        let dir = tempfile::tempdir().unwrap();
2068        let mut config = crate::core::config::HmsConfig::default();
2069        config.ivf.n_clusters = 8;
2070        config.ivf.n_landmarks = 64;
2071        config.ivf.d_reduced = 16;
2072        config.ivf.n_probe = 8;
2073
2074        let hms = HmsCore::new(
2075            10000,
2076            Some(dir.path().to_string_lossy().to_string()),
2077            Some(config),
2078        )
2079        .unwrap();
2080
2081        for i in 0..200 {
2082            let vec = hms.encode_text(&format!("document number {}", i));
2083            hms.memorize(format!("doc_{}", i), vec).unwrap();
2084        }
2085
2086        assert!(!hms.ivf_trained());
2087        hms.train_ivf().unwrap();
2088        assert!(hms.ivf_trained());
2089
2090        let q = hms.encode_text("document number 0");
2091        let results = hms.query(&q, 10);
2092        assert!(!results.is_empty(), "IVF query should return results");
2093    }
2094
2095    #[test]
2096    fn test_auto_train_ivf_at_threshold() {
2097        let dir = tempfile::tempdir().unwrap();
2098        let mut config = crate::core::config::HmsConfig::default();
2099        config.ivf.enabled = true;
2100        config.ivf.auto_threshold = 50;
2101        config.ivf.n_clusters = 8;
2102        config.ivf.n_landmarks = 64;
2103        config.ivf.d_reduced = 16;
2104        config.ivf.n_probe = 8;
2105
2106        let hms = HmsCore::new(
2107            1000,
2108            Some(dir.path().to_string_lossy().to_string()),
2109            Some(config),
2110        )
2111        .unwrap();
2112
2113        for i in 0..49 {
2114            let vec = hms.encode_text(&format!("auto item {}", i));
2115            hms.memorize(format!("auto_{}", i), vec).unwrap();
2116        }
2117        assert!(!hms.ivf_trained(), "Should not be trained before threshold");
2118
2119        // This 50th insert should trigger auto-training
2120        let vec = hms.encode_text("auto item 49");
2121        hms.memorize("auto_49".to_string(), vec).unwrap();
2122        assert!(
2123            hms.ivf_trained(),
2124            "Should be trained after reaching threshold"
2125        );
2126    }
2127}
2128
2129#[cfg(all(test, feature = "security"))]
2130mod security_integration_tests {
2131    use crate::core::audit::AuditOp;
2132    use crate::core::engine::HmsCore;
2133    use crate::core::security::SigningManager;
2134
2135    #[test]
2136    fn test_signed_audit_entries() {
2137        let dir = tempfile::tempdir().unwrap();
2138        let mut config = crate::core::config::HmsConfig::default();
2139        config.security.signing_enabled = true;
2140        config.security.audit_enabled = true;
2141
2142        let hms = HmsCore::new(
2143            10_000,
2144            Some(dir.path().to_string_lossy().to_string()),
2145            Some(config),
2146        )
2147        .unwrap();
2148
2149        let v = hms.encode_text("signed audit test");
2150        hms.memorize("sig_1".to_string(), v).unwrap();
2151        hms.delete("sig_1").unwrap();
2152
2153        let entries = hms.audit_since(0).unwrap();
2154        assert_eq!(entries.len(), 2);
2155
2156        // All entries should be signed (non-zero signature)
2157        for entry in &entries {
2158            assert_ne!(entry.signature, [0u8; 64], "Entry should be signed");
2159        }
2160
2161        // Verify signatures using the same key
2162        let key_path = dir.path().join("hms_signing.key");
2163        let mgr = SigningManager::new(&key_path).unwrap();
2164        for entry in &entries {
2165            let mut signable = [0u8; 41];
2166            signable[0..8].copy_from_slice(&entry.timestamp_ms.to_le_bytes());
2167            signable[8] = entry.op as u8;
2168            signable[9..41].copy_from_slice(&entry.id_hash);
2169            mgr.verify(&signable, &entry.signature)
2170                .expect("Signature verification failed");
2171        }
2172    }
2173
2174    #[test]
2175    fn test_signed_compact_audit() {
2176        let dir = tempfile::tempdir().unwrap();
2177        let mut config = crate::core::config::HmsConfig::default();
2178        config.security.signing_enabled = true;
2179        config.security.audit_enabled = true;
2180
2181        let hms = HmsCore::new(
2182            10_000,
2183            Some(dir.path().to_string_lossy().to_string()),
2184            Some(config),
2185        )
2186        .unwrap();
2187
2188        let v = hms.encode_text("compact signed");
2189        hms.memorize("cs_1".to_string(), v).unwrap();
2190        hms.compact().unwrap();
2191
2192        let entries = hms.audit_since(0).unwrap();
2193        let compact_entry = entries.iter().find(|e| e.op == AuditOp::Compact).unwrap();
2194        assert_ne!(compact_entry.signature, [0u8; 64]);
2195
2196        let key_path = dir.path().join("hms_signing.key");
2197        let mgr = SigningManager::new(&key_path).unwrap();
2198        let mut signable = [0u8; 41];
2199        signable[0..8].copy_from_slice(&compact_entry.timestamp_ms.to_le_bytes());
2200        signable[8] = compact_entry.op as u8;
2201        signable[9..41].copy_from_slice(&compact_entry.id_hash);
2202        mgr.verify(&signable, &compact_entry.signature)
2203            .expect("Compact signature verification failed");
2204    }
2205
2206    #[test]
2207    fn test_encrypted_arena_roundtrip() {
2208        let dir = tempfile::tempdir().unwrap();
2209        let path = dir.path().to_string_lossy().to_string();
2210
2211        {
2212            let mut config = crate::core::config::HmsConfig::default();
2213            config.security.encryption_enabled = true;
2214            config.security.encryption_passphrase = Some("test-passphrase-123".to_string());
2215
2216            let hms = HmsCore::new(10_000, Some(path.clone()), Some(config)).unwrap();
2217            let v = hms.encode_text("encrypted data");
2218            hms.memorize("enc_1".to_string(), v).unwrap();
2219            let v2 = hms.encode_text("more encrypted data");
2220            hms.memorize("enc_2".to_string(), v2).unwrap();
2221            assert_eq!(hms.vector_count(), 2);
2222        }
2223
2224        // Reopen with same passphrase — should decrypt and recover
2225        let mut config = crate::core::config::HmsConfig::default();
2226        config.security.encryption_enabled = true;
2227        config.security.encryption_passphrase = Some("test-passphrase-123".to_string());
2228
2229        let hms = HmsCore::new(10_000, Some(path), Some(config)).unwrap();
2230        assert_eq!(hms.vector_count(), 2);
2231
2232        let q = hms.encode_text("encrypted data");
2233        let results = hms.query(&q, 1);
2234        assert_eq!(results[0].id, "enc_1");
2235    }
2236
2237    #[test]
2238    fn test_encrypted_compact_roundtrip() {
2239        let dir = tempfile::tempdir().unwrap();
2240        let path = dir.path().to_string_lossy().to_string();
2241
2242        let mut config = crate::core::config::HmsConfig::default();
2243        config.security.encryption_enabled = true;
2244        config.security.encryption_passphrase = Some("compact-test".to_string());
2245
2246        let hms = HmsCore::new(10_000, Some(path.clone()), Some(config.clone())).unwrap();
2247        for i in 0..10 {
2248            let v = hms.encode_text(&format!("encrypted compact {}", i));
2249            hms.memorize(format!("ec_{}", i), v).unwrap();
2250        }
2251        for i in 0..5 {
2252            hms.delete(&format!("ec_{}", i)).unwrap();
2253        }
2254        hms.compact().unwrap();
2255        assert_eq!(hms.vector_count(), 5);
2256
2257        // Reopen after compaction
2258        drop(hms);
2259        let hms = HmsCore::new(10_000, Some(path), Some(config)).unwrap();
2260        assert_eq!(hms.vector_count(), 5);
2261    }
2262}
2263
2264#[cfg(test)]
2265mod meaning_tests {
2266    use crate::core::config::HmsConfig;
2267    use crate::core::engine::HmsCore;
2268
2269    fn meaning_hms(dim: u32) -> HmsCore {
2270        let dir = tempfile::tempdir().unwrap();
2271        let mut config = HmsConfig::default();
2272        config.meaning.enabled = true;
2273        config.meaning.auto_decompose = true;
2274        config.meaning.beta = 24.0;
2275        HmsCore::new(
2276            dim,
2277            Some(dir.path().to_string_lossy().to_string()),
2278            Some(config),
2279        )
2280        .unwrap()
2281    }
2282
2283    #[test]
2284    fn test_basin_certification() {
2285        let hms = meaning_hms(16384);
2286        for i in 0..1000u64 {
2287            let v = crate::core::entangled::EntangledHVec::new_deterministic(16384, i);
2288            hms.memorize(format!("atom_{}", i), v).unwrap();
2289        }
2290
2291        let mut recovered = 0;
2292        for probe_seed in [42u64, 100, 250, 500, 750, 999] {
2293            let original =
2294                crate::core::entangled::EntangledHVec::new_deterministic(16384, probe_seed);
2295            let mut noisy_indices = original.indices().to_vec();
2296            let mut rng = rand::thread_rng();
2297            use rand::Rng;
2298            for idx in noisy_indices.iter_mut().take(16) {
2299                *idx = rng.gen_range(0..16384u32);
2300            }
2301            noisy_indices.sort_unstable();
2302            noisy_indices.dedup();
2303            let noisy = crate::core::entangled::EntangledHVec::from_indices(noisy_indices, 16384);
2304
2305            if let Some((id, _conf)) = hms.meaning_cleanup(&noisy) {
2306                if id == format!("atom_{}", probe_seed) {
2307                    recovered += 1;
2308                }
2309            }
2310        }
2311        assert!(
2312            recovered >= 5,
2313            "Should recover at least 5/6 atoms from 25% noise, got {}",
2314            recovered
2315        );
2316    }
2317
2318    #[test]
2319    fn test_structural_query_e2e() {
2320        let hms = meaning_hms(16384);
2321        hms.memorize_meaning("doc1", "Paris is capital_of France")
2322            .unwrap();
2323
2324        let s = hms.encode_text("Paris");
2325        let r = hms.encode_text("is_a");
2326
2327        let _results = hms.structural_query(&[("subject", &s), ("relation", &r)], "object");
2328        // Auto-decompose should have created a composite from "Paris is capital_of France"
2329        // and structural_query should find it
2330        // Note: this tests the full pipeline end-to-end
2331        assert!(hms.meaning_enabled());
2332    }
2333
2334    #[test]
2335    fn test_multi_hop_chained() {
2336        let hms = meaning_hms(16384);
2337        hms.memorize_meaning("t1", "John has father Mark").unwrap();
2338        hms.memorize_meaning("t2", "Mark has father Bob").unwrap();
2339
2340        // Chained lookup via TripleStore (populated by auto_decompose)
2341        let _results = hms.multi_hop("John", &["has_father", "has_father"]);
2342        // Multi-hop depends on decomposer extracting the right triples
2343        // Even if decompose doesn't perfectly extract, the pipeline should not crash
2344        assert!(hms.meaning_enabled());
2345    }
2346
2347    #[test]
2348    fn test_meaning_decompose_e2e() {
2349        let hms = meaning_hms(16384);
2350        hms.memorize_meaning("doc1", "Paris is a city").unwrap();
2351        // Verify decomposer ran by checking atom_memory has entries
2352        assert!(hms.meaning_enabled());
2353    }
2354
2355    #[test]
2356    fn test_backward_compat_meaning_disabled() {
2357        let dir = tempfile::tempdir().unwrap();
2358        let hms =
2359            HmsCore::new(10_000, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
2360        assert!(!hms.meaning_enabled());
2361        let v = hms.encode_text("test");
2362        hms.memorize("t1".to_string(), v).unwrap();
2363        assert_eq!(hms.vector_count(), 1);
2364        assert!(hms.structural_query(&[], "object").is_empty());
2365        assert!(hms.multi_hop("t1", &["r"]).is_empty());
2366    }
2367}
2368
2369#[cfg(test)]
2370mod lib_proptest;
2371
2372#[cfg(test)]
2373mod ts_export_tests {
2374    use ts_rs::TS;
2375
2376    #[test]
2377    fn export_ts_bindings() {
2378        crate::RetrievalResult::export_all().unwrap();
2379        crate::ConceptCandidate::export_all().unwrap();
2380        crate::TextMetrics::export_all().unwrap();
2381        crate::MemorizeBatchItem::export_all().unwrap();
2382        crate::HmsError::export_all().unwrap();
2383    }
2384
2385    #[test]
2386    fn export_json_schemas() {
2387        use schemars::schema_for;
2388        let dir = std::path::Path::new("schemas");
2389        std::fs::create_dir_all(dir).unwrap();
2390
2391        let schemas: Vec<(&str, schemars::schema::RootSchema)> = vec![
2392            ("RetrievalResult", schema_for!(crate::RetrievalResult)),
2393            ("ConceptCandidate", schema_for!(crate::ConceptCandidate)),
2394            ("TextMetrics", schema_for!(crate::TextMetrics)),
2395            ("MemorizeBatchItem", schema_for!(crate::MemorizeBatchItem)),
2396            ("HmsError", schema_for!(crate::HmsError)),
2397        ];
2398
2399        for (name, schema) in schemas {
2400            let json = serde_json::to_string_pretty(&schema).unwrap();
2401            std::fs::write(dir.join(format!("{}.json", name)), json).unwrap();
2402        }
2403    }
2404}