capnp_store: extract helpers, eliminate duplication

- modify_node(): get_mut→modify→version++→append pattern was duplicated
  across mark_used, mark_wrong, categorize — extract once
- resolve_node_uuid(): resolve-or-redirect pattern was inlined in both
  link and causal edge creation — extract once
- ingest_units() + classify_filename(): shared logic between
  scan_dir_for_init and import_file — import_file shrinks to 6 lines
- Remove dead seen_keys HashSet (built but never read)
- partial_cmp().unwrap() → total_cmp() in cap_degree

-95 lines net.
This commit is contained in:
ProofOfConcept 2026-03-03 12:35:00 -05:00
parent ea0d631051
commit 0bce6aac3c

View file

@ -1071,9 +1071,6 @@ impl Store {
let entries = fs::read_dir(dir)
.map_err(|e| format!("read dir {}: {}", dir.display(), e))?;
// Track which keys we see in markdown so we can detect removed sections
let mut seen_keys = std::collections::HashSet::new();
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
@ -1088,73 +1085,11 @@ impl Store {
.map_err(|e| format!("read {}: {}", path.display(), e))?;
let units = parse_units(&filename, &content);
let mut new_nodes = Vec::new();
let mut updated_nodes = Vec::new();
let (new_count, _) = self.ingest_units(&units, &filename)?;
count += new_count;
// Create relations from links
let mut new_relations = Vec::new();
// Determine node type from filename
let node_type = if filename.starts_with("daily-") {
NodeType::EpisodicDaily
} else if filename.starts_with("weekly-") {
NodeType::EpisodicWeekly
} else if filename == "journal.md" {
NodeType::EpisodicSession
} else {
NodeType::Semantic
};
for (pos, unit) in units.iter().enumerate() {
seen_keys.insert(unit.key.clone());
if let Some(existing) = self.nodes.get(&unit.key) {
// Update if content or position changed
let pos_changed = existing.position != pos as u32;
if existing.content != unit.content || pos_changed {
let mut node = existing.clone();
node.content = unit.content.clone();
node.position = pos as u32;
node.version += 1;
if let Some(ref state) = unit.state {
node.state_tag = state.clone();
}
if let Some(ref src) = unit.source_ref {
node.source_ref = src.clone();
}
updated_nodes.push(node);
}
} else {
let mut node = Store::new_node(&unit.key, &unit.content);
node.node_type = node_type;
node.position = pos as u32;
if let Some(ref state) = unit.state {
node.state_tag = state.clone();
}
if let Some(ref src) = unit.source_ref {
node.source_ref = src.clone();
}
new_nodes.push(node);
}
}
// Batch append new nodes
if !new_nodes.is_empty() {
self.append_nodes(&new_nodes)?;
for node in &new_nodes {
self.uuid_to_key.insert(node.uuid, node.key.clone());
self.nodes.insert(node.key.clone(), node.clone());
}
count += new_nodes.len();
}
// Batch append updated nodes
if !updated_nodes.is_empty() {
self.append_nodes(&updated_nodes)?;
for node in &updated_nodes {
self.nodes.insert(node.key.clone(), node.clone());
}
}
// Create relations from links, using resolve_redirect for targets
for unit in &units {
let source_uuid = match self.nodes.get(&unit.key) {
Some(n) => n.uuid,
@ -1162,54 +1097,28 @@ impl Store {
};
for link in unit.marker_links.iter().chain(unit.md_links.iter()) {
// Try direct lookup, then redirect
let resolved_link = if self.nodes.contains_key(link) {
link.clone()
} else if let Some(redirect) = self.resolve_redirect(link) {
redirect
} else {
continue;
};
let target_uuid = match self.nodes.get(&resolved_link) {
Some(n) => n.uuid,
None => continue,
};
// Check if relation already exists
let Some((key, uuid)) = self.resolve_node_uuid(link) else { continue };
let exists = self.relations.iter().any(|r|
(r.source == source_uuid && r.target == target_uuid) ||
(r.source == target_uuid && r.target == source_uuid));
(r.source == source_uuid && r.target == uuid) ||
(r.source == uuid && r.target == source_uuid));
if !exists {
let rel = Store::new_relation(
source_uuid, target_uuid,
RelationType::Link, 1.0,
&unit.key, &resolved_link,
);
new_relations.push(rel);
new_relations.push(Store::new_relation(
source_uuid, uuid, RelationType::Link, 1.0,
&unit.key, &key,
));
}
}
for cause in &unit.causes {
let resolved_cause = if self.nodes.contains_key(cause) {
cause.clone()
} else if let Some(redirect) = self.resolve_redirect(cause) {
redirect
} else {
continue;
};
let target_uuid = match self.nodes.get(&resolved_cause) {
Some(n) => n.uuid,
None => continue,
};
let Some((key, uuid)) = self.resolve_node_uuid(cause) else { continue };
let exists = self.relations.iter().any(|r|
r.source == target_uuid && r.target == source_uuid
r.source == uuid && r.target == source_uuid
&& r.rel_type == RelationType::Causal);
if !exists {
let rel = Store::new_relation(
target_uuid, source_uuid,
RelationType::Causal, 1.0,
&resolved_cause, &unit.key,
);
new_relations.push(rel);
new_relations.push(Store::new_relation(
uuid, source_uuid, RelationType::Causal, 1.0,
&key, &unit.key,
));
}
}
}
@ -1309,6 +1218,16 @@ impl Store {
.map(|(_, to)| to.to_string())
}
/// Resolve a link target to (key, uuid), trying direct lookup then redirect.
fn resolve_node_uuid(&self, target: &str) -> Option<(String, [u8; 16])> {
if let Some(n) = self.nodes.get(target) {
return Some((target.to_string(), n.uuid));
}
let redirected = self.resolve_redirect(target)?;
let n = self.nodes.get(&redirected)?;
Some((redirected, n.uuid))
}
pub fn log_retrieval(&mut self, query: &str, results: &[String]) {
self.retrieval_log.push(RetrievalEvent {
query: query.to_string(),
@ -1339,41 +1258,36 @@ impl Store {
}
}
/// Modify a node in-place, bump version, and persist to capnp log.
fn modify_node(&mut self, key: &str, f: impl FnOnce(&mut Node)) -> Result<(), String> {
let node = self.nodes.get_mut(key)
.ok_or_else(|| format!("No node '{}'", key))?;
f(node);
node.version += 1;
let node = node.clone();
self.append_nodes(&[node])
}
pub fn mark_used(&mut self, key: &str) {
let updated = if let Some(node) = self.nodes.get_mut(key) {
node.uses += 1;
node.weight = (node.weight + self.params.use_boost as f32).min(1.0);
// Reset spaced repetition — used successfully, move up interval
if node.spaced_repetition_interval < 30 {
node.spaced_repetition_interval = match node.spaced_repetition_interval {
let boost = self.params.use_boost as f32;
let _ = self.modify_node(key, |n| {
n.uses += 1;
n.weight = (n.weight + boost).min(1.0);
if n.spaced_repetition_interval < 30 {
n.spaced_repetition_interval = match n.spaced_repetition_interval {
1 => 3, 3 => 7, 7 => 14, 14 => 30, _ => 30,
};
}
node.last_replayed = now_epoch();
node.version += 1;
Some(node.clone())
} else {
None
};
if let Some(node) = updated {
let _ = self.append_nodes(&[node]);
}
n.last_replayed = now_epoch();
});
}
pub fn mark_wrong(&mut self, key: &str, _ctx: Option<&str>) {
let updated = if let Some(node) = self.nodes.get_mut(key) {
node.wrongs += 1;
node.weight = (node.weight - 0.1).max(0.0);
// Reset spaced repetition interval — needs review
node.spaced_repetition_interval = 1;
node.version += 1;
Some(node.clone())
} else {
None
};
if let Some(node) = updated {
let _ = self.append_nodes(&[node]);
}
let _ = self.modify_node(key, |n| {
n.wrongs += 1;
n.weight = (n.weight - 0.1).max(0.0);
n.spaced_repetition_interval = 1;
});
}
pub fn record_gap(&mut self, desc: &str) {
@ -1386,20 +1300,7 @@ impl Store {
pub fn categorize(&mut self, key: &str, cat_str: &str) -> Result<(), String> {
let cat = Category::from_str(cat_str)
.ok_or_else(|| format!("Unknown category '{}'. Use: core/tech/gen/obs/task", cat_str))?;
let updated = if let Some(node) = self.nodes.get_mut(key) {
node.category = cat;
node.version += 1;
Some(node.clone())
} else {
None
};
if let Some(node) = updated {
// Persist to capnp log so category survives cache rebuilds
self.append_nodes(&[node])?;
Ok(())
} else {
Err(format!("No node '{}'", key))
}
self.modify_node(key, |n| { n.category = cat; })
}
pub fn decay(&mut self) -> (usize, usize) {
@ -1556,7 +1457,7 @@ impl Store {
let excess = active.len() - max_degree;
// Sort Auto by strength ascending
auto_indices.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
auto_indices.sort_by(|a, b| a.1.total_cmp(&b.1));
let auto_prune = excess.min(auto_indices.len());
for &(i, _) in auto_indices.iter().take(auto_prune) {
to_delete.insert(i);
@ -1615,43 +1516,30 @@ impl Store {
}
}
/// Import a markdown file into the store, parsing it into nodes.
/// Process parsed memory units: diff against existing nodes, persist changes.
/// Returns (new_count, updated_count).
pub fn import_file(&mut self, path: &Path) -> Result<(usize, usize), String> {
let filename = path.file_name().unwrap().to_string_lossy().to_string();
let content = fs::read_to_string(path)
.map_err(|e| format!("read {}: {}", path.display(), e))?;
let units = parse_units(&filename, &content);
fn ingest_units(&mut self, units: &[MemoryUnit], filename: &str) -> Result<(usize, usize), String> {
let node_type = classify_filename(filename);
let mut new_nodes = Vec::new();
let mut updated_nodes = Vec::new();
let node_type = if filename.starts_with("daily-") {
NodeType::EpisodicDaily
} else if filename.starts_with("weekly-") {
NodeType::EpisodicWeekly
} else if filename == "journal.md" {
NodeType::EpisodicSession
} else {
NodeType::Semantic
};
for (pos, unit) in units.iter().enumerate() {
if let Some(existing) = self.nodes.get(&unit.key) {
let pos_changed = existing.position != pos as u32;
if existing.content != unit.content || pos_changed {
if existing.content != unit.content || existing.position != pos as u32 {
let mut node = existing.clone();
node.content = unit.content.clone();
node.position = pos as u32;
node.version += 1;
println!(" U {}", unit.key);
if let Some(ref s) = unit.state { node.state_tag = s.clone(); }
if let Some(ref s) = unit.source_ref { node.source_ref = s.clone(); }
updated_nodes.push(node);
}
} else {
let mut node = Store::new_node(&unit.key, &unit.content);
node.node_type = node_type;
node.position = pos as u32;
println!(" + {}", unit.key);
if let Some(ref s) = unit.state { node.state_tag = s.clone(); }
if let Some(ref s) = unit.source_ref { node.source_ref = s.clone(); }
new_nodes.push(node);
}
}
@ -1673,6 +1561,16 @@ impl Store {
Ok((new_nodes.len(), updated_nodes.len()))
}
/// Import a markdown file into the store, parsing it into nodes.
/// Returns (new_count, updated_count).
pub fn import_file(&mut self, path: &Path) -> Result<(usize, usize), String> {
let filename = path.file_name().unwrap().to_string_lossy().to_string();
let content = fs::read_to_string(path)
.map_err(|e| format!("read {}: {}", path.display(), e))?;
let units = parse_units(&filename, &content);
self.ingest_units(&units, &filename)
}
/// Gather all sections for a file key, sorted by position.
/// Returns None if no nodes found.
pub fn file_sections(&self, file_key: &str) -> Option<Vec<&Node>> {
@ -1777,6 +1675,13 @@ impl Store {
// Markdown parsing — same as old system but returns structured units
fn classify_filename(filename: &str) -> NodeType {
if filename.starts_with("daily-") { NodeType::EpisodicDaily }
else if filename.starts_with("weekly-") { NodeType::EpisodicWeekly }
else if filename == "journal.md" { NodeType::EpisodicSession }
else { NodeType::Semantic }
}
pub struct MemoryUnit {
pub key: String,
pub content: String,