Add relationship graph
This commit is contained in:
+110
@@ -0,0 +1,110 @@
|
|||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import networkx as nx
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from enhanced_toon import EnhancedToon
|
||||||
|
|
||||||
|
class LocalGraph:
|
||||||
|
def __init__(self, root_path: str):
|
||||||
|
self.root_path = root_path
|
||||||
|
self.graph_dir = os.path.join(root_path, ".mcp_cache", "graph")
|
||||||
|
os.makedirs(self.graph_dir, exist_ok=True)
|
||||||
|
self.graph_path = os.path.join(self.graph_dir, "graph.pkl")
|
||||||
|
self.graph = nx.DiGraph()
|
||||||
|
|
||||||
|
# --- Creation ---
|
||||||
|
def add_node(self, node_id: str, **attrs):
|
||||||
|
self.graph.add_node(node_id, **attrs)
|
||||||
|
|
||||||
|
def add_edge(self, src: str, dst: str, edge_type: str, **attrs):
|
||||||
|
self.graph.add_edge(src, dst, type=edge_type, **attrs)
|
||||||
|
|
||||||
|
# --- Persistence ---
|
||||||
|
def save(self):
|
||||||
|
with open(self.graph_path, "wb") as f:
|
||||||
|
pickle.dump(self.graph, f, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
|
|
||||||
|
def load(self) -> bool:
|
||||||
|
if not os.path.exists(self.graph_path):
|
||||||
|
return False
|
||||||
|
with open(self.graph_path, "rb") as f:
|
||||||
|
self.graph = pickle.load(f)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.graph = nx.DiGraph()
|
||||||
|
if os.path.exists(self.graph_path):
|
||||||
|
os.remove(self.graph_path)
|
||||||
|
|
||||||
|
# --- Query helpers ---
|
||||||
|
def find_nodes(self, **filters):
|
||||||
|
for node, data in self.graph.nodes(data=True):
|
||||||
|
if all(data.get(k) == v for k, v in filters.items()):
|
||||||
|
yield node, data
|
||||||
|
|
||||||
|
def neighbors(self, node_id, edge_type: Optional[str] = None):
|
||||||
|
for n in self.graph.successors(node_id):
|
||||||
|
if not edge_type or self.graph.edges[node_id, n]["type"] == edge_type:
|
||||||
|
yield n, self.graph.nodes[n]
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
out = {
|
||||||
|
"nodes": [
|
||||||
|
{"id": n, **data} for n, data in self.graph.nodes(data=True)
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{"src": s, "dst": d, **data} for s, d, data in self.graph.edges(data=True)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
path = os.path.join(self.graph_dir, "graph.json")
|
||||||
|
with open(path, "w") as f:
|
||||||
|
import json
|
||||||
|
json.dump(out, f, indent=2)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def to_toon(self):
|
||||||
|
"""
|
||||||
|
Export graph to compact TOON (Token-Oriented Object Notation) format.
|
||||||
|
This reduces token count by 30-60% for LLM consumption.
|
||||||
|
"""
|
||||||
|
node_list = []
|
||||||
|
for node_id, data in self.graph.nodes(data=True):
|
||||||
|
# Build a flattened row
|
||||||
|
row = {
|
||||||
|
"id": node_id,
|
||||||
|
"type": data.get("type", ""),
|
||||||
|
"name": data.get("name", ""),
|
||||||
|
"lang": data.get("lang", ""),
|
||||||
|
"file": data.get("file", ""),
|
||||||
|
}
|
||||||
|
node_list.append(row)
|
||||||
|
|
||||||
|
edge_list = []
|
||||||
|
for src, dst, data in self.graph.edges(data=True):
|
||||||
|
edge_list.append({
|
||||||
|
"src": src,
|
||||||
|
"dst": dst,
|
||||||
|
"type": data.get("type", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"# Graph Export",
|
||||||
|
f"nodes[{len(node_list)}]{{id,type,name,lang,file}}:"
|
||||||
|
]
|
||||||
|
for n in node_list:
|
||||||
|
row = [n["id"], n["type"], n["name"], n["lang"], n["file"]]
|
||||||
|
escaped = [EnhancedToon._escape_toon_field(str(f)) for f in row]
|
||||||
|
lines.append(" " + ",".join(escaped))
|
||||||
|
|
||||||
|
lines.append(f"edges[{len(edge_list)}]{{src,dst,type}}:")
|
||||||
|
for e in edge_list:
|
||||||
|
row = [e["src"], e["dst"], e["type"]]
|
||||||
|
escaped = [EnhancedToon._escape_toon_field(str(f)) for f in row]
|
||||||
|
lines.append(" " + ",".join(escaped))
|
||||||
|
|
||||||
|
toon_text = "\n".join(lines)
|
||||||
|
path = os.path.join(self.graph_dir, "graph.toon")
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(toon_text)
|
||||||
|
return path
|
||||||
+1388
-239
File diff suppressed because it is too large
Load Diff
@@ -26,3 +26,5 @@ psutil==5.9.5
|
|||||||
# --- SQL parsing stack ---
|
# --- SQL parsing stack ---
|
||||||
sqlglot==25.3.0 # Portable SQL AST + transpiler (fast for SELECTs, DML)
|
sqlglot==25.3.0 # Portable SQL AST + transpiler (fast for SELECTs, DML)
|
||||||
sqlparse==0.5.1 # Lightweight fallback tokenizer (simple/heuristic parsing)
|
sqlparse==0.5.1 # Lightweight fallback tokenizer (simple/heuristic parsing)
|
||||||
|
|
||||||
|
networkx==3.1 # Graph library for dependency graph
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
// Build from project root:
|
||||||
|
// rustc -O -o tools/parse_rust_ast tools/parse_rust_ast.rs
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::process;
|
||||||
|
use syn::{FnArg, Item, ItemEnum, ItemFn, ItemImpl, ItemStruct, ItemTrait, Pat, Type, Visibility};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct RustDecl {
|
||||||
|
name: String,
|
||||||
|
type_: String,
|
||||||
|
start_line: usize,
|
||||||
|
end_line: usize,
|
||||||
|
visibility: String,
|
||||||
|
is_async: bool,
|
||||||
|
is_unsafe: bool,
|
||||||
|
generics: Vec<String>,
|
||||||
|
traits: Vec<String>,
|
||||||
|
fields: Vec<String>,
|
||||||
|
methods: Vec<String>,
|
||||||
|
return_type: Option<String>,
|
||||||
|
parameters: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() < 2 {
|
||||||
|
eprintln!("Usage: {} <file.rs>", args[0]);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = &args[1];
|
||||||
|
let content = match fs::read_to_string(filename) {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("ERR: Failed to read file {}: {}", filename, e);
|
||||||
|
process::exit(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let syntax = match syn::parse_file(&content) {
|
||||||
|
Ok(syntax) => syntax,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("ERR: Failed to parse Rust file {}: {}", filename, e);
|
||||||
|
process::exit(3);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut decls = Vec::new();
|
||||||
|
|
||||||
|
for item in syntax.items {
|
||||||
|
match item {
|
||||||
|
Item::Fn(item_fn) => {
|
||||||
|
if let Some(decl) = parse_function(item_fn, &content) {
|
||||||
|
decls.push(decl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item::Struct(item_struct) => {
|
||||||
|
if let Some(decl) = parse_struct(item_struct, &content) {
|
||||||
|
decls.push(decl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item::Enum(item_enum) => {
|
||||||
|
if let Some(decl) = parse_enum(item_enum, &content) {
|
||||||
|
decls.push(decl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item::Trait(item_trait) => {
|
||||||
|
if let Some(decl) = parse_trait(item_trait, &content) {
|
||||||
|
decls.push(decl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item::Impl(item_impl) => {
|
||||||
|
if let Some(decl) = parse_impl(item_impl, &content) {
|
||||||
|
decls.push(decl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item::Mod(item_mod) => {
|
||||||
|
if let Some(decl) = parse_mod(item_mod, &content) {
|
||||||
|
decls.push(decl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = serde_json::to_string(&decls).unwrap();
|
||||||
|
println!("{}", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_function(item_fn: ItemFn, content: &str) -> Option<RustDecl> {
|
||||||
|
let start_line = item_fn.span().start().line;
|
||||||
|
let end_line = item_fn.span().end().line;
|
||||||
|
|
||||||
|
let mut parameters = Vec::new();
|
||||||
|
for input in &item_fn.sig.inputs {
|
||||||
|
if let FnArg::Typed(pat_type) = input {
|
||||||
|
if let Pat::Ident(pat_ident) = &*pat_type.pat {
|
||||||
|
parameters.push(pat_ident.ident.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let return_type = match &item_fn.sig.output {
|
||||||
|
syn::ReturnType::Default => None,
|
||||||
|
syn::ReturnType::Type(_, ty) => Some(type_to_string(ty)),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(RustDecl {
|
||||||
|
name: item_fn.sig.ident.to_string(),
|
||||||
|
type_: "function".to_string(),
|
||||||
|
start_line,
|
||||||
|
end_line,
|
||||||
|
visibility: visibility_to_string(&item_fn.vis),
|
||||||
|
is_async: item_fn.sig.asyncness.is_some(),
|
||||||
|
is_unsafe: item_fn.sig.unsafety.is_some(),
|
||||||
|
generics: item_fn
|
||||||
|
.sig
|
||||||
|
.generics
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.map(|param| param.to_string())
|
||||||
|
.collect(),
|
||||||
|
traits: Vec::new(),
|
||||||
|
fields: Vec::new(),
|
||||||
|
methods: Vec::new(),
|
||||||
|
return_type,
|
||||||
|
parameters,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_struct(item_struct: ItemStruct, content: &str) -> Option<RustDecl> {
|
||||||
|
let start_line = item_struct.span().start().line;
|
||||||
|
let end_line = item_struct.span().end().line;
|
||||||
|
|
||||||
|
let fields: Vec<String> = item_struct
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.filter_map(|field| field.ident.as_ref().map(|ident| ident.to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(RustDecl {
|
||||||
|
name: item_struct.ident.to_string(),
|
||||||
|
type_: "struct".to_string(),
|
||||||
|
start_line,
|
||||||
|
end_line,
|
||||||
|
visibility: visibility_to_string(&item_struct.vis),
|
||||||
|
is_async: false,
|
||||||
|
is_unsafe: false,
|
||||||
|
generics: item_struct
|
||||||
|
.generics
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.map(|param| param.to_string())
|
||||||
|
.collect(),
|
||||||
|
traits: Vec::new(),
|
||||||
|
fields,
|
||||||
|
methods: Vec::new(),
|
||||||
|
return_type: None,
|
||||||
|
parameters: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_enum(item_enum: ItemEnum, content: &str) -> Option<RustDecl> {
|
||||||
|
let start_line = item_enum.span().start().line;
|
||||||
|
let end_line = item_enum.span().end().line;
|
||||||
|
|
||||||
|
let variants: Vec<String> = item_enum
|
||||||
|
.variants
|
||||||
|
.iter()
|
||||||
|
.map(|variant| variant.ident.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(RustDecl {
|
||||||
|
name: item_enum.ident.to_string(),
|
||||||
|
type_: "enum".to_string(),
|
||||||
|
start_line,
|
||||||
|
end_line,
|
||||||
|
visibility: visibility_to_string(&item_enum.vis),
|
||||||
|
is_async: false,
|
||||||
|
is_unsafe: false,
|
||||||
|
generics: item_enum
|
||||||
|
.generics
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.map(|param| param.to_string())
|
||||||
|
.collect(),
|
||||||
|
traits: Vec::new(),
|
||||||
|
fields: variants,
|
||||||
|
methods: Vec::new(),
|
||||||
|
return_type: None,
|
||||||
|
parameters: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_trait(item_trait: ItemTrait, content: &str) -> Option<RustDecl> {
|
||||||
|
let start_line = item_trait.span().start().line;
|
||||||
|
let end_line = item_trait.span().end().line;
|
||||||
|
|
||||||
|
let methods: Vec<String> = item_trait
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| {
|
||||||
|
if let syn::TraitItem::Fn(method) = item {
|
||||||
|
Some(method.sig.ident.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(RustDecl {
|
||||||
|
name: item_trait.ident.to_string(),
|
||||||
|
type_: "trait".to_string(),
|
||||||
|
start_line,
|
||||||
|
end_line,
|
||||||
|
visibility: visibility_to_string(&item_trait.vis),
|
||||||
|
is_async: false,
|
||||||
|
is_unsafe: false,
|
||||||
|
generics: item_trait
|
||||||
|
.generics
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.map(|param| param.to_string())
|
||||||
|
.collect(),
|
||||||
|
traits: Vec::new(),
|
||||||
|
fields: Vec::new(),
|
||||||
|
methods,
|
||||||
|
return_type: None,
|
||||||
|
parameters: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_impl(item_impl: ItemImpl, content: &str) -> Option<RustDecl> {
|
||||||
|
let start_line = item_impl.span().start().line;
|
||||||
|
let end_line = item_impl.span().end().line;
|
||||||
|
|
||||||
|
let target = type_to_string(&item_impl.self_ty);
|
||||||
|
let methods: Vec<String> = item_impl
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| {
|
||||||
|
if let syn::ImplItem::Fn(method) = item {
|
||||||
|
Some(method.sig.ident.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let traits: Vec<String> = item_impl
|
||||||
|
.trait_
|
||||||
|
.as_ref()
|
||||||
|
.map(|(_, path, _)| path_to_string(path))
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(RustDecl {
|
||||||
|
name: format!("impl_{}", target),
|
||||||
|
type_: "impl".to_string(),
|
||||||
|
start_line,
|
||||||
|
end_line,
|
||||||
|
visibility: "".to_string(),
|
||||||
|
is_async: false,
|
||||||
|
is_unsafe: item_impl.unsafety.is_some(),
|
||||||
|
generics: item_impl
|
||||||
|
.generics
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.map(|param| param.to_string())
|
||||||
|
.collect(),
|
||||||
|
traits,
|
||||||
|
fields: Vec::new(),
|
||||||
|
methods,
|
||||||
|
return_type: None,
|
||||||
|
parameters: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_mod(item_mod: syn::ItemMod, content: &str) -> Option<RustDecl> {
|
||||||
|
let start_line = item_mod.span().start().line;
|
||||||
|
let end_line = item_mod.span().end().line;
|
||||||
|
|
||||||
|
Some(RustDecl {
|
||||||
|
name: item_mod.ident.to_string(),
|
||||||
|
type_: "module".to_string(),
|
||||||
|
start_line,
|
||||||
|
end_line,
|
||||||
|
visibility: visibility_to_string(&item_mod.vis),
|
||||||
|
is_async: false,
|
||||||
|
is_unsafe: false,
|
||||||
|
generics: Vec::new(),
|
||||||
|
traits: Vec::new(),
|
||||||
|
fields: Vec::new(),
|
||||||
|
methods: Vec::new(),
|
||||||
|
return_type: None,
|
||||||
|
parameters: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visibility_to_string(vis: &Visibility) -> String {
|
||||||
|
match vis {
|
||||||
|
Visibility::Public(_) => "pub".to_string(),
|
||||||
|
_ => "private".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_to_string(ty: &Type) -> String {
|
||||||
|
format!("{}", quote::quote!(#ty))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_to_string(path: &syn::Path) -> String {
|
||||||
|
format!("{}", quote::quote!(#path))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user