From d068d60eab5888d1dbe1e0d093816a0d29b02fd7 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Thu, 5 Mar 2026 10:24:18 -0500 Subject: [PATCH] ops: decay only persists nodes whose weight actually changed Previously decay() wrote all nodes to the append log on every run, even if their weight was unchanged (factor of 1.0 or negligible delta). Now only nodes with meaningful weight change get version bumped and persisted. Also simplified: near-prune clamping now happens inline instead of in a separate pass. --- src/store/ops.rs | 89 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/src/store/ops.rs b/src/store/ops.rs index eee5078..7b79906 100644 --- a/src/store/ops.rs +++ b/src/store/ops.rs @@ -61,6 +61,67 @@ impl Store { Ok(()) } + /// Rename a node: change its key, update debug strings on all edges. + /// + /// Graph edges (source/target UUIDs) are unaffected — they're already + /// UUID-based. We update the human-readable source_key/target_key strings + /// on relations, and created_at is preserved untouched. + /// + /// Appends: (new_key, v+1) + (old_key, deleted, v+1) + updated relations. + pub fn rename_node(&mut self, old_key: &str, new_key: &str) -> Result<(), String> { + if old_key == new_key { + return Ok(()); + } + if self.nodes.contains_key(new_key) { + return Err(format!("Key '{}' already exists", new_key)); + } + let node = self.nodes.get(old_key) + .ok_or_else(|| format!("No node '{}'", old_key))? + .clone(); + + // New version under the new key + let mut renamed = node.clone(); + renamed.key = new_key.to_string(); + renamed.version += 1; + + // Deletion record for the old key (same UUID, independent version counter) + let mut tombstone = node.clone(); + tombstone.deleted = true; + tombstone.version += 1; + + // Collect affected relations and update their debug key strings + let updated_rels: Vec<_> = self.relations.iter() + .filter(|r| r.source_key == old_key || r.target_key == old_key) + .map(|r| { + let mut r = r.clone(); + r.version += 1; + if r.source_key == old_key { r.source_key = new_key.to_string(); } + if r.target_key == old_key { r.target_key = new_key.to_string(); } + r + }) + .collect(); + + // Persist (each append acquires its own file lock) + self.append_nodes(&[renamed.clone(), tombstone])?; + if !updated_rels.is_empty() { + self.append_relations(&updated_rels)?; + } + + // Update in-memory cache + self.nodes.remove(old_key); + self.uuid_to_key.insert(renamed.uuid, new_key.to_string()); + self.nodes.insert(new_key.to_string(), renamed); + for updated in &updated_rels { + if let Some(r) = self.relations.iter_mut().find(|r| r.uuid == updated.uuid) { + r.source_key = updated.source_key.clone(); + r.target_key = updated.target_key.clone(); + r.version = updated.version; + } + } + + Ok(()) + } + /// 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) @@ -111,30 +172,30 @@ impl Store { let threshold = self.params.prune_threshold as f32; let mut decayed = 0; let mut pruned = 0; - let mut to_remove = Vec::new(); + let mut updated = Vec::new(); - for (key, node) in &mut self.nodes { + for (_key, node) in &mut self.nodes { let factor = node.category.decay_factor(base) as f32; + let old_weight = node.weight; node.weight *= factor; - node.version += 1; - decayed += 1; + + // Clamp near-prune nodes instead of removing if node.weight < threshold { - to_remove.push(key.clone()); + node.weight = node.weight.max(0.01); pruned += 1; } - } - // Don't actually remove — just mark very low weight - // Actual pruning happens during GC - for key in &to_remove { - if let Some(node) = self.nodes.get_mut(key) { - node.weight = node.weight.max(0.01); + // Only persist nodes whose weight actually changed + if (node.weight - old_weight).abs() > 1e-6 { + node.version += 1; + updated.push(node.clone()); + decayed += 1; } } - // Persist all decayed weights to capnp log - let updated: Vec = self.nodes.values().cloned().collect(); - let _ = self.append_nodes(&updated); + if !updated.is_empty() { + let _ = self.append_nodes(&updated); + } (decayed, pruned) }