refactor: consolidate nvim crates, extract server library, and update workspace dependencies

This commit is contained in:
Riz Ashraf committed 2026-10-04 01:42:59 +01:00
1 parent a083719cf1
commit 533adfd41b
53 files changed
+5967 -1230

No files matched your search

+434 -5
View File
@@ -119,8 +119,9 @@ impl McpTool for CreateEntitiesHandler {
let req: CreateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut inserted = Vec::new();
state.modify_graph(|g| {
for entity in req.entities {
for mut entity in req.entities {
if !entity.name.is_empty() {
entity.entity_type = crate::models::normalize_entity_type(&entity.entity_type);
inserted.push(entity.clone());
g.entities.insert(entity.name.clone(), entity);
}
@@ -165,8 +166,9 @@ impl McpTool for CreateRelationsHandler {
};
let mut missing_nodes = std::collections::HashSet::new();
state.modify_graph(|g| {
for relation in req.relations {
for mut relation in req.relations {
if !relation.from.is_empty() && !relation.to.is_empty() {
relation.relation_type = crate::models::normalize_relation_type(&relation.relation_type);
let from_exists = g.entities.contains_key(&relation.from);
let to_exists = g.entities.contains_key(&relation.to);
if from_exists && to_exists {
@@ -353,8 +355,9 @@ impl McpTool for ReadGraphHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ReadGraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let max_tokens = req.max_tokens;
let data = state.read_graph(|full| -> crate::error::Result<String> {
if let Some(ns) = req.namespace {
let mut result_json = if let Some(ns) = req.namespace {
let mut filtered = BorrowedGraph::default();
for (k, v) in &full.entities {
if v.namespace == ns {
@@ -366,10 +369,19 @@ impl McpTool for ReadGraphHandler {
filtered.relations.push(r);
}
}
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
serde_json::to_string(&filtered)?
} else {
Ok::<String, crate::error::AppError>(serde_json::to_string(full)?)
serde_json::to_string(full)?
};
if let Some(max_tok) = max_tokens {
let max_chars = max_tok * 4;
if result_json.len() > max_chars {
result_json.truncate(max_chars);
result_json.push_str("... [TRUNCATED_TO_MAX_TOKENS]");
}
}
Ok(result_json)
})?;
Ok(data)
}
@@ -553,6 +565,54 @@ impl McpTool for CondenseEntityHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: CondenseEntityTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut missing = false;
if req.summarized_observations.is_empty() {
let mut obs_to_condense = Vec::new();
state.read_graph(|g| {
if let Some(e) = g.entities.get(&req.entity_name) {
obs_to_condense = e.observations.clone();
} else {
missing = true;
}
});
if missing {
return Err(crate::error::AppError::Internal(format!(
"Error: Entity '{}' not found. Please verify the exact entity name using search_nodes.",
req.entity_name
)));
}
let mut unique_obs: Vec<String> = Vec::new();
for obs in obs_to_condense {
let clean = obs.trim().to_string();
if !clean.is_empty() && !unique_obs.contains(&clean) {
unique_obs.push(clean);
}
}
if unique_obs.len() > 3 && state.ollama.is_available().await {
let prompt = format!(
"Condense the following observations for entity '{}' into 2-3 concise architectural facts:\n- {}",
req.entity_name,
unique_obs.join("\n- ")
);
if let Ok(summary) = state.ollama.generate(&prompt, None, None).await {
let lines: Vec<String> = summary
.lines()
.map(|l| l.trim().trim_start_matches('-').trim().to_string())
.filter(|l| !l.is_empty())
.collect();
if !lines.is_empty() {
unique_obs = lines;
}
}
}
state.modify_graph(|master| {
if let Some(e) = master.entities.get_mut(&req.entity_name) {
e.observations = unique_obs;
}
});
return Ok(format!("Entity '{}' observations condensed with deduplication/AI summary.", req.entity_name));
}
state.modify_graph(|master| {
if let Some(e) = master.entities.get_mut(&req.entity_name) {
e.observations = req.summarized_observations;
@@ -655,14 +715,316 @@ impl McpTool for FindOrphansHandler {
}
}
pub struct GetSubgraphHandler;
#[async_trait]
impl McpTool for GetSubgraphHandler {
fn name(&self) -> &'static str {
"get_subgraph"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<GetSubgraphTool>("get_subgraph", "Execute get_subgraph")
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: GetSubgraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let root = req
.root_entity
.or(req.root_node)
.ok_or_else(|| crate::error::AppError::Internal("root_entity or root_node is required".to_string()))?;
let depth = req.depth.unwrap_or(2);
let format = req.format.unwrap_or(SubgraphFormat::Json);
if format == SubgraphFormat::MarkdownTree {
let (sub_entities, sub_relations) = state.read_graph(|g| {
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back((root.clone(), 0));
visited.insert(root.clone());
while let Some((curr, d)) = queue.pop_front() {
if d >= depth {
continue;
}
for r in &g.relations {
if r.from == curr && !visited.contains(&r.to) {
visited.insert(r.to.clone());
queue.push_back((r.to.clone(), d + 1));
} else if r.to == curr && !visited.contains(&r.from) {
visited.insert(r.from.clone());
queue.push_back((r.from.clone(), d + 1));
}
}
}
let ent: Vec<_> = g.entities.values().filter(|e| visited.contains(&e.name)).cloned().collect();
let rel: Vec<_> = g.relations.iter().filter(|r| visited.contains(&r.from) && visited.contains(&r.to)).cloned().collect();
(ent, rel)
});
let mut markdown = format!("# Subgraph Topology for `{}`\n\n", root);
markdown.push_str("## Entities\n");
for e in &sub_entities {
markdown.push_str(&format!("- **{}** (`{}`)\n", e.name, e.entity_type));
for obs in &e.observations {
markdown.push_str(&format!(" - {}\n", obs));
}
}
markdown.push_str("\n## Relations\n");
for r in &sub_relations {
markdown.push_str(&format!("- `{}` --[{}]--> `{}`\n", r.from, r.relation_type, r.to));
}
return Ok(markdown);
}
let data = state.read_graph(|full| {
let mut visited_nodes: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut matched_entities = std::collections::HashMap::new();
let mut matched_relations = Vec::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back((root.clone(), 0));
visited_nodes.insert(root.clone());
while let Some((curr, current_depth)) = queue.pop_front() {
if let Some(entity) = full.entities.get(&curr) {
matched_entities.insert(curr.clone(), entity.clone());
}
if current_depth < depth {
for rel in &full.relations {
if rel.from == curr {
matched_relations.push(rel.clone());
if !visited_nodes.contains(&rel.to) {
visited_nodes.insert(rel.to.clone());
queue.push_back((rel.to.clone(), current_depth + 1));
}
} else if rel.to == curr {
matched_relations.push(rel.clone());
if !visited_nodes.contains(&rel.from) {
visited_nodes.insert(rel.from.clone());
queue.push_back((rel.from.clone(), current_depth + 1));
}
}
}
}
}
let result = serde_json::json!({
"root_node": root,
"depth": depth,
"entities": matched_entities,
"relations": matched_relations,
});
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&result)?)
})?;
Ok(data)
}
}
use crate::handlers::utils::*;
pub struct SweepGraphHealthHandler;
#[async_trait]
impl McpTool for SweepGraphHealthHandler {
fn name(&self) -> &'static str {
"sweep_graph_health"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<SweepGraphHealthTool>("sweep_graph_health", "Audit and sweep graph health")
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: SweepGraphHealthTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let auto_prune = req.auto_prune_orphans.unwrap_or(false);
let mut orphans = Vec::new();
let mut duplicates = Vec::new();
state.modify_graph(|g| {
// 1. Identify Orphans
let mut connected = std::collections::HashSet::new();
for r in &g.relations {
connected.insert(r.from.clone());
connected.insert(r.to.clone());
}
for name in g.entities.keys() {
if !connected.contains(name) {
orphans.push(name.clone());
}
}
if auto_prune {
for orphan in &orphans {
g.entities.remove(orphan);
}
}
// 2. Compute similarity pairs for duplicate detection
let names: Vec<_> = g.entities.keys().cloned().collect();
for i in 0..names.len() {
for j in (i + 1)..names.len() {
let n1 = &names[i];
let n2 = &names[j];
let l1 = n1.to_lowercase();
let l2 = n2.to_lowercase();
if l1 == l2 || ((l1.contains(&l2) || l2.contains(&l1)) && l1.len().min(l2.len()) > 3) {
duplicates.push(serde_json::json!({
"entity_a": n1,
"entity_b": n2,
"suggested_action": format!("merge_entities(source: '{}', target: '{}')", n1, n2)
}));
}
}
}
});
let report = serde_json::json!({
"orphaned_entities": orphans,
"orphans_pruned": auto_prune,
"potential_duplicates": duplicates,
"health_score": if orphans.is_empty() && duplicates.is_empty() { "100%" } else { "Needs Maintenance" }
});
Ok(serde_json::to_string_pretty(&report)?)
}
}
pub struct ResolveStaleSymbolsHandler;
#[async_trait]
impl McpTool for ResolveStaleSymbolsHandler {
fn name(&self) -> &'static str {
"resolve_stale_symbols"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ResolveStaleSymbolsTool>("resolve_stale_symbols", "Inspect Knowledge Graph entities and tech debt symbol/line references against files on disk, healing stale pointers.")
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ResolveStaleSymbolsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let auto_heal = req.auto_heal.unwrap_or(true);
let mut checked = 0;
let mut stale_entities = Vec::new();
state.modify_graph(|g| {
for (name, _entity) in g.entities.iter_mut() {
if let Some(target) = &req.target {
if name != target && !name.contains(target) {
continue;
}
}
checked += 1;
let path = std::path::Path::new(name);
if (name.contains('/') || name.contains('\\') || name.contains('.')) && !path.exists() {
stale_entities.push(name.clone());
}
}
if auto_heal {
for stale in &stale_entities {
g.entities.remove(stale);
g.relations.retain(|r| &r.from != stale && &r.to != stale);
}
}
});
let report = serde_json::json!({
"checked_count": checked,
"stale_entities_found": stale_entities,
"auto_healed": auto_heal,
"status": "success"
});
Ok(serde_json::to_string_pretty(&report)?)
}
}
pub struct SummarizeSubgraphHandler;
#[async_trait]
impl McpTool for SummarizeSubgraphHandler {
fn name(&self) -> &'static str {
"summarize_subgraph"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<SummarizeSubgraphTool>("summarize_subgraph", "Generate compact, LLM-optimized Markdown topology of a graph component capped within a token budget.")
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: SummarizeSubgraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let depth = req.depth.unwrap_or(2);
let (sub_entities, sub_relations) = state.read_graph(|g| {
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back((req.root_entity.clone(), 0));
visited.insert(req.root_entity.clone());
while let Some((curr, d)) = queue.pop_front() {
if d >= depth {
continue;
}
for r in &g.relations {
if r.from == curr && !visited.contains(&r.to) {
visited.insert(r.to.clone());
queue.push_back((r.to.clone(), d + 1));
} else if r.to == curr && !visited.contains(&r.from) {
visited.insert(r.from.clone());
queue.push_back((r.from.clone(), d + 1));
}
}
}
let ent: Vec<_> = g.entities.values().filter(|e| visited.contains(&e.name)).cloned().collect();
let rel: Vec<_> = g.relations.iter().filter(|r| visited.contains(&r.from) && visited.contains(&r.to)).cloned().collect();
(ent, rel)
});
let mut markdown = format!("# Subgraph Topology for `{}`\n\n", req.root_entity);
markdown.push_str("## Entities\n");
for e in &sub_entities {
markdown.push_str(&format!("- **{}** (`{}`)\n", e.name, e.entity_type));
for obs in &e.observations {
markdown.push_str(&format!(" - {}\n", obs));
}
}
markdown.push_str("\n## Relations\n");
for r in &sub_relations {
markdown.push_str(&format!("- `{}` --[{}]--> `{}`\n", r.from, r.relation_type, r.to));
}
let max_tokens = req.max_tokens.unwrap_or(1000);
let max_chars = max_tokens * 4;
if markdown.len() > max_chars {
markdown.truncate(max_chars);
markdown.push_str("\n... [Truncated to fit token budget]");
}
Ok(markdown)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handlers::meta::{BroadcastAgentSignalHandler, QueryAgentSignalsHandler};
use serde_json::json;
use tempfile::tempdir;
#[tokio::test]
async fn test_create_and_read_entities() {
let dir = tempdir().unwrap();
@@ -888,4 +1250,71 @@ mod tests {
.unwrap();
assert!(!res_orphans.contains("Y"));
}
#[tokio::test]
async fn test_more_graph_handlers() {
let dir = tempfile::tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let create_handler = CreateEntitiesHandler;
let args_ent = json!({
"entities": [
{"name": "Alpha", "entity_type": "Node", "observations": ["Obs1", "Obs2"], "namespace": "global"},
{"name": "Beta", "entity_type": "Node", "observations": ["Obs3"], "namespace": "global"}
]
});
create_handler.execute(args_ent, state.clone()).await.unwrap();
let rel_handler = CreateRelationsHandler;
let args_rel = json!({
"relations": [
{"from": "Alpha", "to": "Beta", "relation_type": "links_to", "namespace": "global"}
]
});
rel_handler.execute(args_rel, state.clone()).await.unwrap();
let path_handler = QueryGraphPathHandler;
let path_res = path_handler.execute(json!({"start_node": "Alpha", "end_node": "Beta"}), state.clone()).await.unwrap();
assert!(path_res.contains("Alpha"));
let sub_handler = GetSubgraphHandler;
let sub_res = sub_handler.execute(json!({"root_entity": "Alpha"}), state.clone()).await.unwrap();
assert!(sub_res.contains("Alpha"));
let sum_handler = SummarizeSubgraphHandler;
let sum_res = sum_handler.execute(json!({"root_entity": "Alpha"}), state.clone()).await.unwrap();
assert!(!sum_res.is_empty());
let sweep_handler = SweepGraphHealthHandler;
let sweep_res = sweep_handler.execute(json!({}), state.clone()).await.unwrap();
assert!(!sweep_res.is_empty());
let del_obs_handler = DeleteObservationsHandler;
let del_obs_res = del_obs_handler.execute(json!({
"deletions": [{"entity_name": "Alpha", "observations": ["Obs1"]}]
}), state.clone()).await.unwrap();
assert_eq!(del_obs_res, "Observations deleted");
let del_rel_handler = DeleteRelationsHandler;
let del_rel_res = del_rel_handler.execute(json!({
"relations": [{"from": "Alpha", "to": "Beta", "relation_type": "links_to"}]
}), state.clone()).await.unwrap();
assert_eq!(del_rel_res, "Relations deleted");
let bcast_handler = BroadcastAgentSignalHandler;
let bcast_res = bcast_handler.execute(json!({
"sender": "agent1",
"signal_type": "task_completed",
"payload": "fix_bug"
}), state.clone()).await.unwrap();
assert!(bcast_res.contains("Broadcasted signal"));
let qsignal_handler = QueryAgentSignalsHandler;
let qsignal_res = qsignal_handler.execute(json!({"sender": "agent1"}), state.clone()).await.unwrap();
assert!(qsignal_res.contains("task_completed"));
}
}