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