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) let entries = fs::read_dir(dir)
.map_err(|e| format!("read dir {}: {}", dir.display(), e))?; .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() { for entry in entries.flatten() {
let path = entry.path(); let path = entry.path();
if path.is_dir() { if path.is_dir() {
@ -1088,73 +1085,11 @@ impl Store {
.map_err(|e| format!("read {}: {}", path.display(), e))?; .map_err(|e| format!("read {}: {}", path.display(), e))?;
let units = parse_units(&filename, &content); let units = parse_units(&filename, &content);
let mut new_nodes = Vec::new(); let (new_count, _) = self.ingest_units(&units, &filename)?;
let mut updated_nodes = Vec::new(); count += new_count;
// Create relations from links
let mut new_relations = Vec::new(); 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 { for unit in &units {
let source_uuid = match self.nodes.get(&unit.key) { let source_uuid = match self.nodes.get(&unit.key) {
Some(n) => n.uuid, Some(n) => n.uuid,
@ -1162,54 +1097,28 @@ impl Store {
}; };
for link in unit.marker_links.iter().chain(unit.md_links.iter()) { for link in unit.marker_links.iter().chain(unit.md_links.iter()) {
// Try direct lookup, then redirect let Some((key, uuid)) = self.resolve_node_uuid(link) else { continue };
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 exists = self.relations.iter().any(|r| let exists = self.relations.iter().any(|r|
(r.source == source_uuid && r.target == target_uuid) || (r.source == source_uuid && r.target == uuid) ||
(r.source == target_uuid && r.target == source_uuid)); (r.source == uuid && r.target == source_uuid));
if !exists { if !exists {
let rel = Store::new_relation( new_relations.push(Store::new_relation(
source_uuid, target_uuid, source_uuid, uuid, RelationType::Link, 1.0,
RelationType::Link, 1.0, &unit.key, &key,
&unit.key, &resolved_link, ));
);
new_relations.push(rel);
} }
} }
for cause in &unit.causes { for cause in &unit.causes {
let resolved_cause = if self.nodes.contains_key(cause) { let Some((key, uuid)) = self.resolve_node_uuid(cause) else { continue };
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 exists = self.relations.iter().any(|r| 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); && r.rel_type == RelationType::Causal);
if !exists { if !exists {
let rel = Store::new_relation( new_relations.push(Store::new_relation(
target_uuid, source_uuid, uuid, source_uuid, RelationType::Causal, 1.0,
RelationType::Causal, 1.0, &key, &unit.key,
&resolved_cause, &unit.key, ));
);
new_relations.push(rel);
} }
} }
} }
@ -1309,6 +1218,16 @@ impl Store {
.map(|(_, to)| to.to_string()) .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]) { pub fn log_retrieval(&mut self, query: &str, results: &[String]) {
self.retrieval_log.push(RetrievalEvent { self.retrieval_log.push(RetrievalEvent {
query: query.to_string(), 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) { pub fn mark_used(&mut self, key: &str) {
let updated = if let Some(node) = self.nodes.get_mut(key) { let boost = self.params.use_boost as f32;
node.uses += 1; let _ = self.modify_node(key, |n| {
node.weight = (node.weight + self.params.use_boost as f32).min(1.0); n.uses += 1;
// Reset spaced repetition — used successfully, move up interval n.weight = (n.weight + boost).min(1.0);
if node.spaced_repetition_interval < 30 { if n.spaced_repetition_interval < 30 {
node.spaced_repetition_interval = match node.spaced_repetition_interval { n.spaced_repetition_interval = match n.spaced_repetition_interval {
1 => 3, 3 => 7, 7 => 14, 14 => 30, _ => 30, 1 => 3, 3 => 7, 7 => 14, 14 => 30, _ => 30,
}; };
} }
node.last_replayed = now_epoch(); n.last_replayed = now_epoch();
node.version += 1; });
Some(node.clone())
} else {
None
};
if let Some(node) = updated {
let _ = self.append_nodes(&[node]);
}
} }
pub fn mark_wrong(&mut self, key: &str, _ctx: Option<&str>) { pub fn mark_wrong(&mut self, key: &str, _ctx: Option<&str>) {
let updated = if let Some(node) = self.nodes.get_mut(key) { let _ = self.modify_node(key, |n| {
node.wrongs += 1; n.wrongs += 1;
node.weight = (node.weight - 0.1).max(0.0); n.weight = (n.weight - 0.1).max(0.0);
// Reset spaced repetition interval — needs review n.spaced_repetition_interval = 1;
node.spaced_repetition_interval = 1; });
node.version += 1;
Some(node.clone())
} else {
None
};
if let Some(node) = updated {
let _ = self.append_nodes(&[node]);
}
} }
pub fn record_gap(&mut self, desc: &str) { 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> { pub fn categorize(&mut self, key: &str, cat_str: &str) -> Result<(), String> {
let cat = Category::from_str(cat_str) let cat = Category::from_str(cat_str)
.ok_or_else(|| format!("Unknown category '{}'. Use: core/tech/gen/obs/task", 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) { self.modify_node(key, |n| { n.category = cat; })
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))
}
} }
pub fn decay(&mut self) -> (usize, usize) { pub fn decay(&mut self) -> (usize, usize) {
@ -1556,7 +1457,7 @@ impl Store {
let excess = active.len() - max_degree; let excess = active.len() - max_degree;
// Sort Auto by strength ascending // 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()); let auto_prune = excess.min(auto_indices.len());
for &(i, _) in auto_indices.iter().take(auto_prune) { for &(i, _) in auto_indices.iter().take(auto_prune) {
to_delete.insert(i); 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). /// Returns (new_count, updated_count).
pub fn import_file(&mut self, path: &Path) -> Result<(usize, usize), String> { fn ingest_units(&mut self, units: &[MemoryUnit], filename: &str) -> Result<(usize, usize), String> {
let filename = path.file_name().unwrap().to_string_lossy().to_string(); let node_type = classify_filename(filename);
let content = fs::read_to_string(path)
.map_err(|e| format!("read {}: {}", path.display(), e))?;
let units = parse_units(&filename, &content);
let mut new_nodes = Vec::new(); let mut new_nodes = Vec::new();
let mut updated_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() { for (pos, unit) in units.iter().enumerate() {
if let Some(existing) = self.nodes.get(&unit.key) { if let Some(existing) = self.nodes.get(&unit.key) {
let pos_changed = existing.position != pos as u32; if existing.content != unit.content || existing.position != pos as u32 {
if existing.content != unit.content || pos_changed {
let mut node = existing.clone(); let mut node = existing.clone();
node.content = unit.content.clone(); node.content = unit.content.clone();
node.position = pos as u32; node.position = pos as u32;
node.version += 1; 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); updated_nodes.push(node);
} }
} else { } else {
let mut node = Store::new_node(&unit.key, &unit.content); let mut node = Store::new_node(&unit.key, &unit.content);
node.node_type = node_type; node.node_type = node_type;
node.position = pos as u32; 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); new_nodes.push(node);
} }
} }
@ -1673,6 +1561,16 @@ impl Store {
Ok((new_nodes.len(), updated_nodes.len())) 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. /// Gather all sections for a file key, sorted by position.
/// Returns None if no nodes found. /// Returns None if no nodes found.
pub fn file_sections(&self, file_key: &str) -> Option<Vec<&Node>> { 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 // 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 struct MemoryUnit {
pub key: String, pub key: String,
pub content: String, pub content: String,