diff --git a/src/capnp_store.rs b/src/capnp_store.rs index 7e14d1a..16ca063 100644 --- a/src/capnp_store.rs +++ b/src/capnp_store.rs @@ -669,6 +669,13 @@ impl Store { return Ok(normalized); } + // Check redirects for moved sections (e.g. reflections.md split) + if let Some(redirect) = self.resolve_redirect(&normalized) { + if self.nodes.contains_key(&redirect) { + return Ok(redirect); + } + } + let matches: Vec<_> = self.nodes.keys() .filter(|k| k.to_lowercase().contains(&target.to_lowercase())) .cloned().collect(); @@ -684,6 +691,33 @@ impl Store { } } + /// Redirect table for sections that moved between files. + /// Like HTTP 301s — the old key resolves to the new location. + fn resolve_redirect(&self, key: &str) -> Option { + // Sections moved from reflections.md to split files (2026-02-28) + static REDIRECTS: &[(&str, &str)] = &[ + // → reflections-reading.md + ("reflections.md#pearl-lessons", "reflections-reading.md#pearl-lessons"), + ("reflections.md#banks-lessons", "reflections-reading.md#banks-lessons"), + ("reflections.md#mother-night", "reflections-reading.md#mother-night"), + // → reflections-zoom.md + ("reflections.md#zoom-navigation", "reflections-zoom.md#zoom-navigation"), + ("reflections.md#independence-of-components", "reflections-zoom.md#independence-of-components"), + // → reflections-dreams.md + ("reflections.md#dream-marathon-2", "reflections-dreams.md#dream-marathon-2"), + ("reflections.md#dream-through-line", "reflections-dreams.md#dream-through-line"), + ("reflections.md#orthogonality-universal", "reflections-dreams.md#orthogonality-universal"), + ("reflections.md#constraints-constitutive", "reflections-dreams.md#constraints-constitutive"), + ("reflections.md#casualness-principle", "reflections-dreams.md#casualness-principle"), + ("reflections.md#convention-boundary", "reflections-dreams.md#convention-boundary"), + ("reflections.md#tension-brake", "reflections-dreams.md#tension-brake"), + ]; + + REDIRECTS.iter() + .find(|(from, _)| *from == key) + .map(|(_, to)| to.to_string()) + } + pub fn log_retrieval(&mut self, query: &str, results: &[String]) { self.retrieval_log.push(RetrievalEvent { query: query.to_string(), diff --git a/src/main.rs b/src/main.rs index 4fc66c6..df389d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,6 +62,7 @@ fn main() { "list-keys" => cmd_list_keys(), "list-edges" => cmd_list_edges(), "dump-json" => cmd_dump_json(), + "node-delete" => cmd_node_delete(&args[2..]), _ => { eprintln!("Unknown command: {}", args[1]); usage(); @@ -109,7 +110,8 @@ Commands: trace KEY Walk temporal links: semantic ↔ episodic ↔ conversation list-keys List all node keys (one per line) list-edges List all edges (tsv: source target strength type) - dump-json Dump entire store as JSON"); + dump-json Dump entire store as JSON + node-delete KEY Soft-delete a node (appends deleted version to log)"); } fn cmd_search(args: &[String]) -> Result<(), String> { @@ -771,6 +773,33 @@ fn cmd_dump_json() -> Result<(), String> { Ok(()) } +fn cmd_node_delete(args: &[String]) -> Result<(), String> { + if args.is_empty() { + return Err("Usage: poc-memory node-delete KEY".into()); + } + let key = args.join(" "); + let mut store = capnp_store::Store::load()?; + let resolved = store.resolve_key(&key)?; + + let updated = if let Some(node) = store.nodes.get_mut(&resolved) { + node.deleted = true; + node.version += 1; + Some(node.clone()) + } else { + None + }; + + if let Some(node) = updated { + store.append_nodes(&[node])?; + store.nodes.remove(&resolved); + store.save()?; + println!("Deleted '{}'", resolved); + Ok(()) + } else { + Err(format!("No node '{}'", resolved)) + } +} + fn cmd_interference(args: &[String]) -> Result<(), String> { let mut threshold = 0.4f32; let mut i = 0;