cli: extract node commands from main.rs into cli/node.rs
Move 15 node subcommand handlers (310 lines) out of main.rs: render, write, used, wrong, not-relevant, not-useful, gap, node-delete, node-rename, history, list-keys, list-edges, dump-json, lookup-bump, lookups. main.rs: 2518 → 2193 lines. Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
c8d86e94c1
commit
aa2fddf137
3 changed files with 350 additions and 340 deletions
|
|
@ -4,3 +4,4 @@
|
||||||
// of related subcommands.
|
// of related subcommands.
|
||||||
|
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
pub mod node;
|
||||||
|
|
|
||||||
334
poc-memory/src/cli/node.rs
Normal file
334
poc-memory/src/cli/node.rs
Normal file
|
|
@ -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<store::Node> = Vec::new();
|
||||||
|
while let Ok(msg) = capnp::serialize::read_message(&mut reader, capnp::message::ReaderOptions::new()) {
|
||||||
|
let log = msg.get_root::<crate::memory_capnp::node_log::Reader>()
|
||||||
|
.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<String> = 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::<u64>());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -732,25 +732,25 @@ fn main() {
|
||||||
// Core
|
// Core
|
||||||
Command::Search { query, pipeline, expand, full, debug, fuzzy, content }
|
Command::Search { query, pipeline, expand, full, debug, fuzzy, content }
|
||||||
=> cmd_search(&query, &pipeline, expand, full, debug, fuzzy, content),
|
=> cmd_search(&query, &pipeline, expand, full, debug, fuzzy, content),
|
||||||
Command::Render { key } => cmd_render(&key),
|
Command::Render { key } => cli::node::cmd_render(&key),
|
||||||
Command::Write { key } => cmd_write(&key),
|
Command::Write { key } => cli::node::cmd_write(&key),
|
||||||
Command::History { full, key } => cmd_history(&key, full),
|
Command::History { full, key } => cli::node::cmd_history(&key, full),
|
||||||
Command::Tail { n, full } => cmd_tail(n, full),
|
Command::Tail { n, full } => cmd_tail(n, full),
|
||||||
Command::Status => cmd_status(),
|
Command::Status => cmd_status(),
|
||||||
Command::Query { expr } => cmd_query(&expr),
|
Command::Query { expr } => cmd_query(&expr),
|
||||||
Command::Used { key } => cmd_used(&key),
|
Command::Used { key } => cli::node::cmd_used(&key),
|
||||||
Command::Wrong { key, context } => cmd_wrong(&key, &context),
|
Command::Wrong { key, context } => cli::node::cmd_wrong(&key, &context),
|
||||||
Command::NotRelevant { key } => cmd_not_relevant(&key),
|
Command::NotRelevant { key } => cli::node::cmd_not_relevant(&key),
|
||||||
Command::NotUseful { key } => cmd_not_useful(&key),
|
Command::NotUseful { key } => cli::node::cmd_not_useful(&key),
|
||||||
Command::Gap { description } => cmd_gap(&description),
|
Command::Gap { description } => cli::node::cmd_gap(&description),
|
||||||
|
|
||||||
// Node
|
// Node
|
||||||
Command::Node(sub) => match sub {
|
Command::Node(sub) => match sub {
|
||||||
NodeCmd::Delete { key } => cmd_node_delete(&key),
|
NodeCmd::Delete { key } => cli::node::cmd_node_delete(&key),
|
||||||
NodeCmd::Rename { old_key, new_key } => cmd_node_rename(&old_key, &new_key),
|
NodeCmd::Rename { old_key, new_key } => cli::node::cmd_node_rename(&old_key, &new_key),
|
||||||
NodeCmd::List { pattern } => cmd_list_keys(pattern.as_deref()),
|
NodeCmd::List { pattern } => cli::node::cmd_list_keys(pattern.as_deref()),
|
||||||
NodeCmd::Edges => cmd_list_edges(),
|
NodeCmd::Edges => cli::node::cmd_list_edges(),
|
||||||
NodeCmd::Dump => cmd_dump_json(),
|
NodeCmd::Dump => cli::node::cmd_dump_json(),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Journal
|
// Journal
|
||||||
|
|
@ -827,8 +827,8 @@ fn main() {
|
||||||
AdminCmd::LoadContext { stats } => cmd_load_context(stats),
|
AdminCmd::LoadContext { stats } => cmd_load_context(stats),
|
||||||
AdminCmd::Log => cmd_log(),
|
AdminCmd::Log => cmd_log(),
|
||||||
AdminCmd::Params => cmd_params(),
|
AdminCmd::Params => cmd_params(),
|
||||||
AdminCmd::LookupBump { keys } => cmd_lookup_bump(&keys),
|
AdminCmd::LookupBump { keys } => cli::node::cmd_lookup_bump(&keys),
|
||||||
AdminCmd::Lookups { date } => cmd_lookups(date.as_deref()),
|
AdminCmd::Lookups { date } => cli::node::cmd_lookups(date.as_deref()),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1416,91 +1416,6 @@ fn cmd_status() -> Result<(), String> {
|
||||||
Ok(())
|
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<String>) -> Result<(), String> {
|
fn cmd_consolidate_batch(count: usize, auto: bool, agent: Option<String>) -> Result<(), String> {
|
||||||
let store = store::Store::load()?;
|
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)
|
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)> {
|
fn get_group_content(group: &config::ContextGroup, store: &store::Store, cfg: &config::Config) -> Vec<(String, String)> {
|
||||||
match group.source {
|
match group.source {
|
||||||
config::ContextSource::Journal => {
|
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<store::Node> = Vec::new();
|
|
||||||
while let Ok(msg) = capnp::serialize::read_message(&mut reader, capnp::message::ReaderOptions::new()) {
|
|
||||||
let log = msg.get_root::<poc_memory::memory_capnp::node_log::Reader>()
|
|
||||||
.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> {
|
fn cmd_tail(n: usize, full: bool) -> Result<(), String> {
|
||||||
let path = store::nodes_path();
|
let path = store::nodes_path();
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
|
|
@ -2153,33 +1886,6 @@ fn cmd_tail(n: usize, full: bool) -> Result<(), String> {
|
||||||
Ok(())
|
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> {
|
fn cmd_import(files: &[String]) -> Result<(), String> {
|
||||||
if files.is_empty() {
|
if files.is_empty() {
|
||||||
return Err("import requires at least one file path".into());
|
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)
|
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<String> = 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::<u64>());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_daemon(sub: DaemonCmd) -> Result<(), String> {
|
fn cmd_daemon(sub: DaemonCmd) -> Result<(), String> {
|
||||||
match sub {
|
match sub {
|
||||||
DaemonCmd::Start => daemon::run_daemon(),
|
DaemonCmd::Start => daemon::run_daemon(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue