Skip to main content

writerslogic_warm/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;
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
33/// Mutations decoded and applied together while replaying a journal frame.
34const 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/// A configuration whose store holds `dimensions`-wide fixture embeddings.
69#[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
83/// Persistent operations take mutation_gate before ShardSet and component locks.
84pub struct WarmCore {
85    config: WarmConfig,
86    mutation_gate: mutation_gate::MutationGate,
87    /// Word interner carried from one document batch to the next.
88    stem_memos: crate::core::documents::StemMemoPool,
89    compaction: crate::core::sync::Mutex<()>,
90    /// The index metadata (`indices.json`) last written by this process: identical metadata
91    /// means identical index files at the same checkpoint, so a repeated persist skips the
92    /// writes.
93    persisted_indices: crate::core::sync::Mutex<Vec<u8>>,
94    revision: std::sync::atomic::AtomicU64,
95    /// The frame format this store is written in (its `store.json` schema); both are read.
96    journal: journal::JournalFormat,
97    /// `store.json` schema version of the open store.
98    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    /// Snapshot answering unfiltered vector queries (`search_vectors`); built by maintenance.
107    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    /// Experimental opt-in plastic relation store (lazily created on first use).
133    #[cfg(feature = "experimental")]
134    connection_graph: crate::core::sync::Mutex<Option<super::connection_graph::ConnectionGraph>>,
135    /// Experimental opt-in phasor relational memory (lazily created on first use).
136    #[cfg(feature = "experimental")]
137    phase_graph: crate::core::sync::Mutex<Option<super::phase_graph::PhaseGraph>>,
138    // Declared last so it is dropped after every mmap, index, and store
139    // component. No subsequent instance can enter while teardown is flushing.
140    _store_lock: StoreLock,
141}
142
143impl WarmCore {
144    /// Create a new WARM instance. If `storage_path` is None, uses the current directory.
145    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            // A store that auto-sharded in a previous run persists its shard
256            // count so the topology (and per-shard indices) reload correctly.
257            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        // A purge interrupted before renaming its canonical segment leaves a copy of the live
336        // text, which a later purge would not reach.
337        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        // Only a completely opened engine may write an image when it closes.
359        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    /// Read the persisted shard count written by a prior auto-shard, if any. An unreadable or
369    /// malformed file reads as absent; `warm-admin verify` reports it as `shard-meta-invalid`.
370    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    /// Per-shard NSG index path. Single-shard stores keep the legacy filename so
386    /// existing on-disk indices continue to load.
387    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    /// Persist every shard's trained NSG/IVF index to disk. Applies to both
475    /// single- and multi-shard stores; multi-shard indices are written to
476    /// per-shard files so they reload after a restart.
477    pub(super) fn persist_indices(&self, shards: &ShardSet) -> Result<()> {
478        // The checkpoint is read before the encoding: an append in between leaves the
479        // metadata behind the arena, which the loader treats as stale (see `encode_indices`).
480        let checkpoint = self.arena.checkpoint();
481        let encoded = self.encode_indices(shards, checkpoint)?;
482        self.write_indices(encoded)
483    }
484
485    /// Serializes every shard's trained NSG/IVF index with the metadata that binds the files
486    /// to `checkpoint`. The caller holds `shards` while the encoding reads them; the files
487    /// are written by [`Self::write_indices`] without any lock. A checkpoint read before a
488    /// mutation that the encoded indices already include only makes the loader rebuild them;
489    /// a checkpoint read after a mutation they lack would make it trust stale indices, so the
490    /// checkpoint must be read while no writer can be in flight (under a writer lease or an
491    /// exclusive publication).
492    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    /// Writes the encoded index files and metadata, unless this process already wrote
526    /// byte-identical metadata (same checkpoint, same file checksums), which makes the
527    /// write a no-op.
528    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
544/// Serialized index files and the metadata binding them to an arena checkpoint.
545pub(super) struct EncodedIndices {
546    files: Vec<(PathBuf, Vec<u8>)>,
547    metadata: Vec<u8>,
548}
549
550impl WarmCore {
551    /// Bundle vectors respecting the PrivacyConfig.
552    /// When dp_enabled, uses Laplace noise with the configured epsilon.
553    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    /// Decrypts `data` when the store is encrypted; a plaintext buffer is returned as is.
570    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            // Only an encrypted store can fail here, and the first frame that does is almost
587            // always a wrong passphrase rather than damage, so say so instead of hiding it.
588            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            // The frame is applied; its pages stay on disk for the next replay or compaction.
608            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    /// Decodes one committed journal frame and applies it in order, `REPLAY_CHUNK` mutations
622    /// at a time, so a frame written by one large batch is never held decoded in full. The
623    /// frame passed its checksum; an invalid mutation still fails the open, as before.
624    /// Applies replayed mutations; ones a store served from a checkpoint cannot take
625    /// rebuild it first, as they would have when committed.
626    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    /// [`Self::replay_frame`] for an `HMS-TXN-3` payload body: mutations are decoded and
688    /// applied `REPLAY_CHUNK` at a time, the frame's term dictionary held meanwhile.
689    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    /// `mutations` as a journal payload in this store's format.
710    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    /// Returns the dimensionality of the hypervector space.
718    pub fn dimensions(&self) -> usize {
719        self.dimensions
720    }
721
722    /// Encode text; panics on model/resource failure. Use `try_encode_text` for fallible models.
723    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    /// Compute word, sentence, syllable, and character-class counts for text.
749    pub fn analyze_text(&self, text: &str) -> TextMetrics {
750        TextProcessor::analyze(text)
751    }
752
753    /// Compute Flesch Reading Ease score from text metrics.
754    pub fn calculate_readability(&self, metrics: &TextMetrics) -> f64 {
755        TextProcessor::calculate_readability(metrics)
756    }
757
758    /// Delete a vector by ID. Returns true if it existed. Crash-safe: tombstone is persisted first.
759    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    /// Store a vector with the given ID. Persists to the arena log and updates all indices.
848    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    /// Convert a dense f32 vector to sparse and memorize it.
906    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    /// Encode a bounded scalar value as a hypervector and memorize it.
913    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    /// Returns the total number of stored vectors across all shards.
919    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    /// Query across local instance and remote peers in parallel.
996    ///
997    /// Improvements over naive collect-sort-truncate:
998    /// - **BinaryHeap merge**: O(N log k) top-k selection instead of O(N log N) sort.
999    /// - **ID deduplication**: same document appearing in multiple peers is counted once
1000    ///   (highest similarity wins).
1001    /// - **Partial failure tolerance**: a failed peer logs a warning and is skipped
1002    ///   rather than aborting the entire query.
1003    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        // BinaryHeap with RetrievalResult's Ord (min-heap by similarity):
1033        // pop() removes the lowest-similarity item, so we maintain top-k.
1034        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        // `RetrievalResult`'s reversed `Ord` makes ascending order highest-similarity first.
1070        Ok(heap.into_sorted_vec())
1071    }
1072
1073    /// Build the shared meaning-memory context, if all required subsystems are
1074    /// present. Returns `None` when meaning memory is not fully initialized.
1075    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    /// Uses default QueryLimits; panics if a work, frontier or byte limit is exhausted.
1112    /// Use [`Self::multi_hop_with_limits`] to handle budget errors.
1113    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    /// Returns true if the IVF index has been trained.
1387    pub fn ivf_trained(&self) -> bool {
1388        self.shards.read().ivf_trained()
1389    }
1390
1391    /// Train the IVF index on current vectors. Persists the index to disk.
1392    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    /// Returns true if the NSG graph index has been trained.
1400    pub fn nsg_trained(&self) -> bool {
1401        self.shards.read().nsg_trained()
1402    }
1403
1404    /// Train the NSG graph index on current vectors. Persists the index to disk.
1405    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    /// Trains one index per shard from a snapshot taken under the read gate, without holding
1413    /// the gate, then installs and persists them only if no mutation landed meanwhile.
1414    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)] // replay reads and decrypts in two steps to tell a bad frame from a wrong passphrase
1456    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    /// Query the audit log for entries since `timestamp_ms`.
1475    /// Returns an empty vec if audit logging is disabled.
1476    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    /// A trust store containing this instance's own provenance key, for
1539    /// authenticating records it signed. `None` if provenance is disabled.
1540    #[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    /// Decompose a product vector into factors from domain codebooks using diffusion.
1678    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    /// Lock the connection graph, lazily opening it over its durable append-only
1688    /// event log at `{storage}/connection_graph.log` on first use so plastic
1689    /// state survives restart. Falls back to in-memory if the log is unopenable.
1690    #[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    /// Assert a `(subject, relation, object)` edge into the experimental plastic
1712    /// connection graph. Opt-in and additive: independent of the sparse-binary
1713    /// store. Durable — the edge is appended to the graph's event log.
1714    #[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    /// Query a relation in the plastic connection graph, returning its presence
1723    /// score. This is a *mutating* read (the observer effect): it reinforces the
1724    /// queried relation and lets the untouched background decay, all recorded as
1725    /// verifiable, durable events.
1726    #[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    /// Dimension and phase-quantization of the experimental phasor memory. A
1735    /// modest dim keeps the (dim * n_phases) histogram small; retrieval works
1736    /// well past this substrate's load in the validation experiments.
1737    #[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    /// Assert `(subject, relation, object)` into the experimental phasor memory,
1749    /// which supports relation algebra (rotation binding) and associative
1750    /// retrieval the sparse-binary store cannot. Opt-in and additive.
1751    #[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    /// Recover the object of `(subject, relation)` from the phasor memory.
1760    #[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    /// Recover the subject of `(relation, object)` — the inverse query — from the
1768    /// phasor memory.
1769    #[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    /// Multi-hop reasoning over the phasor memory: follow `relations` from
1777    /// `start`, retrieving at each hop, returning the final entity.
1778    #[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        // Query before any relation exists -> graph unused -> 0.0.
1797        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        } // engine dropped -> connection graph's BufWriter flushed on drop
1817
1818        // New engine over the same storage: the relation must reload from the log.
1819        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        // forward retrieval and inverse (subject) retrieval from the same field.
1836        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    /// Closing writes a checkpoint when one is due (see `checkpoint_if_due`), so the next open
1865    /// maps it instead of replaying the frames. A failure leaves the journal as it was.
1866    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}