consciousness/src/hippocampus/store/mod.rs
Kent Overstreet c2de14dcab store: add reindex_relations for after Vec mutations
- Add index::clear_relations() to drop and recreate RELS table
- Add Store::reindex_relations() to rebuild index from Vec
- Call reindex_relations() at end of dedup command

This ensures index stays in sync with Vec after complex mutations
like UUID redirection in dedup. Vec mutations remain for now but
index is correctly updated afterward.

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-13 21:24:49 -04:00

171 lines
5.4 KiB
Rust

// Append-only Cap'n Proto storage + redb indices
//
// capnp logs are the source of truth:
// nodes.capnp - ContentNode messages
// relations.capnp - Relation messages
//
// redb provides indexed access; Store struct holds in-memory state.
//
// Module layout:
// types.rs — Node, Relation, enums, path/time helpers
// capnp.rs — serialization macros, log IO (load, replay, append, fsck)
// index.rs — redb index operations
// ops.rs — mutations (upsert, delete, rename, etc.)
// view.rs — StoreView trait for read-only access
mod types;
mod index;
mod capnp;
mod ops;
mod view;
// Re-export everything callers need
pub use types::{
memory_dir, nodes_path,
now_epoch, epoch_to_local, format_date, format_datetime, format_datetime_space, compact_timestamp, today,
Node, Relation, NodeType, RelationType,
new_node, new_relation,
};
pub use view::StoreView;
pub use capnp::fsck;
pub use ops::current_provenance;
use crate::graph::{self, Graph};
use anyhow::{bail, Result};
/// Strip .md suffix from a key, handling both bare keys and section keys.
/// "identity.md" → "identity", "foo.md#section" → "foo#section", "identity" → "identity"
pub fn strip_md_suffix(key: &str) -> String {
if let Some((file, section)) = key.split_once('#') {
let bare = file.strip_suffix(".md").unwrap_or(file);
format!("{}#{}", bare, section)
} else {
key.strip_suffix(".md").unwrap_or(key).to_string()
}
}
// The full in-memory store
pub struct Store {
pub relations: Vec<Relation>, // all active relations
/// Log sizes at load time — used for staleness detection.
pub(crate) loaded_nodes_size: u64,
pub(crate) loaded_rels_size: u64,
/// redb index database
pub(crate) db: Option<redb::Database>,
}
impl Default for Store {
fn default() -> Self {
Store {
relations: Vec::new(),
loaded_nodes_size: 0,
loaded_rels_size: 0,
db: None,
}
}
}
impl Store {
pub fn build_graph(&self) -> Graph {
graph::build_graph(self)
}
/// Get a node by key, reading from capnp via the index.
pub fn get_node(&self, key: &str) -> Result<Option<Node>> {
let db = self.db.as_ref()
.ok_or_else(|| anyhow::anyhow!("store not loaded"))?;
match index::get_offset(db, key)? {
Some(offset) => Ok(Some(capnp::read_node_at_offset(offset)?)),
None => Ok(None),
}
}
/// Check if a node exists by key.
pub fn contains_key(&self, key: &str) -> Result<bool> {
let db = self.db.as_ref()
.ok_or_else(|| anyhow::anyhow!("store not loaded"))?;
index::contains_key(db, key)
}
/// Get all node keys.
pub fn all_keys(&self) -> Result<Vec<String>> {
let db = self.db.as_ref()
.ok_or_else(|| anyhow::anyhow!("store not loaded"))?;
index::all_keys(db)
}
/// Get neighbors of a node: (key, strength) pairs.
pub fn neighbors(&self, key: &str) -> Result<Vec<(String, f32)>> {
let db = self.db.as_ref()
.ok_or_else(|| anyhow::anyhow!("store not loaded"))?;
let uuid = match index::get_uuid_for_key(db, key)? {
Some(u) => u,
None => return Ok(Vec::new()),
};
let edges = index::edges_for_node(db, &uuid)?;
let mut neighbors = Vec::new();
for (other_uuid, strength, _, _) in edges {
// Look up key for other_uuid
let offsets = index::get_offsets_for_uuid(db, &other_uuid)?;
if offsets.is_empty() { continue; }
match capnp::read_node_at_offset(offsets[0]) {
Ok(n) if !n.deleted => neighbors.push((n.key, strength)),
_ => continue,
}
}
Ok(neighbors)
}
/// Remove a node from the index (used after appending a tombstone).
pub fn remove_from_index(&self, key: &str, uuid: &[u8; 16]) -> Result<()> {
if let Some(db) = self.db.as_ref() {
index::remove_node(db, key, uuid)?;
}
Ok(())
}
/// Rebuild relation index from Vec. Call after mutations that modify relations.
pub fn reindex_relations(&self) -> Result<()> {
if let Some(db) = self.db.as_ref() {
index::clear_relations(db)?;
for rel in &self.relations {
if rel.deleted { continue; }
index::index_relation(db, &rel.source, &rel.target, rel.strength, rel.rel_type as u8)?;
}
}
Ok(())
}
pub fn resolve_key(&self, target: &str) -> Result<String> {
// Strip .md suffix if present — keys no longer use it
let bare = strip_md_suffix(target);
if self.contains_key(&bare)? {
return Ok(bare);
}
let db = self.db.as_ref()
.ok_or_else(|| anyhow::anyhow!("store not loaded"))?;
let all_keys = index::all_keys(db)?;
let matches: Vec<_> = all_keys.iter()
.filter(|k| k.to_lowercase().contains(&target.to_lowercase()))
.cloned().collect();
match matches.len() {
0 => bail!("No entry for '{}'. Run 'init'?", target),
1 => Ok(matches[0].clone()),
n if n <= 10 => {
let list = matches.join("\n ");
bail!("Ambiguous '{}'. Matches:\n {}", target, list)
}
n => bail!("Too many matches for '{}' ({}). Be more specific.", target, n),
}
}
}