1#![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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 let expected = 1000 / 256;
952 assert_eq!(e.indices.len(), expected);
953 }
954
955 #[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 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 #[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 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 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 #[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 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 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 let base = hms.encode_text("base concept");
1062 for i in 0..20 {
1063 let variant = if i == 0 {
1065 base.clone()
1066 } else {
1067 let perturb = EntangledHVec::new_deterministic(1000, 10000 + i);
1069 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 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 #[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 #[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 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 #[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 #[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 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 #[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 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 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 #[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 #[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 #[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 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 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 let hms = HmsCore::new(10_000, Some(path), None).unwrap();
1451 assert_eq!(hms.vector_count(), 10);
1452 }
1453
1454 #[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; 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 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 #[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 #[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 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 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 {
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 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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 let q = hms.encode_text("accuracy test 0");
1970 let brute_results = hms.query(&q, 1);
1971
1972 hms.train_nsg().unwrap();
1973
1974 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 #[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 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 #[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 #[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 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 for entry in &entries {
2158 assert_ne!(entry.signature, [0u8; 64], "Entry should be signed");
2159 }
2160
2161 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 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 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 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 let _results = hms.multi_hop("John", &["has_father", "has_father"]);
2342 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 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}