1mod compaction;
5pub(crate) mod concepts;
6mod document_api;
7mod document_batch;
8#[cfg(feature = "local-embed")]
9mod embed_api;
10pub(crate) mod journal;
11pub(crate) mod knowledge;
12pub(crate) mod memory_access;
13pub(crate) mod multi_hop;
14mod mutation;
15mod mutation_gate;
16
17#[cfg(test)]
18mod document_snapshot_tests;
19
20#[cfg(all(test, feature = "local-models"))]
21mod document_stage_tests;
22pub(crate) mod query;
23pub(crate) mod router;
24pub(crate) mod shard;
25pub(crate) mod structural;
26
27use anyhow::{Context, Result};
28
29const REPLAY_CHUNK: usize = 512;
31use parking_lot::RwLock;
32use std::path::{Path, PathBuf};
33use std::sync::Arc;
34
35use super::admission::AdmissionControl;
36use super::agency::goals::GoalStore;
37use super::agency::planner::{Plan, Planner};
38use super::agency::questions::{Question, QuestionGenerator};
39use super::agency::self_modify::{ProposalKind, SelfModifier};
40use super::atom_memory::AtomMemory;
41use super::audit::{AuditLog, AuditOp};
42use super::cognition::governor::{GovernanceReport, GovernorConfig, MemoryGovernor};
43use super::cognition::r#loop::{CognitionConfig as CognitionLoopConfig, CognitionLoop, Insight};
44use super::composite_memory::CompositeMemory;
45use super::config::HmsConfig;
46use super::decompose::Decomposer;
47use super::diffusion::DiffusionFactorizer;
48use super::entangled::EntangledHVec;
49use super::graph::RelationStore;
50use super::ivf::IVFIndex;
51use super::role::RoleRegistry;
52use super::rules::RuleStore;
53use super::storage::PersistentArena;
54use super::store_lock::StoreLock;
55use super::text::TextProcessor;
56use super::triple_store::TripleStore;
57use super::types::{GraphPath, Relation, RelationType, TextMetrics};
58
59use mutation::Mutation;
60use shard::{ShardManager, ShardSet};
61
62type SignFn<'a> = Box<dyn Fn(&[u8]) -> super::audit::SignatureBytes + 'a>;
63
64pub struct HmsCore {
66 config: HmsConfig,
67 mutation_gate: mutation_gate::MutationGate,
68 stem_memos: crate::core::documents::StemMemoPool,
70 compaction: parking_lot::Mutex<()>,
71 persisted_indices: parking_lot::Mutex<Vec<u8>>,
75 revision: std::sync::atomic::AtomicU64,
76 journal: journal::JournalFormat,
78 pub(crate) arena: Arc<PersistentArena>,
79 pub(crate) dimensions: usize,
80 pub(crate) storage_path: PathBuf,
81 shards: RwLock<ShardSet>,
82 graph: RelationStore,
83 documents: RwLock<super::documents::DocumentStore>,
84 vector_index: super::documents::VectorIndexSlot,
86 #[cfg(feature = "local-models")]
87 model_stages: RwLock<Option<Arc<crate::core::models::ModelStages>>>,
88 #[cfg(feature = "local-embed")]
89 text_embedder: RwLock<Option<Arc<crate::core::embed::Embedder>>>,
90 derived: RwLock<std::collections::BTreeMap<String, Vec<String>>>,
91 atom_memory: Option<Arc<AtomMemory>>,
92 composite_memory: Option<Arc<CompositeMemory>>,
93 triple_store: Option<Arc<TripleStore>>,
94 role_registry: Option<RoleRegistry>,
95 rule_store: Option<RuleStore>,
96 decomposer: Option<Decomposer>,
97 admission: Option<AdmissionControl>,
98 cognition_loop: parking_lot::Mutex<Option<CognitionLoop>>,
99 goal_store: Option<GoalStore>,
100 self_modifier: Option<SelfModifier>,
101 audit: Option<AuditLog>,
102 #[cfg(feature = "security")]
103 signing: Option<super::security::SigningManager>,
104 #[cfg(feature = "security")]
105 #[allow(dead_code)]
106 encryption: Option<std::sync::Arc<super::security::EncryptionManager>>,
107 #[cfg(feature = "provenance")]
108 agents: RwLock<super::provenance::access::AgentRegistry>,
109 #[cfg(feature = "provenance")]
110 provenance: Option<super::provenance::ProvenanceManager>,
111 #[cfg(feature = "experimental")]
113 connection_graph: parking_lot::Mutex<Option<super::connection_graph::ConnectionGraph>>,
114 #[cfg(feature = "experimental")]
116 phase_graph: parking_lot::Mutex<Option<super::phase_graph::PhaseGraph>>,
117 _store_lock: StoreLock,
120}
121
122impl HmsCore {
123 pub fn new(
125 dimensions: u32,
126 storage_path: Option<String>,
127 config: Option<HmsConfig>,
128 ) -> Result<Self> {
129 const MAX_DIMENSIONS: u32 = 1_000_000;
130 if dimensions == 0 || dimensions > MAX_DIMENSIONS {
131 return Err(anyhow::anyhow!(
132 "dimensions must be between 1 and {} (got {})",
133 MAX_DIMENSIONS,
134 dimensions
135 ));
136 }
137 let dim = dimensions as usize;
138 let config = config.unwrap_or_default();
139 #[cfg(feature = "local-embed")]
140 let (config, text_embedder) = embed_api::prepare(config)?;
141 config.validate()?;
142 anyhow::ensure!(
143 ((dim / 256).max(1) as f64 / config.privacy.epsilon).is_finite(),
144 "privacy epsilon is too small for the configured dimensions"
145 );
146 #[cfg(feature = "security")]
147 let mut config = config;
148
149 let base_path = storage_path
150 .map(PathBuf::from)
151 .unwrap_or_else(|| Path::new(".").to_path_buf());
152 if !base_path.exists() {
153 std::fs::create_dir_all(&base_path)?;
154 }
155
156 let store_lock = StoreLock::acquire(&base_path)?;
157 let schema = super::schema::validate_store(&base_path, dim, &config)?;
158
159 let arena = Arc::new(PersistentArena::new(base_path.join("vectors_data.bin"))?);
160
161 let audit = if config.security.audit_enabled {
162 Some(AuditLog::new(&base_path)?)
163 } else {
164 None
165 };
166
167 #[cfg(feature = "security")]
168 let signing = if config.security.signing_enabled {
169 let key_path = config
170 .security
171 .key_path
172 .as_ref()
173 .map(PathBuf::from)
174 .unwrap_or_else(|| base_path.join("hms_signing.key"));
175 Some(super::security::SigningManager::new(&key_path)?)
176 } else {
177 None
178 };
179
180 #[cfg(feature = "security")]
181 let encryption = if config.security.encryption_enabled {
182 use zeroize::Zeroize;
183 let mut passphrase = config
184 .security
185 .encryption_passphrase
186 .take()
187 .or_else(|| {
188 config
189 .security
190 .encryption_passphrase_env
191 .as_deref()
192 .and_then(|name| std::env::var(name).ok())
193 })
194 .ok_or_else(|| {
195 anyhow::anyhow!(
196 "encryption requires encryption_passphrase or encryption_passphrase_env"
197 )
198 })?;
199 let manager = super::security::EncryptionManager::new(&passphrase, &base_path);
200 passphrase.zeroize();
201 Some(std::sync::Arc::new(manager?))
202 } else {
203 None
204 };
205
206 #[cfg(feature = "provenance")]
207 let provenance = if config.provenance.enabled {
208 let key_path = config
209 .provenance
210 .key_path
211 .as_ref()
212 .map(std::path::PathBuf::from)
213 .unwrap_or_else(|| base_path.join("hms_provenance.key"));
214 let mgr = super::provenance::ProvenanceManager::new(&key_path, Some(&base_path))?;
215 #[cfg(feature = "provenance-scitt")]
216 let mgr = if let Some(ref endpoint) = config.provenance.scitt_endpoint {
217 mgr.with_scitt_endpoint(endpoint.clone())
218 } else {
219 mgr
220 };
221 Some(mgr)
222 } else {
223 None
224 };
225
226 let shard_set = if config.shard.enabled && config.shard.shard_count > 1 {
227 ShardSet::Multi(ShardManager::new(config.shard.shard_count, dim))
228 } else if let Some(n) = Self::load_shard_meta(&base_path).filter(|&n| n > 1) {
229 ShardSet::Multi(ShardManager::new(n, dim))
232 } else {
233 ShardSet::Single(Box::new(shard::Shard::new(dim)))
234 };
235
236 let (atom_mem, comp_mem, tri_store, role_reg, rule_st, decomp, adm) =
237 if config.meaning.enabled {
238 let mc = &config.meaning;
239 (
240 Some(Arc::new(AtomMemory::new(dim, mc.idf_clip_factor))),
241 Some(Arc::new(CompositeMemory::new(dim, mc.idf_clip_factor))),
242 Some(Arc::new(TripleStore::new())),
243 Some(RoleRegistry::new(dim)),
244 Some(RuleStore::new()),
245 Some(Decomposer::new()),
246 Some(AdmissionControl::new(mc.algebraic_max_fanout)),
247 )
248 } else {
249 (None, None, None, None, None, None, None)
250 };
251
252 let core = Self {
253 config: config.clone(),
254 mutation_gate: mutation_gate::MutationGate::default(),
255 stem_memos: Default::default(),
256 compaction: parking_lot::Mutex::new(()),
257 persisted_indices: parking_lot::Mutex::new(Vec::new()),
258 revision: std::sync::atomic::AtomicU64::new(super::documents::opaque_counter_start()),
259 journal: journal::JournalFormat::for_schema(schema),
260 arena,
261 dimensions: dim,
262 storage_path: base_path,
263 shards: RwLock::new(shard_set),
264 graph: RelationStore::new(),
265 documents: RwLock::new(super::documents::DocumentStore::opened()),
266 vector_index: super::documents::VectorIndexSlot::default(),
267 #[cfg(feature = "local-models")]
268 model_stages: RwLock::new(None),
269 #[cfg(feature = "local-embed")]
270 text_embedder: RwLock::new(text_embedder),
271 derived: RwLock::new(std::collections::BTreeMap::new()),
272 atom_memory: atom_mem,
273 composite_memory: comp_mem,
274 triple_store: tri_store,
275 role_registry: role_reg,
276 rule_store: rule_st,
277 decomposer: decomp,
278 admission: adm,
279 cognition_loop: parking_lot::Mutex::new(None),
280 goal_store: if config.meaning.enabled {
281 Some(GoalStore::new())
282 } else {
283 None
284 },
285 self_modifier: if config.meaning.enabled {
286 Some(SelfModifier::new())
287 } else {
288 None
289 },
290 audit,
291 #[cfg(feature = "security")]
292 signing,
293 #[cfg(feature = "security")]
294 encryption,
295 #[cfg(feature = "provenance")]
296 agents: RwLock::new(super::provenance::access::AgentRegistry::default()),
297 #[cfg(feature = "provenance")]
298 provenance,
299 #[cfg(feature = "experimental")]
300 connection_graph: parking_lot::Mutex::new(None),
301 #[cfg(feature = "experimental")]
302 phase_graph: parking_lot::Mutex::new(None),
303 _store_lock: store_lock,
304 };
305
306 let text_path = core.storage_path.join("documents.text");
307 let staging = super::documents::canonical_staging_path(&text_path);
310 if staging.exists() {
311 super::durable_file::scrub_remove_file(&staging)?;
312 }
313 core.documents.write().attach_text_segment(
314 &text_path,
315 #[cfg(feature = "security")]
316 core.encryption.clone(),
317 )?;
318 core.load_from_log()?;
319 core.load_indices()?;
320 {
321 let shards = core.shards.read();
322 shards.try_for_each_shard(|s| s.rebuild_inverted_index(dim))?;
323 }
324
325 Ok(core)
326 }
327
328 fn shard_meta_path(base: &Path) -> PathBuf {
329 base.join("shard_meta.json")
330 }
331
332 fn load_shard_meta(base: &Path) -> Option<usize> {
334 let data = std::fs::read_to_string(Self::shard_meta_path(base)).ok()?;
335 let value: serde_json::Value = serde_json::from_str(&data).ok()?;
336 value.get("shard_count")?.as_u64().map(|n| n as usize)
337 }
338
339 fn save_shard_meta(&self, shard_count: usize) -> Result<()> {
340 let json = serde_json::json!({ "shard_count": shard_count });
341 super::durable_file::atomic_write(
342 &Self::shard_meta_path(&self.storage_path),
343 &serde_json::to_vec(&json)?,
344 )?;
345 Ok(())
346 }
347
348 fn nsg_index_path(&self, shard_idx: usize, multi: bool) -> PathBuf {
351 if multi {
352 self.storage_path.join(format!("nsg_index_{shard_idx}.bin"))
353 } else {
354 self.storage_path.join("nsg_index.bin")
355 }
356 }
357
358 fn ivf_index_path(&self, shard_idx: usize, multi: bool) -> PathBuf {
359 if multi {
360 self.storage_path.join(format!("ivf_index_{shard_idx}.bin"))
361 } else {
362 self.storage_path.join("ivf_index.bin")
363 }
364 }
365
366 fn load_indices(&self) -> Result<()> {
367 let metadata = self.storage_path.join("indices.json");
368 let Ok(bytes) = std::fs::read(metadata) else {
369 return Ok(());
370 };
371 let saved: serde_json::Value = match serde_json::from_slice(&bytes) {
372 Ok(value) => value,
373 Err(error) => {
374 tracing::warn!(%error, "index metadata invalid; using rebuilt inverted index");
375 return Ok(());
376 }
377 };
378 if saved["checkpoint"] != serde_json::to_value(self.arena.checkpoint())? {
379 tracing::info!("stale ANN checkpoint; using rebuilt inverted index");
380 return Ok(());
381 }
382 let shards = self.shards.read();
383 let multi = shards.shard_count() > 1;
384 if saved["shardCount"] != shards.shard_count() {
385 return Ok(());
386 }
387 shards.try_for_each_shard_indexed(|i, shard| {
388 let read_cache = |path: &Path, expected_crc: &serde_json::Value| -> Result<Vec<u8>> {
389 let raw = std::fs::read(path)?;
390 anyhow::ensure!(
391 expected_crc.as_u64() == Some(crc32fast::hash(&raw) as u64),
392 "index cache checksum mismatch"
393 );
394 self.maybe_decrypt(raw)
395 };
396 let nsg_path = self.nsg_index_path(i, multi);
397 if nsg_path.exists() && saved["shards"][i]["nsg"] == true {
398 let result = (|| -> Result<super::nsg::NSGIndex> {
399 let data = read_cache(&nsg_path, &saved["shards"][i]["nsgCrc"])?;
400 let (nsg, consumed) =
401 bincode::serde::decode_from_slice(&data, bincode::config::standard())?;
402 anyhow::ensure!(consumed == data.len(), "trailing index cache data");
403 Ok(nsg)
404 })();
405 match result {
406 Ok(nsg) => *shard.nsg.write() = Some(nsg),
407 Err(error) => tracing::warn!(%error, shard = i, "discarding invalid NSG cache"),
408 }
409 }
410
411 let ivf_path = self.ivf_index_path(i, multi);
412 if ivf_path.exists() && saved["shards"][i]["ivf"] == true {
413 let result = (|| -> Result<IVFIndex> {
414 let data = read_cache(&ivf_path, &saved["shards"][i]["ivfCrc"])?;
415 let (mut ivf, _): (IVFIndex, usize) =
416 bincode::serde::decode_from_slice(&data, bincode::config::standard())?;
417 ivf.lists = Some(super::ivf::inverted_list::InvertedLists::new());
418
419 let vectors = shard.vectors.read();
420 let registry = shard.registry.read();
421 for id in registry.iter() {
422 if let Some(vec) = vectors.get(id) {
423 ivf.insert(id, vec)?;
424 }
425 }
426 Ok(ivf)
427 })();
428 match result {
429 Ok(ivf) => *shard.ivf.write() = Some(ivf),
430 Err(error) => tracing::warn!(%error, shard = i, "discarding invalid IVF cache"),
431 }
432 }
433 Ok(())
434 })
435 }
436
437 pub(super) fn persist_indices(&self, shards: &ShardSet) -> Result<()> {
441 let checkpoint = self.arena.checkpoint();
444 let encoded = self.encode_indices(shards, checkpoint)?;
445 self.write_indices(encoded)
446 }
447
448 pub(super) fn encode_indices(
456 &self,
457 shards: &ShardSet,
458 checkpoint: (String, usize),
459 ) -> Result<EncodedIndices> {
460 let multi = shards.shard_count() > 1;
461 let mut files = Vec::new();
462 let mut states = Vec::new();
463 shards.try_for_each_shard_indexed(|i, shard| {
464 let mut nsg_crc = None;
465 let mut ivf_crc = None;
466 if let Some(ref nsg) = *shard.nsg.read() {
467 let data = bincode::serde::encode_to_vec(nsg, bincode::config::standard())?;
468 let data = self.maybe_encrypt(&data)?.into_owned();
469 nsg_crc = Some(crc32fast::hash(&data));
470 files.push((self.nsg_index_path(i, multi), data));
471 }
472 if let Some(ref ivf) = *shard.ivf.read() {
473 let data = bincode::serde::encode_to_vec(ivf, bincode::config::standard())?;
474 let data = self.maybe_encrypt(&data)?.into_owned();
475 ivf_crc = Some(crc32fast::hash(&data));
476 files.push((self.ivf_index_path(i, multi), data));
477 }
478 states
479 .push(serde_json::json!({"nsg": shard.nsg_trained(), "ivf": shard.ivf_trained(), "nsgCrc": nsg_crc, "ivfCrc": ivf_crc}));
480 Ok(())
481 })?;
482 let metadata = serde_json::to_vec(
483 &serde_json::json!({"checkpoint": checkpoint, "shardCount": shards.shard_count(), "shards": states}),
484 )?;
485 Ok(EncodedIndices { files, metadata })
486 }
487
488 pub(super) fn write_indices(&self, encoded: EncodedIndices) -> Result<()> {
492 if *self.persisted_indices.lock() == encoded.metadata {
493 return Ok(());
494 }
495 for (path, data) in &encoded.files {
496 super::durable_file::atomic_write(path, data)?;
497 }
498 super::durable_file::atomic_write(
499 &self.storage_path.join("indices.json"),
500 &encoded.metadata,
501 )?;
502 *self.persisted_indices.lock() = encoded.metadata;
503 Ok(())
504 }
505}
506
507pub(super) struct EncodedIndices {
509 files: Vec<(PathBuf, Vec<u8>)>,
510 metadata: Vec<u8>,
511}
512
513impl HmsCore {
514 pub fn bundle<V: std::borrow::Borrow<EntangledHVec>>(&self, vectors: &[V]) -> EntangledHVec {
517 if self.config.privacy.dp_enabled {
518 EntangledHVec::bundle_dp_in_space(vectors, self.config.privacy.epsilon, self.dimensions)
519 } else {
520 EntangledHVec::bundle(vectors)
521 }
522 }
523
524 fn maybe_encrypt<'a>(&self, data: &'a [u8]) -> Result<std::borrow::Cow<'a, [u8]>> {
525 #[cfg(feature = "security")]
526 if let Some(ref enc) = self.encryption {
527 return enc.encrypt(data).map(std::borrow::Cow::Owned);
528 }
529 Ok(std::borrow::Cow::Borrowed(data))
530 }
531
532 fn maybe_decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>> {
534 #[cfg(feature = "security")]
535 if let Some(ref enc) = self.encryption {
536 return enc.decrypt(&data);
537 }
538 Ok(data)
539 }
540
541 fn load_from_log(&self) -> Result<()> {
542 let shards = self.shards.write();
543 let mut offset = 0;
544 while offset < self.arena.stats().used_bytes {
545 let (data, _) = self
546 .arena
547 .read_frame(offset)
548 .with_context(|| format!("cannot replay arena frame at {offset}"))?;
549 let payload = self.maybe_decrypt(data).with_context(|| {
552 format!("cannot decrypt arena frame at {offset}: wrong passphrase, or the store is corrupted")
553 })?;
554 if let Some(json) = payload.strip_prefix(mutation::TRANSACTION_MAGIC) {
555 self.replay_frame(json, &shards)?;
556 } else if let Some(body) = payload.strip_prefix(journal::BINARY_MAGIC) {
557 self.replay_binary_frame(body, &shards)
558 .with_context(|| format!("cannot replay arena frame at {offset}"))?;
559 } else {
560 anyhow::bail!(match super::format::scan::classify_payload(&payload) {
561 super::format::scan::PayloadKind::OtherTransactionVersion(version)
562 if version.parse::<u32>().is_ok_and(|v| v > 3) =>
563 {
564 format!("journal frame format HMS-TXN-{version} is newer than the supported HMS-TXN-3; open the store with a release that supports it")
565 }
566 _ => "unsupported transaction format; re-encode original sources into a new store".to_string(),
567 });
568 }
569 let next = self.arena.next_offset(offset)?;
570 self.arena
572 .release_pages(offset, next.saturating_sub(offset));
573 offset = next;
574 }
575 if let Some(atoms) = &self.atom_memory {
576 atoms.rebuild_indices();
577 }
578 if let Some(composites) = &self.composite_memory {
579 composites.rebuild_indices();
580 }
581 Ok(())
582 }
583
584 fn replay_frame(&self, json: &[u8], shards: &ShardSet) -> Result<()> {
588 struct Chunked<'a, F> {
589 apply: F,
590 failure: &'a mut Option<anyhow::Error>,
591 }
592 impl<'de, F: FnMut(&[Mutation]) -> Result<()>> serde::de::Visitor<'de> for Chunked<'_, F> {
593 type Value = ();
594 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
595 f.write_str("a sequence")
596 }
597 fn visit_seq<A: serde::de::SeqAccess<'de>>(
598 mut self,
599 mut seq: A,
600 ) -> std::result::Result<(), A::Error> {
601 let mut chunk = Vec::with_capacity(REPLAY_CHUNK);
602 loop {
603 let next = seq.next_element::<Mutation>()?;
604 let done = next.is_none();
605 chunk.extend(next);
606 if chunk.len() == REPLAY_CHUNK || (done && !chunk.is_empty()) {
607 if let Err(error) = (self.apply)(&chunk) {
608 *self.failure = Some(error);
609 return Err(serde::de::Error::custom("journal replay failed"));
610 }
611 chunk.clear();
612 }
613 if done {
614 return Ok(());
615 }
616 }
617 }
618 }
619 let mut failure = None;
620 let mut decoder = serde_json::Deserializer::from_slice(json);
621 let decoded = serde::Deserializer::deserialize_seq(
622 &mut decoder,
623 Chunked {
624 apply: |chunk: &[Mutation]| {
625 for mutation in chunk {
626 self.validate_mutation(mutation)?;
627 }
628 self.apply_mutations(&self.prepare_mutations(chunk), shards)
629 },
630 failure: &mut failure,
631 },
632 );
633 if let Some(error) = failure {
634 return Err(error);
635 }
636 decoded?;
637 decoder.end()?;
638 Ok(())
639 }
640
641 fn replay_binary_frame(&self, body: &[u8], shards: &ShardSet) -> Result<()> {
644 let mut decoder = journal::BinaryDecoder::new(body)?;
645 let mut chunk = Vec::with_capacity(REPLAY_CHUNK);
646 loop {
647 let next = decoder.next_mutation()?;
648 let done = next.is_none();
649 chunk.extend(next);
650 if chunk.len() == REPLAY_CHUNK || (done && !chunk.is_empty()) {
651 for mutation in &chunk {
652 self.validate_mutation(mutation)?;
653 }
654 self.apply_mutations(&self.prepare_mutations(&chunk), shards)?;
655 chunk.clear();
656 }
657 if done {
658 return Ok(());
659 }
660 }
661 }
662
663 fn transaction_payload(&self, mutations: &[Mutation]) -> Result<Vec<u8>> {
665 match self.journal {
666 journal::JournalFormat::Json => mutation::transaction_payload(mutations),
667 journal::JournalFormat::Binary => journal::binary_payload(mutations),
668 }
669 }
670
671 pub fn dimensions(&self) -> usize {
673 self.dimensions
674 }
675
676 pub fn encode_text(&self, text: &str) -> EntangledHVec {
678 #[cfg(feature = "local-embed")]
679 if self.config.text_encoder == super::encoding::TextEncoder::LocalEmbedV1 {
680 return self.encode_text_with_embedder(text);
681 }
682 self.config.text_encoder.encode(text, self.dimensions)
683 }
684
685 pub fn analyze_text(&self, text: &str) -> TextMetrics {
687 TextProcessor::analyze(text)
688 }
689
690 pub fn calculate_readability(&self, metrics: &TextMetrics) -> f64 {
692 TextProcessor::calculate_readability(metrics)
693 }
694
695 pub fn delete(&self, id: &str) -> Result<bool> {
697 self.delete_with_reason(id, None)
698 }
699
700 pub fn delete_with_reason(
701 &self,
702 id: &str,
703 #[allow(unused_variables)] reason: Option<&str>,
704 ) -> Result<bool> {
705 mutation::validate_public_id(id)?;
706 anyhow::ensure!(
707 !id.starts_with(super::documents::CHUNK_PREFIX),
708 "use deleteDocument to delete document chunks"
709 );
710 let existed = self.shards.read().get_vector(id).is_some();
711 self.commit(&[Mutation::Delete { id: id.to_owned() }])?;
712 if let Some(ref audit) = self.audit {
713 audit.record(AuditOp::Delete, id, self.sign_fn().as_deref())?;
714 }
715 #[cfg(feature = "provenance")]
716 if let Some(ref mgr) = self.provenance {
717 if self.config.provenance.auto_sign {
718 mgr.record_deletion(id, reason)?;
719 }
720 }
721 Ok(existed)
722 }
723
724 pub fn memorize_meaning(&self, id: &str, text: &str) -> Result<()> {
725 self.memorize_meaning_with_source(id, text, None)
726 }
727
728 pub fn memorize_meaning_with_source(
729 &self,
730 id: &str,
731 text: &str,
732 #[allow(unused_variables)] source_uri: Option<&str>,
733 ) -> Result<()> {
734 mutation::validate_public_id(id)?;
735 let mut mutations = vec![Mutation::Vector {
736 id: id.to_owned(),
737 vector: self.encode_text(text),
738 }];
739 let mut children = Vec::new();
740 if self.config.meaning.auto_decompose {
741 if let Some(decomposer) = &self.decomposer {
742 for (i, unit) in decomposer.decompose(text).into_iter().enumerate() {
743 let child = format!("{}{}:{id}:{i}", mutation::MEANING_PREFIX, id.len());
744 children.push(child.clone());
745 mutations.push(Mutation::Triplet {
746 id: child,
747 subject: unit.subject,
748 relation: unit.relation,
749 object: unit.object,
750 });
751 }
752 }
753 }
754 if !children.is_empty() {
755 mutations.push(Mutation::Derived {
756 id: id.to_owned(),
757 children,
758 });
759 }
760 self.commit(&mutations)?;
761 #[cfg(feature = "provenance")]
762 if let Some(ref mgr) = self.provenance {
763 if self.config.provenance.auto_sign {
764 mgr.create_fact_provenance(id, text.as_bytes(), source_uri)?;
765 }
766 }
767 Ok(())
768 }
769
770 pub fn memorize(&self, id: String, vector: EntangledHVec) -> Result<()> {
772 mutation::validate_public_id(&id)?;
773 anyhow::ensure!(
774 !id.starts_with(super::documents::CHUNK_PREFIX),
775 "chunk IDs are reserved for document ingestion"
776 );
777
778 self.commit(&[Mutation::Vector {
779 id: id.clone(),
780 vector,
781 }])?;
782 if let Some(ref audit) = self.audit {
783 audit.record(AuditOp::Memorize, &id, self.sign_fn().as_deref())?;
784 }
785 Ok(())
786 }
787
788 fn maybe_auto_shard(&self, count: u64) -> Result<()> {
789 let cfg = &self.config.shard;
790 if !cfg.enabled
791 || cfg.shard_count > 0
792 || cfg.auto_threshold == 0
793 || count < cfg.auto_threshold as u64
794 {
795 return Ok(());
796 }
797 let (revision, snapshot) = {
798 let _transaction = self.mutation_gate.read();
799 let shards = self.shards.read();
800 if shards.shard_count() > 1 {
801 return Ok(());
802 }
803 (
804 self.revision.load(std::sync::atomic::Ordering::Acquire),
805 shards.collect_all_patterns(),
806 )
807 };
808 let count = (count as usize / cfg.target_shard_size).clamp(2, 1024);
809 let new_set = ShardSet::Multi(ShardManager::new(count, self.dimensions));
810 for (id, vector) in snapshot {
811 new_set.insert(id, vector, self.dimensions)?;
812 }
813 let _transaction = self.mutation_gate.write();
814 anyhow::ensure!(
815 revision == self.revision.load(std::sync::atomic::Ordering::Acquire),
816 "store changed during sharding; retry maintenance"
817 );
818 self.save_shard_meta(count)?;
819 *self.shards.write() = new_set;
820 self.revision
821 .fetch_add(1, std::sync::atomic::Ordering::Release);
822 self.persist_indices(&self.shards.read())
823 }
824
825 pub fn memorize_vector(&self, id: String, dense: &[f32]) -> Result<()> {
827 self.validate_dense(dense)?;
828 let vector = EntangledHVec::from_dense(dense, self.dimensions);
829 self.memorize(id, vector)
830 }
831
832 pub fn memorize_scalar(&self, id: String, value: f64, min: f64, max: f64) -> Result<()> {
834 let vector = EntangledHVec::from_scalar(value, min, max, self.dimensions);
835 self.memorize(id, vector)
836 }
837
838 pub fn vector_count(&self) -> u64 {
840 self.shards.read().count()
841 }
842
843 pub fn add_relation(&self, rel: &Relation) -> Result<()> {
846 self.commit(&[Mutation::Relation(rel.clone())])
847 }
848
849 pub fn remove_relation(
850 &self,
851 source_id: &str,
852 relation_type: &str,
853 target_id: &str,
854 ) -> Result<bool> {
855 let existed = self
856 .graph
857 .outgoing(source_id, Some(relation_type), 0.0)
858 .iter()
859 .any(|r| r.target_id == target_id);
860 self.commit(&[Mutation::RemoveRelation {
861 source: source_id.into(),
862 relation: relation_type.into(),
863 target: target_id.into(),
864 }])?;
865 Ok(existed)
866 }
867
868 pub fn declare_relation_type(&self, rel_type: RelationType) -> Result<()> {
869 self.commit(&[Mutation::RelationType(rel_type)])
870 }
871
872 pub fn traverse(
873 &self,
874 start_id: &str,
875 relation_type: Option<&str>,
876 max_depth: u32,
877 at_time: f64,
878 ) -> Vec<GraphPath> {
879 let _transaction = self.mutation_gate.read();
880 let shards = self.shards.read();
881 self.graph
882 .traverse(start_id, relation_type, max_depth, at_time, &|a, b| {
883 let vec_a = shards.get_vector(a);
884 let vec_b = shards.get_vector(b);
885 match (vec_a, vec_b) {
886 (Some(va), Some(vb)) => va.similarity(&vb),
887 _ => 0.0,
888 }
889 })
890 }
891
892 pub fn outgoing_relations(
893 &self,
894 source_id: &str,
895 relation_type: Option<&str>,
896 at_time: f64,
897 ) -> Vec<Relation> {
898 let _transaction = self.mutation_gate.read();
899 self.graph.outgoing(source_id, relation_type, at_time)
900 }
901
902 pub fn incoming_relations(
903 &self,
904 target_id: &str,
905 relation_type: Option<&str>,
906 at_time: f64,
907 ) -> Vec<Relation> {
908 let _transaction = self.mutation_gate.read();
909 self.graph.incoming(target_id, relation_type, at_time)
910 }
911
912 pub fn relation_count(&self) -> usize {
913 let _transaction = self.mutation_gate.read();
914 self.graph.count()
915 }
916
917 pub fn federated_query(
928 &self,
929 peer_paths: &[String],
930 query_vec: &EntangledHVec,
931 k: u32,
932 ) -> Result<Vec<super::types::RetrievalResult>> {
933 use rayon::prelude::*;
934 use std::collections::BinaryHeap;
935
936 let k = k as usize;
937
938 let local_results = self.query(query_vec, k as u32);
940
941 let peer_outcomes: Vec<(String, Result<Vec<super::types::RetrievalResult>>)> = peer_paths
943 .par_iter()
944 .map(|path| {
945 let outcome = HmsCore::new(
946 self.dimensions as u32,
947 Some(path.clone()),
948 Some(self.config.clone()),
949 )
950 .map(|peer| peer.query(query_vec, k as u32));
951 (path.clone(), outcome)
952 })
953 .collect();
954
955 let mut seen: fxhash::FxHashMap<String, f64> =
957 fxhash::FxHashMap::with_capacity_and_hasher(k * 2, Default::default());
958
959 let mut heap: BinaryHeap<super::types::RetrievalResult> = BinaryHeap::with_capacity(k + 1);
962
963 let mut insert = |r: super::types::RetrievalResult| {
964 let dominated = seen
966 .get(&r.id)
967 .is_some_and(|&prev_sim| prev_sim >= r.similarity);
968 if dominated {
969 return;
970 }
971 seen.insert(r.id.clone(), r.similarity);
972 heap.push(r);
973 if heap.len() > k {
974 if let Some(evicted) = heap.pop() {
975 seen.remove(&evicted.id);
976 }
977 }
978 };
979
980 for r in local_results {
981 insert(r);
982 }
983
984 for (path, outcome) in peer_outcomes {
985 match outcome {
986 Ok(results) => {
987 for r in results {
988 insert(r);
989 }
990 }
991 Err(e) => {
992 tracing::warn!("federated query: peer {:?} failed, skipping: {}", path, e);
993 }
994 }
995 }
996
997 Ok(heap.into_sorted_vec())
999 }
1000
1001 fn meaning_ctx(&self) -> Option<structural::MeaningContext<'_>> {
1006 let (atom_mem, comp_mem, tri, roles, adm) = match (
1007 &self.atom_memory,
1008 &self.composite_memory,
1009 &self.triple_store,
1010 &self.role_registry,
1011 &self.admission,
1012 ) {
1013 (Some(a), Some(c), Some(t), Some(r), Some(ad)) => (a, c, t, r, ad),
1014 _ => return None,
1015 };
1016 let mc = &self.config.meaning;
1017 Some(structural::MeaningContext {
1018 atom_memory: atom_mem,
1019 composite_memory: comp_mem,
1020 triple_store: tri,
1021 roles,
1022 admission: adm,
1023 beta: mc.beta,
1024 k: 64,
1025 max_iter: 3,
1026 })
1027 }
1028
1029 pub fn structural_query(
1030 &self,
1031 known: &[(&str, &EntangledHVec)],
1032 target_role: &str,
1033 ) -> Vec<structural::StructuralResult> {
1034 let _transaction = self.mutation_gate.read();
1035 let ctx = match self.meaning_ctx() {
1036 Some(ctx) => ctx,
1037 None => return Vec::new(),
1038 };
1039 structural::fuzzy_structural_query(&ctx, known, target_role)
1040 }
1041
1042 pub fn multi_hop(&self, start: &str, relations: &[&str]) -> Vec<multi_hop::MultiHopResult> {
1043 let _transaction = self.mutation_gate.read();
1044 let (ctx, rules) = match (self.meaning_ctx(), &self.rule_store) {
1045 (Some(ctx), Some(rules)) => (ctx, rules),
1046 _ => return Vec::new(),
1047 };
1048 let mc = &self.config.meaning;
1049 multi_hop::multi_hop_query(start, relations, &ctx, rules, mc.max_hop_depth)
1050 }
1051
1052 pub fn meaning_cleanup(&self, noisy: &EntangledHVec) -> Option<(String, f64)> {
1053 let atom_mem = self.atom_memory.as_ref()?;
1054 let mc = &self.config.meaning;
1055 let result = atom_mem.cleanup(noisy, mc.beta, 64, 3);
1056 if result.found {
1057 Some((result.id, result.confidence))
1058 } else {
1059 None
1060 }
1061 }
1062
1063 pub fn declare_rule(
1064 &self,
1065 name: &str,
1066 input_relations: Vec<String>,
1067 output_relation: String,
1068 ) -> Result<()> {
1069 self.commit(&[Mutation::Rule(super::rules::CompositionRule {
1070 name: name.to_string(),
1071 input_relations,
1072 output_relation,
1073 })])
1074 }
1075
1076 pub fn meaning_enabled(&self) -> bool {
1077 self.config.meaning.enabled
1078 }
1079
1080 pub fn meaning_atom_count(&self) -> usize {
1081 self.atom_memory.as_ref().map_or(0, |m| m.count())
1082 }
1083
1084 pub fn meaning_composite_count(&self) -> usize {
1085 self.composite_memory.as_ref().map_or(0, |m| m.count())
1086 }
1087
1088 pub fn meaning_triple_count(&self) -> usize {
1089 self.triple_store.as_ref().map_or(0, |t| t.count())
1090 }
1091
1092 pub fn meaning_rule_count(&self) -> usize {
1093 self.rule_store.as_ref().map_or(0, |r| r.count())
1094 }
1095
1096 pub fn register_role(&mut self, name: &str, shift: usize) -> anyhow::Result<()> {
1097 if let Some(ref mut roles) = self.role_registry {
1098 roles.register(name, shift)
1099 } else {
1100 Err(anyhow::anyhow!("meaning memory not enabled"))
1101 }
1102 }
1103
1104 pub fn start_cognition(&self) -> Result<()> {
1107 if self.cognition_running() {
1108 return Err(anyhow::anyhow!("cognition loop is already running"));
1109 }
1110 let atom_mem = self
1111 .atom_memory
1112 .as_ref()
1113 .ok_or_else(|| anyhow::anyhow!("meaning memory not enabled"))?;
1114 let tri_store = self
1115 .triple_store
1116 .as_ref()
1117 .ok_or_else(|| anyhow::anyhow!("meaning memory not enabled"))?;
1118
1119 let cl = CognitionLoop::start(
1120 Arc::clone(atom_mem),
1121 Arc::clone(tri_store),
1122 self.cognition_loop_config(),
1123 );
1124
1125 *self.cognition_loop.lock() = Some(cl);
1126 Ok(())
1127 }
1128
1129 pub fn stop_cognition(&self) {
1130 if let Some(ref mut cl) = *self.cognition_loop.lock() {
1131 cl.stop();
1132 }
1133 }
1134
1135 pub fn cognition_running(&self) -> bool {
1136 self.cognition_loop
1137 .lock()
1138 .as_ref()
1139 .is_some_and(|cl| cl.state().is_running())
1140 }
1141
1142 pub fn cognition_cycle_count(&self) -> u64 {
1143 self.cognition_loop
1144 .lock()
1145 .as_ref()
1146 .map_or(0, |cl| cl.state().cycle_count())
1147 }
1148
1149 pub fn take_insights(&self) -> Vec<Insight> {
1150 self.cognition_loop
1151 .lock()
1152 .as_ref()
1153 .map_or_else(Vec::new, |cl| cl.state().take_insights())
1154 }
1155
1156 pub fn cognition_insight_count(&self) -> usize {
1157 self.cognition_loop
1158 .lock()
1159 .as_ref()
1160 .map_or(0, |cl| cl.state().insight_count())
1161 }
1162
1163 fn cognition_loop_config(&self) -> CognitionLoopConfig {
1164 let cc = &self.config.cognition;
1165 CognitionLoopConfig {
1166 interval: std::time::Duration::from_secs(cc.interval_secs),
1167 min_pattern_freq: cc.min_pattern_freq,
1168 min_abstraction_members: cc.min_abstraction_members,
1169 min_shared_relations: cc.min_shared_relations,
1170 min_peer_coverage: cc.min_peer_coverage,
1171 hypothesis_beta: cc.hypothesis_beta,
1172 min_hypothesis_confidence: cc.min_hypothesis_confidence,
1173 min_analogy_relations: cc.min_analogy_relations,
1174 }
1175 }
1176
1177 pub fn run_cognition_once(&self) -> Vec<Insight> {
1178 let (atom_mem, tri_store) = match (&self.atom_memory, &self.triple_store) {
1179 (Some(a), Some(t)) => (a, t),
1180 _ => return Vec::new(),
1181 };
1182 let cfg = self.cognition_loop_config();
1183 CognitionLoop::run_once(atom_mem, tri_store, &cfg)
1184 }
1185
1186 pub fn govern_memory(&self) -> GovernanceReport {
1187 let (atom_mem, comp_mem, tri_store) = match (
1188 &self.atom_memory,
1189 &self.composite_memory,
1190 &self.triple_store,
1191 ) {
1192 (Some(a), Some(c), Some(t)) => (a, c, t),
1193 _ => return GovernanceReport::default(),
1194 };
1195 let cc = &self.config.cognition;
1196 let gov_config = GovernorConfig {
1197 duplicate_threshold: cc.governor_duplicate_threshold,
1198 max_scan_size: cc.governor_max_scan_size,
1199 forget_unreferenced_atoms: cc.governor_forget_unreferenced,
1200 refine_atoms: cc.refine_atoms,
1201 ..Default::default()
1202 };
1203 MemoryGovernor::govern(atom_mem, comp_mem, tri_store, &gov_config)
1204 }
1205
1206 pub fn cognition_enabled(&self) -> bool {
1207 self.config.cognition.enabled
1208 }
1209
1210 pub fn add_goal(
1213 &self,
1214 name: &str,
1215 description: &str,
1216 relevance: f64,
1217 urgency: f64,
1218 cost: f64,
1219 ) -> Option<usize> {
1220 let goal_store = self.goal_store.as_ref()?;
1221 let atom_mem = self.atom_memory.as_ref()?;
1222 let (_, vec) = atom_mem.get_or_insert(name);
1223 Some(goal_store.add(super::agency::goals::Goal {
1224 name: name.to_string(),
1225 description: description.to_string(),
1226 vector: vec,
1227 relevance,
1228 urgency,
1229 cost,
1230 active: true,
1231 }))
1232 }
1233
1234 pub fn deactivate_goal(&self, name: &str) -> bool {
1235 self.goal_store
1236 .as_ref()
1237 .is_some_and(|gs| gs.deactivate(name))
1238 }
1239
1240 pub fn active_goals(&self) -> Vec<(String, f64)> {
1241 self.goal_store.as_ref().map_or_else(Vec::new, |gs| {
1242 gs.prioritized()
1243 .iter()
1244 .map(|g| (g.name.clone(), g.utility()))
1245 .collect()
1246 })
1247 }
1248
1249 pub fn plan_goal(&self, goal: &str, causal_relations: &[&str], max_depth: usize) -> Plan {
1250 let tri = match &self.triple_store {
1251 Some(t) => t,
1252 None => {
1253 return Plan {
1254 goal: goal.to_string(),
1255 actions: Vec::new(),
1256 complete: false,
1257 total_cost: 0.0,
1258 }
1259 }
1260 };
1261 Planner::backward_chain(tri, goal, causal_relations, max_depth)
1262 }
1263
1264 pub fn generate_questions(&self) -> Vec<Question> {
1265 let (atom_mem, tri_store, goal_store) =
1266 match (&self.atom_memory, &self.triple_store, &self.goal_store) {
1267 (Some(a), Some(t), Some(g)) => (a, t, g),
1268 _ => return Vec::new(),
1269 };
1270
1271 let cc = &self.config.cognition;
1272 let gaps = super::cognition::gaps::GapDetector::detect(
1273 tri_store,
1274 cc.min_shared_relations,
1275 cc.min_peer_coverage,
1276 );
1277 let hypotheses = super::cognition::hypothesis::HypothesisEngine::propose(
1278 &gaps,
1279 tri_store,
1280 atom_mem,
1281 cc.hypothesis_beta,
1282 cc.min_hypothesis_confidence,
1283 );
1284
1285 let mut questions = Vec::new();
1286 questions.extend(QuestionGenerator::from_gaps(&gaps, atom_mem, goal_store));
1287 questions.extend(QuestionGenerator::from_hypotheses(
1288 &hypotheses,
1289 atom_mem,
1290 goal_store,
1291 ));
1292 QuestionGenerator::prioritize(questions)
1293 }
1294
1295 pub fn propose_rule(
1296 &self,
1297 name: &str,
1298 input_relations: Vec<String>,
1299 output_relation: &str,
1300 reason: &str,
1301 ) -> Option<usize> {
1302 let sm = self.self_modifier.as_ref()?;
1303 Some(sm.propose(
1304 ProposalKind::AddRule {
1305 name: name.to_string(),
1306 input_relations,
1307 output_relation: output_relation.to_string(),
1308 },
1309 reason.to_string(),
1310 ))
1311 }
1312
1313 pub fn approve_proposal(&self, id: usize) -> bool {
1314 self.self_modifier.as_ref().is_some_and(|sm| sm.approve(id))
1315 }
1316
1317 pub fn reject_proposal(&self, id: usize) -> bool {
1318 self.self_modifier.as_ref().is_some_and(|sm| sm.reject(id))
1319 }
1320
1321 pub fn pending_proposals(&self) -> usize {
1322 self.self_modifier
1323 .as_ref()
1324 .map_or(0, |sm| sm.pending_count())
1325 }
1326
1327 pub fn goal_count(&self) -> usize {
1328 self.goal_store.as_ref().map_or(0, |gs| gs.count())
1329 }
1330
1331 pub fn active_goal_count(&self) -> usize {
1332 self.goal_store.as_ref().map_or(0, |gs| gs.active_count())
1333 }
1334
1335 pub fn ivf_trained(&self) -> bool {
1337 self.shards.read().ivf_trained()
1338 }
1339
1340 pub fn train_ivf(&self) -> Result<()> {
1342 let (revision, snapshots) = {
1343 let _transaction = self.mutation_gate.read();
1344 let shards = self.shards.read();
1345 let mut snapshots = Vec::new();
1346 shards.for_each_shard(|shard| snapshots.push(shard.load_all_vectors()));
1347 (
1348 self.revision.load(std::sync::atomic::Ordering::Acquire),
1349 snapshots,
1350 )
1351 };
1352 let mut indices = Vec::with_capacity(snapshots.len());
1353 for (ids, vectors) in snapshots {
1354 indices.push(if ids.is_empty() {
1355 None
1356 } else {
1357 Some(IVFIndex::train(
1358 &vectors,
1359 &ids,
1360 self.dimensions,
1361 &self.config.ivf,
1362 )?)
1363 });
1364 }
1365 let _transaction = self.mutation_gate.write();
1366 anyhow::ensure!(
1367 revision == self.revision.load(std::sync::atomic::Ordering::Acquire),
1368 "store changed during training; retry maintenance"
1369 );
1370 let shards = self.shards.read();
1371 shards.try_for_each_shard_indexed(|i, shard| {
1372 *shard.ivf.write() = indices[i].take();
1373 Ok(())
1374 })?;
1375 self.persist_indices(&shards)
1376 }
1377
1378 pub fn nsg_trained(&self) -> bool {
1380 self.shards.read().nsg_trained()
1381 }
1382
1383 pub fn train_nsg(&self) -> Result<()> {
1385 let (revision, snapshots) = {
1386 let _transaction = self.mutation_gate.read();
1387 let shards = self.shards.read();
1388 let mut snapshots = Vec::new();
1389 shards.for_each_shard(|shard| snapshots.push(shard.load_all_vectors()));
1390 (
1391 self.revision.load(std::sync::atomic::Ordering::Acquire),
1392 snapshots,
1393 )
1394 };
1395 let mut indices = Vec::with_capacity(snapshots.len());
1396 for (ids, vectors) in snapshots {
1397 indices.push(if ids.is_empty() {
1398 None
1399 } else {
1400 Some(super::nsg::training::train(
1401 &vectors,
1402 &ids,
1403 &self.config.nsg,
1404 )?)
1405 });
1406 }
1407 let _transaction = self.mutation_gate.write();
1408 anyhow::ensure!(
1409 revision == self.revision.load(std::sync::atomic::Ordering::Acquire),
1410 "store changed during training; retry maintenance"
1411 );
1412 let shards = self.shards.read();
1413 shards.try_for_each_shard_indexed(|i, shard| {
1414 *shard.nsg.write() = indices[i].take();
1415 Ok(())
1416 })?;
1417 self.persist_indices(&shards)
1418 }
1419
1420 fn arena_write(&self, data: &[u8]) -> Result<usize> {
1421 let payload = self.maybe_encrypt(data)?;
1422 self.arena.write_slice(&payload)
1423 }
1424
1425 #[cfg(test)] fn arena_read_frame(&self, offset: usize) -> Result<(Vec<u8>, u32)> {
1427 let (data, version) = self.arena.read_frame(offset)?;
1428 Ok((self.maybe_decrypt(data)?, version))
1429 }
1430
1431 fn sign_fn(&self) -> Option<SignFn<'_>> {
1432 #[cfg(feature = "security")]
1433 {
1434 self.signing
1435 .as_ref()
1436 .map(|s| Box::new(move |data: &[u8]| s.sign(data)) as SignFn<'_>)
1437 }
1438 #[cfg(not(feature = "security"))]
1439 {
1440 None
1441 }
1442 }
1443
1444 pub fn audit_since(&self, timestamp_ms: u64) -> Result<Vec<super::audit::AuditEntry>> {
1447 match self.audit {
1448 Some(ref audit) => audit.entries_since(timestamp_ms),
1449 None => Ok(Vec::new()),
1450 }
1451 }
1452
1453 #[cfg(feature = "provenance")]
1456 pub fn provenance_enabled(&self) -> bool {
1457 self.provenance.is_some()
1458 }
1459
1460 #[cfg(feature = "provenance")]
1461 pub fn create_fact_provenance(
1462 &self,
1463 fact_id: &str,
1464 content: &[u8],
1465 source_uri: Option<&str>,
1466 ) -> Result<super::provenance::types::ProvenanceRecord> {
1467 let mgr = self
1468 .provenance
1469 .as_ref()
1470 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1471 mgr.create_fact_provenance(fact_id, content, source_uri)
1472 }
1473
1474 #[cfg(feature = "provenance")]
1475 pub fn create_triple_provenance(
1476 &self,
1477 params: &super::provenance::TripleProvenanceParams<'_>,
1478 ) -> Result<super::provenance::types::ProvenanceRecord> {
1479 let mgr = self
1480 .provenance
1481 .as_ref()
1482 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1483 mgr.create_triple_provenance(params)
1484 }
1485
1486 #[cfg(feature = "provenance")]
1487 pub fn create_store_manifest(
1488 &self,
1489 store_id: &str,
1490 store_data: &[u8],
1491 fact_count: usize,
1492 dimensions: u32,
1493 title: Option<&str>,
1494 ) -> Result<super::provenance::types::StoreManifest> {
1495 let mgr = self
1496 .provenance
1497 .as_ref()
1498 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1499 mgr.create_store_manifest(store_id, store_data, fact_count, dimensions, title)
1500 }
1501
1502 #[cfg(feature = "provenance")]
1503 pub fn verify_fact_provenance(
1504 &self,
1505 record: &super::provenance::types::ProvenanceRecord,
1506 trust: &super::provenance::trust::TrustStore,
1507 ) -> Result<super::provenance::types::VerificationResult> {
1508 let mgr = self
1509 .provenance
1510 .as_ref()
1511 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1512 mgr.verify_fact_provenance(record, trust)
1513 }
1514
1515 #[cfg(feature = "provenance")]
1518 pub fn provenance_self_trust(&self) -> Option<super::provenance::trust::TrustStore> {
1519 self.provenance.as_ref().map(|mgr| mgr.self_trust())
1520 }
1521
1522 #[cfg(feature = "provenance")]
1523 pub fn verify_store_manifest(
1524 &self,
1525 manifest: &super::provenance::types::StoreManifest,
1526 store_data: Option<&[u8]>,
1527 trust: &super::provenance::trust::TrustStore,
1528 ) -> Result<super::provenance::types::VerificationResult> {
1529 let mgr = self
1530 .provenance
1531 .as_ref()
1532 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1533 mgr.verify_store_manifest(manifest, store_data, trust)
1534 }
1535
1536 #[cfg(feature = "provenance")]
1537 pub fn issuer_did(&self) -> Option<&str> {
1538 self.provenance.as_ref().map(|mgr| mgr.issuer_did())
1539 }
1540
1541 #[cfg(feature = "provenance")]
1542 pub fn get_provenance(
1543 &self,
1544 fact_id: &str,
1545 ) -> Option<super::provenance::types::ProvenanceRecord> {
1546 self.provenance.as_ref()?.get_record(fact_id)
1547 }
1548
1549 #[cfg(feature = "provenance")]
1550 pub fn provenance_count(&self) -> usize {
1551 self.provenance.as_ref().map_or(0, |mgr| mgr.record_count())
1552 }
1553
1554 #[cfg(feature = "provenance")]
1555 pub fn create_self_manifest(
1556 &self,
1557 title: Option<&str>,
1558 ) -> Result<super::provenance::types::StoreManifest> {
1559 let mgr = self
1560 .provenance
1561 .as_ref()
1562 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1563 let store_data =
1564 std::fs::read(self.storage_path.join("vectors_data.bin")).unwrap_or_default();
1565 let store_id = self
1566 .storage_path
1567 .file_name()
1568 .and_then(|n| n.to_str())
1569 .unwrap_or("default");
1570 mgr.create_store_manifest(
1571 store_id,
1572 &store_data,
1573 self.vector_count() as usize,
1574 self.dimensions as u32,
1575 title,
1576 )
1577 }
1578
1579 #[cfg(feature = "provenance")]
1580 pub fn revoke_credential(&self, status_index: u64) -> Result<()> {
1581 let mgr = self
1582 .provenance
1583 .as_ref()
1584 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1585 mgr.revoke_credential(status_index)
1586 }
1587
1588 #[cfg(feature = "provenance")]
1589 pub fn is_credential_revoked(&self, status_index: u64) -> bool {
1590 self.provenance
1591 .as_ref()
1592 .is_some_and(|mgr| mgr.is_revoked(status_index))
1593 }
1594
1595 #[cfg(feature = "provenance")]
1596 pub fn verify_provenance_log(&self) -> Result<bool> {
1597 let mgr = self
1598 .provenance
1599 .as_ref()
1600 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1601 mgr.verify_log_integrity()
1602 }
1603
1604 #[cfg(feature = "provenance")]
1605 pub fn create_batch_provenance(
1606 &self,
1607 items: &[(&str, &[u8], Option<&str>)],
1608 ) -> Result<Vec<super::provenance::types::ProvenanceRecord>> {
1609 let mgr = self
1610 .provenance
1611 .as_ref()
1612 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1613 mgr.create_batch_provenance(items)
1614 }
1615
1616 #[cfg(feature = "provenance")]
1617 pub fn create_sigstore_bundle(
1618 &self,
1619 content: &[u8],
1620 identity: Option<&str>,
1621 ) -> Result<super::provenance::sigstore::SigstoreBundle> {
1622 let mgr = self
1623 .provenance
1624 .as_ref()
1625 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1626 mgr.create_sigstore_bundle(content, identity)
1627 }
1628
1629 #[cfg(feature = "provenance")]
1630 pub fn verify_sigstore_bundle(
1631 &self,
1632 bundle: &super::provenance::sigstore::SigstoreBundle,
1633 content: &[u8],
1634 trusted_key: &ed25519_dalek::VerifyingKey,
1635 ) -> Result<()> {
1636 let mgr = self
1637 .provenance
1638 .as_ref()
1639 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1640 mgr.verify_sigstore_bundle(bundle, content, trusted_key)
1641 }
1642
1643 #[cfg(feature = "provenance")]
1644 pub fn create_cawg_assertion(
1645 &self,
1646 referenced: &[(String, String, Vec<u8>)],
1647 display_name: &str,
1648 ) -> Result<serde_json::Value> {
1649 let mgr = self
1650 .provenance
1651 .as_ref()
1652 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1653 mgr.create_cawg_assertion(referenced, display_name)
1654 }
1655
1656 #[cfg(feature = "provenance")]
1657 pub fn verify_cawg_assertion(
1658 &self,
1659 assertion: &serde_json::Value,
1660 trust: &super::provenance::trust::TrustStore,
1661 ) -> Result<serde_json::Value> {
1662 let mgr = self
1663 .provenance
1664 .as_ref()
1665 .ok_or_else(|| anyhow::anyhow!("provenance not enabled"))?;
1666 mgr.verify_cawg_assertion(assertion, trust)
1667 }
1668
1669 pub fn factorize_diffusion(
1671 &self,
1672 product: &EntangledHVec,
1673 domain_codebooks: &[Vec<EntangledHVec>],
1674 max_iter: usize,
1675 ) -> Vec<Option<EntangledHVec>> {
1676 DiffusionFactorizer::factorize(&self.config.diffusion, product, domain_codebooks, max_iter)
1677 }
1678
1679 #[cfg(feature = "experimental")]
1683 fn connection_graph_lazy(
1684 &self,
1685 ) -> parking_lot::MutexGuard<'_, Option<super::connection_graph::ConnectionGraph>> {
1686 let mut guard = self.connection_graph.lock();
1687 if guard.is_none() {
1688 let path = self.storage_path.join("connection_graph.log");
1689 let graph = super::connection_graph::ConnectionGraph::open(
1690 self.dimensions,
1691 super::connection_graph::GraphConfig::default(),
1692 &path,
1693 )
1694 .unwrap_or_else(|e| {
1695 tracing::warn!("connection graph persistence unavailable ({e}); in-memory only");
1696 super::connection_graph::ConnectionGraph::new(self.dimensions)
1697 });
1698 *guard = Some(graph);
1699 }
1700 guard
1701 }
1702
1703 #[cfg(feature = "experimental")]
1707 pub fn relate(&self, subject: &str, relation: &str, object: &str) {
1708 let mut guard = self.connection_graph_lazy();
1709 if let Some(g) = guard.as_mut() {
1710 g.store(subject, relation, object);
1711 }
1712 }
1713
1714 #[cfg(feature = "experimental")]
1719 pub fn query_relation(&self, subject: &str, relation: &str, object: &str) -> f64 {
1720 let mut guard = self.connection_graph_lazy();
1721 guard
1722 .as_mut()
1723 .map_or(0.0, |g| g.query(subject, relation, object))
1724 }
1725
1726 #[cfg(feature = "experimental")]
1730 fn phase_graph_lazy(
1731 &self,
1732 ) -> parking_lot::MutexGuard<'_, Option<super::phase_graph::PhaseGraph>> {
1733 let mut guard = self.phase_graph.lock();
1734 if guard.is_none() {
1735 *guard = Some(super::phase_graph::PhaseGraph::new(2048, 256));
1736 }
1737 guard
1738 }
1739
1740 #[cfg(feature = "experimental")]
1744 pub fn relate_phase(&self, subject: &str, relation: &str, object: &str) {
1745 self.phase_graph_lazy()
1746 .as_mut()
1747 .expect("lazily initialized")
1748 .relate(subject, relation, object);
1749 }
1750
1751 #[cfg(feature = "experimental")]
1753 pub fn phase_retrieve_object(&self, subject: &str, relation: &str) -> Option<String> {
1754 self.phase_graph_lazy()
1755 .as_ref()
1756 .and_then(|g| g.retrieve_object(subject, relation).map(String::from))
1757 }
1758
1759 #[cfg(feature = "experimental")]
1762 pub fn phase_retrieve_subject(&self, relation: &str, object: &str) -> Option<String> {
1763 self.phase_graph_lazy()
1764 .as_ref()
1765 .and_then(|g| g.retrieve_subject(relation, object).map(String::from))
1766 }
1767
1768 #[cfg(feature = "experimental")]
1771 pub fn phase_retrieve_path(&self, start: &str, relations: &[&str]) -> Option<String> {
1772 self.phase_graph_lazy()
1773 .as_ref()
1774 .and_then(|g| g.retrieve_path(start, relations))
1775 }
1776}
1777
1778#[cfg(all(test, feature = "experimental"))]
1779mod connection_graph_tests {
1780 use super::*;
1781
1782 #[test]
1783 fn relate_and_query_through_engine() {
1784 let dir = tempfile::tempdir().unwrap();
1785 let hms = HmsCore::new(4096, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1786
1787 assert_eq!(hms.query_relation("paris", "capital_of", "france"), 0.0);
1789
1790 hms.relate("paris", "capital_of", "france");
1791 let present = hms.query_relation("paris", "capital_of", "france");
1792 let absent = hms.query_relation("berlin", "capital_of", "spain");
1793 assert!(
1794 present > absent,
1795 "asserted relation ({present}) must score above an absent one ({absent})"
1796 );
1797 }
1798
1799 #[test]
1800 fn connection_graph_survives_restart() {
1801 let dir = tempfile::tempdir().unwrap();
1802 let path = dir.path().to_string_lossy().to_string();
1803
1804 {
1805 let hms = HmsCore::new(4096, Some(path.clone()), None).unwrap();
1806 hms.relate("paris", "capital_of", "france");
1807 } let hms2 = HmsCore::new(4096, Some(path), None).unwrap();
1811 let present = hms2.query_relation("paris", "capital_of", "france");
1812 let absent = hms2.query_relation("madrid", "capital_of", "italy");
1813 assert!(
1814 present > absent,
1815 "relation must survive restart: reloaded ({present}) !> absent ({absent})"
1816 );
1817 }
1818
1819 #[test]
1820 fn phase_graph_relate_and_retrieve() {
1821 let dir = tempfile::tempdir().unwrap();
1822 let hms = HmsCore::new(4096, Some(dir.path().to_string_lossy().to_string()), None).unwrap();
1823 hms.relate_phase("paris", "capital_of", "france");
1824 hms.relate_phase("berlin", "capital_of", "germany");
1825 assert_eq!(
1827 hms.phase_retrieve_object("paris", "capital_of").as_deref(),
1828 Some("france")
1829 );
1830 assert_eq!(
1831 hms.phase_retrieve_subject("capital_of", "germany")
1832 .as_deref(),
1833 Some("berlin")
1834 );
1835 }
1836}