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