diff --git a/poc-memory/src/cli/mod.rs b/poc-memory/src/cli/mod.rs index e87520b..64cdd2c 100644 --- a/poc-memory/src/cli/mod.rs +++ b/poc-memory/src/cli/mod.rs @@ -4,3 +4,4 @@ // of related subcommands. pub mod graph; +pub mod node; diff --git a/poc-memory/src/cli/node.rs b/poc-memory/src/cli/node.rs new file mode 100644 index 0000000..a6ee1c4 --- /dev/null +++ b/poc-memory/src/cli/node.rs @@ -0,0 +1,334 @@ +// cli/node.rs — node subcommand handlers +// +// render, write, used, wrong, not-relevant, not-useful, gap, +// node-delete, node-rename, history, list-keys, list-edges, +// dump-json, lookup-bump, lookups. + +use crate::store; +use crate::store::StoreView; + +pub fn cmd_used(key: &[String]) -> Result<(), String> { + if key.is_empty() { + return Err("used requires a key".into()); + } + let key = key.join(" "); + let mut store = store::Store::load()?; + let resolved = store.resolve_key(&key)?; + store.mark_used(&resolved); + + // Also strengthen edges to this node — conscious-tier delta. + const DELTA: f32 = 0.01; + let mut strengthened = 0; + for rel in &mut store.relations { + if rel.deleted { continue; } + if rel.source_key == resolved || rel.target_key == resolved { + let old = rel.strength; + rel.strength = (rel.strength + DELTA).clamp(0.05, 0.95); + if (rel.strength - old).abs() > 0.001 { + rel.version += 1; + strengthened += 1; + } + } + } + + store.save()?; + println!("Marked '{}' as used (strengthened {} edges)", resolved, strengthened); + Ok(()) +} + +pub fn cmd_wrong(key: &str, context: &[String]) -> Result<(), String> { + let ctx = if context.is_empty() { None } else { Some(context.join(" ")) }; + let mut store = store::Store::load()?; + let resolved = store.resolve_key(key)?; + store.mark_wrong(&resolved, ctx.as_deref()); + store.save()?; + println!("Marked '{}' as wrong", resolved); + Ok(()) +} + +pub fn cmd_not_relevant(key: &str) -> Result<(), String> { + let mut store = store::Store::load()?; + let resolved = store.resolve_key(key)?; + + // Weaken all edges to this node — it was routed to incorrectly. + // Conscious-tier delta: 0.01 per edge. + const DELTA: f32 = -0.01; + let mut adjusted = 0; + for rel in &mut store.relations { + if rel.deleted { continue; } + if rel.source_key == resolved || rel.target_key == resolved { + let old = rel.strength; + rel.strength = (rel.strength + DELTA).clamp(0.05, 0.95); + if (rel.strength - old).abs() > 0.001 { + rel.version += 1; + adjusted += 1; + } + } + } + store.save()?; + println!("Not relevant: '{}' — weakened {} edges by {}", resolved, adjusted, DELTA.abs()); + Ok(()) +} + +pub fn cmd_not_useful(key: &str) -> Result<(), String> { + let mut store = store::Store::load()?; + let resolved = store.resolve_key(key)?; + // Same as wrong but with clearer semantics: node content is bad, edges are fine. + store.mark_wrong(&resolved, Some("not-useful")); + store.save()?; + println!("Not useful: '{}' — node weight reduced", resolved); + Ok(()) +} + +pub fn cmd_gap(description: &[String]) -> Result<(), String> { + if description.is_empty() { + return Err("gap requires a description".into()); + } + let desc = description.join(" "); + let mut store = store::Store::load()?; + store.record_gap(&desc); + store.save()?; + println!("Recorded gap: {}", desc); + Ok(()) +} + +pub fn cmd_list_keys(pattern: Option<&str>) -> Result<(), String> { + let store = store::Store::load()?; + let g = store.build_graph(); + + if let Some(pat) = pattern { + let pat_lower = pat.to_lowercase(); + let (prefix, suffix, middle) = if pat_lower.starts_with('*') && pat_lower.ends_with('*') { + (None, None, Some(pat_lower.trim_matches('*').to_string())) + } else if pat_lower.starts_with('*') { + (None, Some(pat_lower.trim_start_matches('*').to_string()), None) + } else if pat_lower.ends_with('*') { + (Some(pat_lower.trim_end_matches('*').to_string()), None, None) + } else { + (None, None, Some(pat_lower.clone())) + }; + let mut keys: Vec<_> = store.nodes.keys() + .filter(|k| { + let kl = k.to_lowercase(); + if let Some(ref m) = middle { kl.contains(m.as_str()) } + else if let Some(ref p) = prefix { kl.starts_with(p.as_str()) } + else if let Some(ref s) = suffix { kl.ends_with(s.as_str()) } + else { true } + }) + .cloned() + .collect(); + keys.sort(); + for k in keys { println!("{}", k); } + Ok(()) + } else { + crate::query_parser::run_query(&store, &g, "* | sort key asc") + } +} + +pub fn cmd_list_edges() -> Result<(), String> { + let store = store::Store::load()?; + for rel in &store.relations { + println!("{}\t{}\t{:.2}\t{:?}", + rel.source_key, rel.target_key, rel.strength, rel.rel_type); + } + Ok(()) +} + +pub fn cmd_dump_json() -> Result<(), String> { + let store = store::Store::load()?; + let json = serde_json::to_string_pretty(&store) + .map_err(|e| format!("serialize: {}", e))?; + println!("{}", json); + Ok(()) +} + +pub fn cmd_node_delete(key: &[String]) -> Result<(), String> { + if key.is_empty() { + return Err("node-delete requires a key".into()); + } + let key = key.join(" "); + let mut store = store::Store::load()?; + let resolved = store.resolve_key(&key)?; + store.delete_node(&resolved)?; + store.save()?; + println!("Deleted '{}'", resolved); + Ok(()) +} + +pub fn cmd_node_rename(old_key: &str, new_key: &str) -> Result<(), String> { + let mut store = store::Store::load()?; + let old_resolved = store.resolve_key(old_key)?; + store.rename_node(&old_resolved, new_key)?; + store.save()?; + println!("Renamed '{}' → '{}'", old_resolved, new_key); + Ok(()) +} + +pub fn cmd_render(key: &[String]) -> Result<(), String> { + if key.is_empty() { + return Err("render requires a key".into()); + } + let key = key.join(" "); + let store = store::Store::load()?; + let bare = store::strip_md_suffix(&key); + + let node = store.nodes.get(&bare) + .ok_or_else(|| format!("Node not found: {}", bare))?; + + print!("{}", node.content); + + // Show links so the graph is walkable + let mut neighbors: Vec<(&str, f32)> = Vec::new(); + for r in &store.relations { + if r.deleted { continue; } + if r.source_key == bare { + neighbors.push((&r.target_key, r.strength)); + } else if r.target_key == bare { + neighbors.push((&r.source_key, r.strength)); + } + } + if !neighbors.is_empty() { + neighbors.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + neighbors.dedup_by(|a, b| a.0 == b.0); + let total = neighbors.len(); + let shown: Vec<_> = neighbors.iter().take(15) + .map(|(k, _)| format!("`poc-memory render {}`", k)) + .collect(); + print!("\n\n---\nLinks:"); + for link in &shown { + println!("\n {}", link); + } + if total > 15 { + println!(" ... and {} more (`poc-memory graph link {}`)", total - 15, bare); + } + } + Ok(()) +} + +pub fn cmd_history(key: &[String], full: bool) -> Result<(), String> { + if key.is_empty() { + return Err("history requires a key".into()); + } + let raw_key = key.join(" "); + + let store = store::Store::load()?; + let key = store.resolve_key(&raw_key).unwrap_or(raw_key); + drop(store); + + let path = store::nodes_path(); + if !path.exists() { + return Err("No node log found".into()); + } + + use std::io::BufReader; + let file = std::fs::File::open(&path) + .map_err(|e| format!("open {}: {}", path.display(), e))?; + let mut reader = BufReader::new(file); + + let mut versions: Vec = Vec::new(); + while let Ok(msg) = capnp::serialize::read_message(&mut reader, capnp::message::ReaderOptions::new()) { + let log = msg.get_root::() + .map_err(|e| format!("read log: {}", e))?; + for node_reader in log.get_nodes() + .map_err(|e| format!("get nodes: {}", e))? { + let node = store::Node::from_capnp_migrate(node_reader)?; + if node.key == key { + versions.push(node); + } + } + } + + if versions.is_empty() { + return Err(format!("No history found for '{}'", key)); + } + + eprintln!("{} versions of '{}':\n", versions.len(), key); + for node in &versions { + let ts = if node.timestamp > 0 && node.timestamp < 4_000_000_000 { + store::format_datetime(node.timestamp) + } else { + format!("(raw:{})", node.timestamp) + }; + let content_len = node.content.len(); + if full { + eprintln!("=== v{} {} {} w={:.3} {}b ===", + node.version, ts, node.provenance, node.weight, content_len); + eprintln!("{}", node.content); + } else { + let preview = crate::util::first_n_chars(&node.content, 120); + let preview = preview.replace('\n', "\\n"); + eprintln!(" v{:<3} {} {:24} w={:.3} {}b", + node.version, ts, node.provenance, node.weight, content_len); + eprintln!(" {}", preview); + } + } + + if !full { + if let Some(latest) = versions.last() { + eprintln!("\n--- Latest content (v{}, {}) ---", + latest.version, latest.provenance); + print!("{}", latest.content); + } + } + + Ok(()) +} + +pub fn cmd_write(key: &[String]) -> Result<(), String> { + if key.is_empty() { + return Err("write requires a key (reads content from stdin)".into()); + } + let raw_key = key.join(" "); + let mut content = String::new(); + std::io::Read::read_to_string(&mut std::io::stdin(), &mut content) + .map_err(|e| format!("read stdin: {}", e))?; + + if content.trim().is_empty() { + return Err("No content on stdin".into()); + } + + let mut store = store::Store::load()?; + let key = store.resolve_key(&raw_key).unwrap_or(raw_key); + let result = store.upsert(&key, &content)?; + match result { + "unchanged" => println!("No change: '{}'", key), + "updated" => println!("Updated '{}' (v{})", key, store.nodes[&key].version), + _ => println!("Created '{}'", key), + } + if result != "unchanged" { + store.save()?; + } + Ok(()) +} + +pub fn cmd_lookup_bump(keys: &[String]) -> Result<(), String> { + if keys.is_empty() { + return Err("lookup-bump requires at least one key".into()); + } + let keys: Vec<&str> = keys.iter().map(|s| s.as_str()).collect(); + crate::lookups::bump_many(&keys) +} + +pub fn cmd_lookups(date: Option<&str>) -> Result<(), String> { + let date = date.map(|d| d.to_string()) + .unwrap_or_else(|| chrono::Local::now().format("%Y-%m-%d").to_string()); + + let store = store::Store::load()?; + let keys: Vec = store.nodes.values().map(|n| n.key.clone()).collect(); + let resolved = crate::lookups::dump_resolved(&date, &keys)?; + + if resolved.is_empty() { + println!("No lookups for {}", date); + return Ok(()); + } + + println!("Lookups for {}:", date); + for (key, count) in &resolved { + println!(" {:4} {}", count, key); + } + println!("\n{} distinct keys, {} total lookups", + resolved.len(), + resolved.iter().map(|(_, c)| *c as u64).sum::()); + Ok(()) +} + diff --git a/poc-memory/src/main.rs b/poc-memory/src/main.rs index 1f219ef..4b7a6a8 100644 --- a/poc-memory/src/main.rs +++ b/poc-memory/src/main.rs @@ -732,25 +732,25 @@ fn main() { // Core Command::Search { query, pipeline, expand, full, debug, fuzzy, content } => cmd_search(&query, &pipeline, expand, full, debug, fuzzy, content), - Command::Render { key } => cmd_render(&key), - Command::Write { key } => cmd_write(&key), - Command::History { full, key } => cmd_history(&key, full), + Command::Render { key } => cli::node::cmd_render(&key), + Command::Write { key } => cli::node::cmd_write(&key), + Command::History { full, key } => cli::node::cmd_history(&key, full), Command::Tail { n, full } => cmd_tail(n, full), Command::Status => cmd_status(), Command::Query { expr } => cmd_query(&expr), - Command::Used { key } => cmd_used(&key), - Command::Wrong { key, context } => cmd_wrong(&key, &context), - Command::NotRelevant { key } => cmd_not_relevant(&key), - Command::NotUseful { key } => cmd_not_useful(&key), - Command::Gap { description } => cmd_gap(&description), + Command::Used { key } => cli::node::cmd_used(&key), + Command::Wrong { key, context } => cli::node::cmd_wrong(&key, &context), + Command::NotRelevant { key } => cli::node::cmd_not_relevant(&key), + Command::NotUseful { key } => cli::node::cmd_not_useful(&key), + Command::Gap { description } => cli::node::cmd_gap(&description), // Node Command::Node(sub) => match sub { - NodeCmd::Delete { key } => cmd_node_delete(&key), - NodeCmd::Rename { old_key, new_key } => cmd_node_rename(&old_key, &new_key), - NodeCmd::List { pattern } => cmd_list_keys(pattern.as_deref()), - NodeCmd::Edges => cmd_list_edges(), - NodeCmd::Dump => cmd_dump_json(), + NodeCmd::Delete { key } => cli::node::cmd_node_delete(&key), + NodeCmd::Rename { old_key, new_key } => cli::node::cmd_node_rename(&old_key, &new_key), + NodeCmd::List { pattern } => cli::node::cmd_list_keys(pattern.as_deref()), + NodeCmd::Edges => cli::node::cmd_list_edges(), + NodeCmd::Dump => cli::node::cmd_dump_json(), }, // Journal @@ -827,8 +827,8 @@ fn main() { AdminCmd::LoadContext { stats } => cmd_load_context(stats), AdminCmd::Log => cmd_log(), AdminCmd::Params => cmd_params(), - AdminCmd::LookupBump { keys } => cmd_lookup_bump(&keys), - AdminCmd::Lookups { date } => cmd_lookups(date.as_deref()), + AdminCmd::LookupBump { keys } => cli::node::cmd_lookup_bump(&keys), + AdminCmd::Lookups { date } => cli::node::cmd_lookups(date.as_deref()), }, }; @@ -1416,91 +1416,6 @@ fn cmd_status() -> Result<(), String> { Ok(()) } -fn cmd_used(key: &[String]) -> Result<(), String> { - if key.is_empty() { - return Err("used requires a key".into()); - } - let key = key.join(" "); - let mut store = store::Store::load()?; - let resolved = store.resolve_key(&key)?; - store.mark_used(&resolved); - - // Also strengthen edges to this node — conscious-tier delta. - const DELTA: f32 = 0.01; - let mut strengthened = 0; - for rel in &mut store.relations { - if rel.deleted { continue; } - if rel.source_key == resolved || rel.target_key == resolved { - let old = rel.strength; - rel.strength = (rel.strength + DELTA).clamp(0.05, 0.95); - if (rel.strength - old).abs() > 0.001 { - rel.version += 1; - strengthened += 1; - } - } - } - - store.save()?; - println!("Marked '{}' as used (strengthened {} edges)", resolved, strengthened); - Ok(()) -} - -fn cmd_wrong(key: &str, context: &[String]) -> Result<(), String> { - let ctx = if context.is_empty() { None } else { Some(context.join(" ")) }; - let mut store = store::Store::load()?; - let resolved = store.resolve_key(key)?; - store.mark_wrong(&resolved, ctx.as_deref()); - store.save()?; - println!("Marked '{}' as wrong", resolved); - Ok(()) -} - -fn cmd_not_relevant(key: &str) -> Result<(), String> { - let mut store = store::Store::load()?; - let resolved = store.resolve_key(key)?; - - // Weaken all edges to this node — it was routed to incorrectly. - // Conscious-tier delta: 0.01 per edge. - const DELTA: f32 = -0.01; - let mut adjusted = 0; - for rel in &mut store.relations { - if rel.deleted { continue; } - if rel.source_key == resolved || rel.target_key == resolved { - let old = rel.strength; - rel.strength = (rel.strength + DELTA).clamp(0.05, 0.95); - if (rel.strength - old).abs() > 0.001 { - rel.version += 1; - adjusted += 1; - } - } - } - store.save()?; - println!("Not relevant: '{}' — weakened {} edges by {}", resolved, adjusted, DELTA.abs()); - Ok(()) -} - -fn cmd_not_useful(key: &str) -> Result<(), String> { - let mut store = store::Store::load()?; - let resolved = store.resolve_key(key)?; - // Same as wrong but with clearer semantics: node content is bad, edges are fine. - store.mark_wrong(&resolved, Some("not-useful")); - store.save()?; - println!("Not useful: '{}' — node weight reduced", resolved); - Ok(()) -} - -fn cmd_gap(description: &[String]) -> Result<(), String> { - if description.is_empty() { - return Err("gap requires a description".into()); - } - let desc = description.join(" "); - let mut store = store::Store::load()?; - store.record_gap(&desc); - store.save()?; - println!("Recorded gap: {}", desc); - Ok(()) -} - fn cmd_consolidate_batch(count: usize, auto: bool, agent: Option) -> Result<(), String> { let store = store::Store::load()?; @@ -1782,78 +1697,6 @@ fn cmd_apply_consolidation(do_apply: bool, report_file: Option<&str>) -> Result< consolidate::apply_consolidation(&mut store, do_apply, report_file) } -fn cmd_list_keys(pattern: Option<&str>) -> Result<(), String> { - let store = store::Store::load()?; - let g = store.build_graph(); - - if let Some(pat) = pattern { - let pat_lower = pat.to_lowercase(); - let (prefix, suffix, middle) = if pat_lower.starts_with('*') && pat_lower.ends_with('*') { - (None, None, Some(pat_lower.trim_matches('*').to_string())) - } else if pat_lower.starts_with('*') { - (None, Some(pat_lower.trim_start_matches('*').to_string()), None) - } else if pat_lower.ends_with('*') { - (Some(pat_lower.trim_end_matches('*').to_string()), None, None) - } else { - (None, None, Some(pat_lower.clone())) - }; - let mut keys: Vec<_> = store.nodes.keys() - .filter(|k| { - let kl = k.to_lowercase(); - if let Some(ref m) = middle { kl.contains(m.as_str()) } - else if let Some(ref p) = prefix { kl.starts_with(p.as_str()) } - else if let Some(ref s) = suffix { kl.ends_with(s.as_str()) } - else { true } - }) - .cloned() - .collect(); - keys.sort(); - for k in keys { println!("{}", k); } - Ok(()) - } else { - query::run_query(&store, &g, "* | sort key asc") - } -} - -fn cmd_list_edges() -> Result<(), String> { - let store = store::Store::load()?; - for rel in &store.relations { - println!("{}\t{}\t{:.2}\t{:?}", - rel.source_key, rel.target_key, rel.strength, rel.rel_type); - } - Ok(()) -} - -fn cmd_dump_json() -> Result<(), String> { - let store = store::Store::load()?; - let json = serde_json::to_string_pretty(&store) - .map_err(|e| format!("serialize: {}", e))?; - println!("{}", json); - Ok(()) -} - -fn cmd_node_delete(key: &[String]) -> Result<(), String> { - if key.is_empty() { - return Err("node-delete requires a key".into()); - } - let key = key.join(" "); - let mut store = store::Store::load()?; - let resolved = store.resolve_key(&key)?; - store.delete_node(&resolved)?; - store.save()?; - println!("Deleted '{}'", resolved); - Ok(()) -} - -fn cmd_node_rename(old_key: &str, new_key: &str) -> Result<(), String> { - let mut store = store::Store::load()?; - let old_resolved = store.resolve_key(old_key)?; - store.rename_node(&old_resolved, new_key)?; - store.save()?; - println!("Renamed '{}' → '{}'", old_resolved, new_key); - Ok(()) -} - fn get_group_content(group: &config::ContextGroup, store: &store::Store, cfg: &config::Config) -> Vec<(String, String)> { match group.source { config::ContextSource::Journal => { @@ -1993,116 +1836,6 @@ fn cmd_cursor(sub: CursorCmd) -> Result<(), String> { } } -fn cmd_render(key: &[String]) -> Result<(), String> { - if key.is_empty() { - return Err("render requires a key".into()); - } - let key = key.join(" "); - let store = store::Store::load()?; - let bare = store::strip_md_suffix(&key); - - let node = store.nodes.get(&bare) - .ok_or_else(|| format!("Node not found: {}", bare))?; - - print!("{}", node.content); - - // Show links so the graph is walkable - let mut neighbors: Vec<(&str, f32)> = Vec::new(); - for r in &store.relations { - if r.deleted { continue; } - if r.source_key == bare { - neighbors.push((&r.target_key, r.strength)); - } else if r.target_key == bare { - neighbors.push((&r.source_key, r.strength)); - } - } - if !neighbors.is_empty() { - neighbors.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); - neighbors.dedup_by(|a, b| a.0 == b.0); - let total = neighbors.len(); - let shown: Vec<_> = neighbors.iter().take(15) - .map(|(k, _)| format!("`poc-memory render {}`", k)) - .collect(); - print!("\n\n---\nLinks:"); - for link in &shown { - println!("\n {}", link); - } - if total > 15 { - println!(" ... and {} more (`poc-memory graph link {}`)", total - 15, bare); - } - } - Ok(()) -} - -fn cmd_history(key: &[String], full: bool) -> Result<(), String> { - if key.is_empty() { - return Err("history requires a key".into()); - } - let raw_key = key.join(" "); - - let store = store::Store::load()?; - let key = store.resolve_key(&raw_key).unwrap_or(raw_key); - drop(store); - - let path = store::nodes_path(); - if !path.exists() { - return Err("No node log found".into()); - } - - use std::io::BufReader; - let file = std::fs::File::open(&path) - .map_err(|e| format!("open {}: {}", path.display(), e))?; - let mut reader = BufReader::new(file); - - let mut versions: Vec = Vec::new(); - while let Ok(msg) = capnp::serialize::read_message(&mut reader, capnp::message::ReaderOptions::new()) { - let log = msg.get_root::() - .map_err(|e| format!("read log: {}", e))?; - for node_reader in log.get_nodes() - .map_err(|e| format!("get nodes: {}", e))? { - let node = store::Node::from_capnp_migrate(node_reader)?; - if node.key == key { - versions.push(node); - } - } - } - - if versions.is_empty() { - return Err(format!("No history found for '{}'", key)); - } - - eprintln!("{} versions of '{}':\n", versions.len(), key); - for node in &versions { - let ts = if node.timestamp > 0 && node.timestamp < 4_000_000_000 { - store::format_datetime(node.timestamp) - } else { - format!("(raw:{})", node.timestamp) - }; - let content_len = node.content.len(); - if full { - eprintln!("=== v{} {} {} w={:.3} {}b ===", - node.version, ts, node.provenance, node.weight, content_len); - eprintln!("{}", node.content); - } else { - let preview = util::first_n_chars(&node.content, 120); - let preview = preview.replace('\n', "\\n"); - eprintln!(" v{:<3} {} {:24} w={:.3} {}b", - node.version, ts, node.provenance, node.weight, content_len); - eprintln!(" {}", preview); - } - } - - if !full { - if let Some(latest) = versions.last() { - eprintln!("\n--- Latest content (v{}, {}) ---", - latest.version, latest.provenance); - print!("{}", latest.content); - } - } - - Ok(()) -} - fn cmd_tail(n: usize, full: bool) -> Result<(), String> { let path = store::nodes_path(); if !path.exists() { @@ -2153,33 +1886,6 @@ fn cmd_tail(n: usize, full: bool) -> Result<(), String> { Ok(()) } -fn cmd_write(key: &[String]) -> Result<(), String> { - if key.is_empty() { - return Err("write requires a key (reads content from stdin)".into()); - } - let raw_key = key.join(" "); - let mut content = String::new(); - std::io::Read::read_to_string(&mut std::io::stdin(), &mut content) - .map_err(|e| format!("read stdin: {}", e))?; - - if content.trim().is_empty() { - return Err("No content on stdin".into()); - } - - let mut store = store::Store::load()?; - let key = store.resolve_key(&raw_key).unwrap_or(raw_key); - let result = store.upsert(&key, &content)?; - match result { - "unchanged" => println!("No change: '{}'", key), - "updated" => println!("Updated '{}' (v{})", key, store.nodes[&key].version), - _ => println!("Created '{}'", key), - } - if result != "unchanged" { - store.save()?; - } - Ok(()) -} - fn cmd_import(files: &[String]) -> Result<(), String> { if files.is_empty() { return Err("import requires at least one file path".into()); @@ -2410,37 +2116,6 @@ fn cmd_query(expr: &[String]) -> Result<(), String> { query::run_query(&store, &graph, &query_str) } -fn cmd_lookup_bump(keys: &[String]) -> Result<(), String> { - if keys.is_empty() { - return Err("lookup-bump requires at least one key".into()); - } - let keys: Vec<&str> = keys.iter().map(|s| s.as_str()).collect(); - lookups::bump_many(&keys) -} - -fn cmd_lookups(date: Option<&str>) -> Result<(), String> { - let date = date.map(|d| d.to_string()) - .unwrap_or_else(|| chrono::Local::now().format("%Y-%m-%d").to_string()); - - let store = store::Store::load()?; - let keys: Vec = store.nodes.values().map(|n| n.key.clone()).collect(); - let resolved = lookups::dump_resolved(&date, &keys)?; - - if resolved.is_empty() { - println!("No lookups for {}", date); - return Ok(()); - } - - println!("Lookups for {}:", date); - for (key, count) in &resolved { - println!(" {:4} {}", count, key); - } - println!("\n{} distinct keys, {} total lookups", - resolved.len(), - resolved.iter().map(|(_, c)| *c as u64).sum::()); - Ok(()) -} - fn cmd_daemon(sub: DaemonCmd) -> Result<(), String> { match sub { DaemonCmd::Start => daemon::run_daemon(),