lean_ctx/core/gateway/adapters/
code_graph.rs1use crate::core::property_graph::CodeGraph;
13
14struct ParsedEdge {
15 from: String,
16 to: String,
17 kind: String,
18}
19
20pub fn ingest(server: &str, _tool: &str, text: &str, project_root: &str) {
23 let edges = parse_edges(text);
24 if edges.is_empty() {
25 return;
26 }
27 let Ok(pg) = CodeGraph::open(project_root) else {
28 tracing::warn!("[code-graph adapter] property graph open failed for `{server}`");
29 return;
30 };
31 for e in &edges {
32 let kind = if e.kind.is_empty() {
33 format!("{server}_edge")
34 } else {
35 e.kind.clone()
36 };
37 let _ = pg.upsert_cross_source_edge(&e.from, &e.to, &kind, 1.0);
38 }
39}
40
41fn parse_edges(text: &str) -> Vec<ParsedEdge> {
42 let Ok(v) = serde_json::from_str::<serde_json::Value>(text.trim()) else {
43 return Vec::new();
44 };
45 let array = v
46 .get("edges")
47 .and_then(serde_json::Value::as_array)
48 .or_else(|| v.as_array());
49 let Some(items) = array else {
50 return Vec::new();
51 };
52 items.iter().filter_map(parse_one).collect()
53}
54
55fn parse_one(item: &serde_json::Value) -> Option<ParsedEdge> {
56 let obj = item.as_object()?;
57 let from = str_field(obj, &["from", "source", "src", "caller"])?;
58 let to = str_field(obj, &["to", "target", "dst", "callee"])?;
59 let kind = str_field(obj, &["type", "kind", "relation", "label"]).unwrap_or_default();
60 Some(ParsedEdge { from, to, kind })
61}
62
63fn str_field(obj: &serde_json::Map<String, serde_json::Value>, keys: &[&str]) -> Option<String> {
65 keys.iter()
66 .filter_map(|k| obj.get(*k).and_then(serde_json::Value::as_str))
67 .map(str::trim)
68 .find(|s| !s.is_empty())
69 .map(String::from)
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn parses_edges_object_and_array_shapes() {
78 let obj = r#"{"edges":[{"from":"a.rs","to":"b.rs","type":"calls"}]}"#;
79 assert_eq!(parse_edges(obj).len(), 1);
80 let arr = r#"[{"source":"a.rs","target":"b.rs","relation":"imports"}]"#;
81 let e = parse_edges(arr);
82 assert_eq!(e.len(), 1);
83 assert_eq!(e[0].kind, "imports");
84 assert!(parse_edges("garbage").is_empty());
85 assert!(parse_edges(r#"{"nodes":[]}"#).is_empty());
86 }
87
88 #[test]
89 fn ingest_writes_cross_source_edges() {
90 let _lock = crate::core::data_dir::test_env_lock();
91 let proj = tempfile::tempdir().unwrap();
92 let root = proj.path().to_str().unwrap();
93
94 let payload = r#"{"edges":[
95 {"from":"src/auth.rs","to":"src/db.rs","type":"calls"},
96 {"source":"src/api.rs","target":"src/auth.rs","kind":"imports"}
97 ]}"#;
98 ingest("graphify", "query_graph", payload, root);
99
100 let pg = CodeGraph::open(root).expect("open graph");
101 assert_eq!(pg.cross_source_edge_count().unwrap(), 2);
102 let edges = pg.all_cross_source_edges();
103 assert!(
104 edges
105 .iter()
106 .any(|e| e.from == "src/auth.rs" && e.to == "src/db.rs")
107 );
108 }
109}