// cli/graph.rs — graph subcommand handlers // // Extracted from main.rs. All graph-related CLI commands: // link, link-add, link-impact, link-audit, cap-degree, // normalize-strengths, trace, spectral-*, organize, communities. use crate::{store, graph}; use crate::store::StoreView; pub fn cmd_graph() -> Result<(), String> { let store = store::Store::load()?; let g = store.build_graph(); println!("Graph: {} nodes, {} edges, {} communities", g.nodes().len(), g.edge_count(), g.community_count()); println!("σ={:.2} α={:.2} gini={:.3} cc={:.4}", g.small_world_sigma(), g.degree_power_law_exponent(), g.degree_gini(), g.avg_clustering_coefficient()); Ok(()) } pub fn cmd_cap_degree(max_deg: usize) -> Result<(), String> { let mut store = store::Store::load()?; let (hubs, pruned) = store.cap_degree(max_deg)?; store.save()?; println!("Capped {} hubs, pruned {} weak Auto edges (max_degree={})", hubs, pruned, max_deg); Ok(()) } pub fn cmd_normalize_strengths(apply: bool) -> Result<(), String> { if apply { super::check_dry_run(); } let result = crate::mcp_server::memory_rpc( "graph_normalize_strengths", serde_json::json!({"apply": apply}), ).map_err(|e| e.to_string())?; print!("{}", result); Ok(()) } pub fn cmd_link(key: &[String]) -> Result<(), String> { if key.is_empty() { return Err("link requires a key".into()); } let key = key.join(" "); let result = crate::mcp_server::memory_rpc( "memory_links", serde_json::json!({"key": key}), ).map_err(|e| e.to_string())?; print!("{}", result); Ok(()) } pub fn cmd_link_add(source: &str, target: &str, _reason: &[String]) -> Result<(), String> { super::check_dry_run(); let result = crate::mcp_server::memory_rpc( "memory_link_add", serde_json::json!({"source": source, "target": target}), ).map_err(|e| e.to_string())?; println!("{}", result); Ok(()) } pub fn cmd_link_set(source: &str, target: &str, strength: f32) -> Result<(), String> { super::check_dry_run(); let result = crate::mcp_server::memory_rpc( "memory_link_set", serde_json::json!({"source": source, "target": target, "strength": strength}), ).map_err(|e| e.to_string())?; println!("{}", result); Ok(()) } pub fn cmd_link_impact(source: &str, target: &str) -> Result<(), String> { let store = store::Store::load()?; let source = store.resolve_key(source)?; let target = store.resolve_key(target)?; let g = store.build_graph(); let impact = g.link_impact(&source, &target); println!("Link impact: {} → {}", source, target); println!(" Source degree: {} Target degree: {}", impact.source_deg, impact.target_deg); println!(" Hub link: {} Same community: {}", impact.is_hub_link, impact.same_community); println!(" ΔCC source: {:+.4} ΔCC target: {:+.4}", impact.delta_cc_source, impact.delta_cc_target); println!(" ΔGini: {:+.6}", impact.delta_gini); println!(" Assessment: {}", impact.assessment); Ok(()) } pub fn cmd_trace(key: &[String]) -> Result<(), String> { if key.is_empty() { return Err("trace requires a key".into()); } let key = key.join(" "); let store = store::Store::load()?; let resolved = store.resolve_key(&key)?; let g = store.build_graph(); let node = store.nodes.get(&resolved) .ok_or_else(|| format!("Node not found: {}", resolved))?; // Display the node itself println!("=== {} ===", resolved); println!("Type: {:?} Weight: {:.2}", node.node_type, node.weight); if !node.source_ref.is_empty() { println!("Source: {}", node.source_ref); } // Show content preview let preview = crate::util::truncate(&node.content, 200, "..."); println!("\n{}\n", preview); // Walk neighbors, grouped by node type let neighbors = g.neighbors(&resolved); let mut episodic_session = Vec::new(); let mut episodic_daily = Vec::new(); let mut episodic_weekly = Vec::new(); let mut semantic = Vec::new(); for (n, strength) in &neighbors { if let Some(nnode) = store.nodes.get(n.as_str()) { let entry = (n.as_str(), *strength, nnode); match nnode.node_type { store::NodeType::EpisodicSession => episodic_session.push(entry), store::NodeType::EpisodicDaily => episodic_daily.push(entry), store::NodeType::EpisodicWeekly | store::NodeType::EpisodicMonthly => episodic_weekly.push(entry), store::NodeType::Semantic => semantic.push(entry), } } } if !episodic_weekly.is_empty() { println!("Weekly digests:"); for (k, s, n) in &episodic_weekly { let preview = crate::util::first_n_chars(n.content.lines().next().unwrap_or(""), 80); println!(" [{:.2}] {} — {}", s, k, preview); } } if !episodic_daily.is_empty() { println!("Daily digests:"); for (k, s, n) in &episodic_daily { let preview = crate::util::first_n_chars(n.content.lines().next().unwrap_or(""), 80); println!(" [{:.2}] {} — {}", s, k, preview); } } if !episodic_session.is_empty() { println!("Session entries:"); for (k, s, n) in &episodic_session { let preview = crate::util::first_n_chars( n.content.lines() .find(|l| !l.is_empty() && !l.starts_with("