Skip to main content

holographic_memory/core/engine/
mod.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4mod 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
29/// Mutations decoded and applied together while replaying a journal frame.
30const 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
64/// Persistent operations take mutation_gate before ShardSet and component locks.
65pub struct HmsCore {
66    config: HmsConfig,
67    mutation_gate: mutation_gate::MutationGate,
68    /// Word interner carried from one document batch to the next.
69    stem_memos: crate::core::documents::StemMemoPool,
70    compaction: parking_lot::Mutex<()>,
71    /// The index metadata (`indices.json`) last written by this process: identical metadata
72    /// means identical index files at the same checkpoint, so a repeated persist skips the
73    /// writes.
74    persisted_indices: parking_lot::Mutex<Vec<u8>>,
75    revision: std::sync::atomic::AtomicU64,
76    /// The frame format this store is written in (its `store.json` schema); both are read.
77    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    /// Snapshot answering unfiltered vector queries (`search_vectors`); built by maintenance.
85    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    /// Experimental opt-in plastic relation store (lazily created on first use).
112    #[cfg(feature = "experimental")]
113    connection_graph: parking_lot::Mutex<Option<super::connection_graph::ConnectionGraph>>,
114    /// Experimental opt-in phasor relational memory (lazily created on first use).
115    #[cfg(feature = "experimental")]
116    phase_graph: parking_lot::Mutex<Option<super::phase_graph::PhaseGraph>>,
117    // Declared last so it is dropped after every mmap, index, and store
118    // component. No subsequent instance can enter while teardown is flushing.
119    _store_lock: StoreLock,
120}
121
122impl HmsCore {
123    /// Create a new HMS instance. If `storage_path` is None, uses the current directory.
124    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            // A store that auto-sharded in a previous run persists its shard
230            // count so the topology (and per-shard indices) reload correctly.
231            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        // A purge interrupted before renaming its canonical segment leaves a copy of the live
308        // text, which a later purge would not reach.
309        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    /// Read the persisted shard count written by a prior auto-shard, if any.
333    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    /// Per-shard NSG index path. Single-shard stores keep the legacy filename so
349    /// existing on-disk indices continue to load.
350    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    /// Persist every shard's trained NSG/IVF index to disk. Applies to both
438    /// single- and multi-shard stores; multi-shard indices are written to
439    /// per-shard files so they reload after a restart.
440    pub(super) fn persist_indices(&self, shards: &ShardSet) -> Result<()> {
441        // The checkpoint is read before the encoding: an append in between leaves the
442        // metadata behind the arena, which the loader treats as stale (see `encode_indices`).
443        let checkpoint = self.arena.checkpoint();
444        let encoded = self.encode_indices(shards, checkpoint)?;
445        self.write_indices(encoded)
446    }
447
448    /// Serializes every shard's trained NSG/IVF index with the metadata that binds the files
449    /// to `checkpoint`. The caller holds `shards` while the encoding reads them; the files
450    /// are written by [`Self::write_indices`] without any lock. A checkpoint read before a
451    /// mutation that the encoded indices already include only makes the loader rebuild them;
452    /// a checkpoint read after a mutation they lack would make it trust stale indices, so the
453    /// checkpoint must be read while no writer can be in flight (under a writer lease or an
454    /// exclusive publication).
455    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    /// Writes the encoded index files and metadata, unless this process already wrote
489    /// byte-identical metadata (same checkpoint, same file checksums), which makes the
490    /// write a no-op.
491    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
507/// Serialized index files and the metadata binding them to an arena checkpoint.
508pub(super) struct EncodedIndices {
509    files: Vec<(PathBuf, Vec<u8>)>,
510    metadata: Vec<u8>,
511}
512
513impl HmsCore {
514    /// Bundle vectors respecting the PrivacyConfig.
515    /// When dp_enabled, uses Laplace noise with the configured epsilon.
516    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    /// Decrypts `data` when the store is encrypted; a plaintext buffer is returned as is.
533    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            // Only an encrypted store can fail here, and the first frame that does is almost
550            // always a wrong passphrase rather than damage, so say so instead of hiding it.
551            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            // The frame is applied; its pages stay on disk for the next replay or compaction.
571            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    /// Decodes one committed journal frame and applies it in order, `REPLAY_CHUNK` mutations
585    /// at a time, so a frame written by one large batch is never held decoded in full. The
586    /// frame passed its checksum; an invalid mutation still fails the open, as before.
587    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    /// [`Self::replay_frame`] for an `HMS-TXN-3` payload body: mutations are decoded and
642    /// applied `REPLAY_CHUNK` at a time, the frame's term dictionary held meanwhile.
643    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    /// `mutations` as a journal payload in this store's format.
664    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    /// Returns the dimensionality of the hypervector space.
672    pub fn dimensions(&self) -> usize {
673        self.dimensions
674    }
675
676    /// Encode text into a sparse hypervector using multi-scale n-grams and word tokens.
677    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    /// Compute word, sentence, syllable, and character-class counts for text.
686    pub fn analyze_text(&self, text: &str) -> TextMetrics {
687        TextProcessor::analyze(text)
688    }
689
690    /// Compute Flesch Reading Ease score from text metrics.
691    pub fn calculate_readability(&self, metrics: &TextMetrics) -> f64 {
692        TextProcessor::calculate_readability(metrics)
693    }
694
695    /// Delete a vector by ID. Returns true if it existed. Crash-safe: tombstone is persisted first.
696    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    /// Store a vector with the given ID. Persists to the arena log and updates all indices.
771    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    /// Convert a dense f32 vector to sparse and memorize it.
826    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    /// Encode a bounded scalar value as a hypervector and memorize it.
833    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    /// Returns the total number of stored vectors across all shards.
839    pub fn vector_count(&self) -> u64 {
840        self.shards.read().count()
841    }
842
843    // === Graph API ===
844
845    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    // === Federated Query ===
918
919    /// Query across local instance and remote peers in parallel.
920    ///
921    /// Improvements over naive collect-sort-truncate:
922    /// - **BinaryHeap merge**: O(N log k) top-k selection instead of O(N log N) sort.
923    /// - **ID deduplication**: same document appearing in multiple peers is counted once
924    ///   (highest similarity wins).
925    /// - **Partial failure tolerance**: a failed peer logs a warning and is skipped
926    ///   rather than aborting the entire query.
927    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        // Query local instance
939        let local_results = self.query(query_vec, k as u32);
940
941        // Query each peer in parallel; collect successes and log failures
942        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        // Deduplicate by ID, keeping highest similarity per ID.
956        let mut seen: fxhash::FxHashMap<String, f64> =
957            fxhash::FxHashMap::with_capacity_and_hasher(k * 2, Default::default());
958
959        // BinaryHeap with RetrievalResult's Ord (min-heap by similarity):
960        // pop() removes the lowest-similarity item, so we maintain top-k.
961        let mut heap: BinaryHeap<super::types::RetrievalResult> = BinaryHeap::with_capacity(k + 1);
962
963        let mut insert = |r: super::types::RetrievalResult| {
964            // Dedup: skip if we already have this ID with equal-or-higher similarity
965            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        // `RetrievalResult`'s reversed `Ord` makes ascending order highest-similarity first.
998        Ok(heap.into_sorted_vec())
999    }
1000
1001    // === Meaning Memory API ===
1002
1003    /// Build the shared meaning-memory context, if all required subsystems are
1004    /// present. Returns `None` when meaning memory is not fully initialized.
1005    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    // === Cognition API ===
1105
1106    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    // === Agency API ===
1211
1212    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    /// Returns true if the IVF index has been trained.
1336    pub fn ivf_trained(&self) -> bool {
1337        self.shards.read().ivf_trained()
1338    }
1339
1340    /// Train the IVF index on current vectors. Persists the index to disk.
1341    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    /// Returns true if the NSG graph index has been trained.
1379    pub fn nsg_trained(&self) -> bool {
1380        self.shards.read().nsg_trained()
1381    }
1382
1383    /// Train the NSG graph index on current vectors. Persists the index to disk.
1384    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)] // replay reads and decrypts in two steps to tell a bad frame from a wrong passphrase
1426    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    /// Query the audit log for entries since `timestamp_ms`.
1445    /// Returns an empty vec if audit logging is disabled.
1446    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    // === Provenance API ===
1454
1455    #[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    /// A trust store containing this instance's own provenance key, for
1516    /// authenticating records it signed. `None` if provenance is disabled.
1517    #[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    /// Decompose a product vector into factors from domain codebooks using diffusion.
1670    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    /// Lock the connection graph, lazily opening it over its durable append-only
1680    /// event log at `{storage}/connection_graph.log` on first use so plastic
1681    /// state survives restart. Falls back to in-memory if the log is unopenable.
1682    #[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    /// Assert a `(subject, relation, object)` edge into the experimental plastic
1704    /// connection graph. Opt-in and additive: independent of the sparse-binary
1705    /// store. Durable — the edge is appended to the graph's event log.
1706    #[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    /// Query a relation in the plastic connection graph, returning its presence
1715    /// score. This is a *mutating* read (the observer effect): it reinforces the
1716    /// queried relation and lets the untouched background decay, all recorded as
1717    /// verifiable, durable events.
1718    #[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    /// Dimension and phase-quantization of the experimental phasor memory. A
1727    /// modest dim keeps the (dim * n_phases) histogram small; retrieval works
1728    /// well past this substrate's load in the validation experiments.
1729    #[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    /// Assert `(subject, relation, object)` into the experimental phasor memory,
1741    /// which supports relation algebra (rotation binding) and associative
1742    /// retrieval the sparse-binary store cannot. Opt-in and additive.
1743    #[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    /// Recover the object of `(subject, relation)` from the phasor memory.
1752    #[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    /// Recover the subject of `(relation, object)` — the inverse query — from the
1760    /// phasor memory.
1761    #[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    /// Multi-hop reasoning over the phasor memory: follow `relations` from
1769    /// `start`, retrieving at each hop, returning the final entity.
1770    #[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        // Query before any relation exists -> graph unused -> 0.0.
1788        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        } // engine dropped -> connection graph's BufWriter flushed on drop
1808
1809        // New engine over the same storage: the relation must reload from the log.
1810        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        // forward retrieval and inverse (subject) retrieval from the same field.
1826        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}