1use crate::graph::{Graph, Node, Edge, NodeStatus};
5use crate::code_graph::{CodeGraph, CodeNode, CodeEdge, NodeKind, EdgeRelation};
6
7
8pub fn codegraph_to_graph_nodes(cg: &CodeGraph, _project_root: &std::path::Path) -> (Vec<Node>, Vec<Edge>) {
11 let mut nodes = Vec::with_capacity(cg.nodes.len());
12 let mut edges = Vec::with_capacity(cg.edges.len());
13
14 for cn in &cg.nodes {
15 let mut node = Node::new(&cn.id, &cn.name);
16 node.source = Some("extract".to_string());
17 node.node_type = Some("code".to_string());
18 node.node_kind = Some(format!("{:?}", cn.kind)); node.status = NodeStatus::Done;
20 node.file_path = Some(cn.file_path.clone());
21 if let Some(line) = cn.line {
22 node.start_line = Some(line);
23 }
24 if let Some(ref sig) = cn.signature {
25 node.signature = Some(sig.clone());
26 }
27 if let Some(ref doc) = cn.docstring {
28 node.doc_comment = Some(doc.clone());
29 }
30 if cn.is_test {
32 node.metadata.insert("is_test".to_string(), serde_json::json!(true));
33 }
34 if cn.line_count > 0 {
35 node.metadata.insert("line_count".to_string(), serde_json::json!(cn.line_count));
36 }
37 if !cn.decorators.is_empty() {
38 node.metadata.insert("decorators".to_string(), serde_json::json!(cn.decorators));
39 }
40 node.visibility = cn.visibility.clone();
42 node.lang = cn.lang.clone();
43 node.body_hash = cn.body_hash.clone();
44 node.end_line = cn.end_line;
45 if let Some(ref vis) = cn.visibility {
47 node.is_public = Some(vis == "pub" || vis == "export");
48 }
49 nodes.push(node);
50 }
51
52 for ce in &cg.edges {
53 let relation = ce.relation.to_string(); let mut edge = Edge::new(&ce.from, &ce.to, &relation);
55 edge.confidence = Some(ce.confidence as f64);
57 edge.weight = Some(ce.weight as f64);
58 let mut meta = serde_json::Map::new();
59 meta.insert("source".to_string(), serde_json::json!("extract"));
60 if ce.call_count > 1 {
61 meta.insert("call_count".to_string(), serde_json::json!(ce.call_count));
62 }
63 if ce.in_error_path {
64 meta.insert("in_error_path".to_string(), serde_json::json!(true));
65 }
66 edge.metadata = Some(serde_json::Value::Object(meta));
67 edges.push(edge);
68 }
69
70 (nodes, edges)
71}
72
73pub fn graph_to_codegraph(graph: &Graph) -> CodeGraph {
79 let code_nodes_refs = graph.code_nodes();
80 let code_edges_refs = graph.code_edges();
81
82 let mut nodes = Vec::with_capacity(code_nodes_refs.len());
83 let mut edges = Vec::with_capacity(code_edges_refs.len());
84
85 for n in &code_nodes_refs {
86 let kind = match n.node_kind.as_deref() {
87 Some("File") => NodeKind::File,
88 Some("Class") => NodeKind::Class,
89 Some("Function") => NodeKind::Function,
90 Some("Module") => NodeKind::Module,
91 Some("Constant") => NodeKind::Constant,
92 Some("Interface") => NodeKind::Interface,
93 Some("Enum") => NodeKind::Enum,
94 Some("TypeAlias") => NodeKind::TypeAlias,
95 Some("Trait") => NodeKind::Trait,
96 Some("Method") => NodeKind::Function, _ => NodeKind::File, };
99
100 let is_test = n.metadata.get("is_test")
101 .and_then(|v| v.as_bool())
102 .unwrap_or(false);
103 let line_count = n.metadata.get("line_count")
104 .and_then(|v| v.as_u64())
105 .unwrap_or(0) as usize;
106 let decorators: Vec<String> = n.metadata.get("decorators")
107 .and_then(|v| v.as_array())
108 .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
109 .unwrap_or_default();
110
111 nodes.push(CodeNode {
112 id: n.id.clone(),
113 kind,
114 name: n.title.clone(),
115 file_path: n.file_path.as_deref().unwrap_or("").to_string(),
116 line: n.start_line,
117 decorators,
118 signature: n.signature.clone(),
119 docstring: n.doc_comment.clone(),
120 line_count,
121 is_test,
122 visibility: n.visibility.clone(),
123 lang: n.lang.clone(),
124 body_hash: n.body_hash.clone(),
125 end_line: n.end_line,
126 complexity: None,
127 });
128 }
129
130 for e in &code_edges_refs {
131 let relation = match e.relation.as_str() {
132 "imports" => EdgeRelation::Imports,
133 "inherits" => EdgeRelation::Inherits,
134 "defined_in" => EdgeRelation::DefinedIn,
135 "calls" => EdgeRelation::Calls,
136 "tests_for" => EdgeRelation::TestsFor,
137 "overrides" => EdgeRelation::Overrides,
138 "implements" => EdgeRelation::Implements,
139 "belongs_to" => EdgeRelation::BelongsTo,
140 "type_reference" => EdgeRelation::TypeReference,
141 _ => continue, };
143
144 let meta = e.metadata.as_ref();
145 let weight = e.weight.map(|w| w as f32)
147 .or_else(|| meta.and_then(|m| m.get("weight")).and_then(|v| v.as_f64()).map(|v| v as f32))
148 .unwrap_or(0.5);
149 let call_count = meta.and_then(|m| m.get("call_count")).and_then(|v| v.as_u64()).unwrap_or(1) as u32;
150 let in_error_path = meta.and_then(|m| m.get("in_error_path")).and_then(|v| v.as_bool()).unwrap_or(false);
151 let confidence = e.confidence.map(|c| c as f32)
152 .or_else(|| meta.and_then(|m| m.get("confidence")).and_then(|v| v.as_f64()).map(|v| v as f32))
153 .unwrap_or(1.0);
154
155 edges.push(CodeEdge {
156 from: e.from.clone(),
157 to: e.to.clone(),
158 relation,
159 weight,
160 call_count,
161 in_error_path,
162 confidence,
163 call_site_line: None,
164 call_site_column: None,
165 });
166 }
167
168 let mut cg = CodeGraph {
169 nodes,
170 edges,
171 outgoing: Default::default(),
172 incoming: Default::default(),
173 node_index: Default::default(),
174 };
175 cg.build_indexes();
176 cg
177}
178
179pub fn merge_code_layer(graph: &mut Graph, code_nodes: Vec<Node>, code_edges: Vec<Edge>) {
183 graph.nodes.retain(|n| n.source.as_deref() != Some("extract"));
185 graph.edges.retain(|e| {
187 let src = e.source();
188 src != Some("extract") && src != Some("auto-bridge")
189 });
190 graph.nodes.extend(code_nodes);
192 graph.edges.extend(code_edges);
193
194 let node_ids: std::collections::HashSet<&str> =
200 graph.nodes.iter().map(|n| n.id.as_str()).collect();
201 graph.edges.retain(|e| {
202 let from_ok = node_ids.contains(e.from.as_str());
203 let to_ok = node_ids.contains(e.to.as_str());
204 if from_ok && to_ok {
205 return true;
206 }
207 let src = e.source();
210 if src != Some("extract") && src != Some("auto-bridge") {
211 let stale_from = !from_ok && e.from.starts_with("code_");
215 let stale_to = !to_ok && e.to.starts_with("code_");
216 if stale_from || stale_to {
217 return false; }
219 return true; }
221 false
223 });
224}
225
226pub fn merge_project_layer(existing: &mut Graph, new_project: Graph) {
229 let code_nodes: Vec<Node> = existing.nodes.drain(..).filter(|n| n.source.as_deref() == Some("extract")).collect();
231 let code_and_bridge_edges: Vec<Edge> = existing.edges.drain(..).filter(|e| {
232 let src = e.source();
233 src == Some("extract") || src == Some("auto-bridge")
234 }).collect();
235
236 let mut project_nodes: Vec<Node> = new_project.nodes.into_iter().map(|mut n| {
239 if n.source.is_none() {
240 n.source = Some("project".to_string());
241 }
242 n
243 }).collect();
244
245 existing.nodes = code_nodes;
247 existing.nodes.append(&mut project_nodes);
248
249 existing.edges = code_and_bridge_edges;
251 existing.edges.extend(new_project.edges);
252}
253
254pub fn generate_bridge_edges(graph: &mut Graph) {
262 graph.edges.retain(|e| e.source() != Some("auto-bridge"));
264
265 let project_info: Vec<(String, Vec<String>)> = graph.project_nodes().iter().map(|n| {
267 let code_paths: Vec<String> = n.metadata.get("code_paths")
268 .and_then(|v| v.as_array())
269 .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
270 .unwrap_or_default();
271 (n.id.clone(), code_paths)
272 }).collect();
273
274 let code_info: Vec<(String, Option<String>)> = graph.code_nodes().iter().map(|n| {
275 (n.id.clone(), n.file_path.clone())
276 }).collect();
277
278 let mut new_edges: Vec<Edge> = Vec::new();
279
280 for (code_id, file_path) in &code_info {
281 let mut matched = false;
282
283 if let Some(fp) = file_path {
285 for (proj_id, code_paths) in &project_info {
286 if code_paths.iter().any(|cp| cp == fp) {
287 let mut edge = Edge::new(proj_id, code_id, "maps_to");
288 edge.metadata = Some(serde_json::json!({"source": "auto-bridge", "confidence": 1.0}));
289 new_edges.push(edge);
290 matched = true;
291 }
292 }
293 }
294
295 if !matched {
297 let id_path = code_id.split(':').nth(1).unwrap_or(code_id);
300 let segments: Vec<&str> = id_path
301 .split('/')
302 .filter(|s| *s != "src" && *s != "lib" && *s != "mod.rs" && *s != "index.ts" && *s != "index.js")
303 .filter_map(|s| {
304 let name = s.split('.').next().unwrap_or(s);
305 if name.is_empty() || name == "main" || name == "mod" || name == "index" {
306 None
307 } else {
308 Some(name)
309 }
310 })
311 .collect();
312
313 for segment in &segments {
314 let seg_lower = segment.to_lowercase();
315 for (proj_id, _) in &project_info {
316 let proj_lower = proj_id.to_lowercase();
317 if proj_lower.contains(&seg_lower) {
318 let already = new_edges.iter().any(|e| e.from == *proj_id && e.to == *code_id);
320 if !already {
321 let mut edge = Edge::new(proj_id, code_id, "maps_to");
322 edge.metadata = Some(serde_json::json!({"source": "auto-bridge", "confidence": 0.8}));
323 new_edges.push(edge);
324 }
325 }
326 }
327 }
328 }
329 }
330
331 graph.edges.extend(new_edges);
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use crate::code_graph::{CodeNode, CodeEdge, CodeGraph, NodeKind, EdgeRelation};
338 use std::path::Path;
339
340 fn sample_codegraph() -> CodeGraph {
341 let mut cg = CodeGraph::default();
342 let file = CodeNode::new_file("src/main.rs");
343 let func = CodeNode::new_function("src/main.rs", "main", 5, false);
344 let class = CodeNode::new_class("src/auth.rs", "AuthService", 10);
345 cg.nodes = vec![file, func, class];
346 cg.edges = vec![
347 CodeEdge::new("func:src/main.rs:main", "file:src/main.rs", EdgeRelation::DefinedIn),
348 CodeEdge::new("func:src/main.rs:main", "class:src/auth.rs:AuthService", EdgeRelation::Calls),
349 ];
350 cg
351 }
352
353 #[test]
354 fn test_codegraph_to_graph_nodes_basic() {
355 let cg = sample_codegraph();
356 let (nodes, edges) = codegraph_to_graph_nodes(&cg, Path::new("/tmp/project"));
357 assert_eq!(nodes.len(), 3);
358 assert_eq!(edges.len(), 2);
359
360 for n in &nodes {
362 assert_eq!(n.source.as_deref(), Some("extract"));
363 assert_eq!(n.node_type.as_deref(), Some("code"));
364 assert_eq!(n.status, NodeStatus::Done);
365 }
366 }
367
368 #[test]
369 fn test_codegraph_node_kind_mapping() {
370 let cg = sample_codegraph();
371 let (nodes, _) = codegraph_to_graph_nodes(&cg, Path::new("/tmp"));
372
373 let file_node = nodes.iter().find(|n| n.id == "file:src/main.rs").unwrap();
374 assert_eq!(file_node.node_kind.as_deref(), Some("File"));
375
376 let func_node = nodes.iter().find(|n| n.id == "func:src/main.rs:main").unwrap();
377 assert_eq!(func_node.node_kind.as_deref(), Some("Function"));
378 assert_eq!(func_node.start_line, Some(5));
379 }
380
381 #[test]
382 fn test_codegraph_edge_conversion() {
383 let cg = sample_codegraph();
384 let (_, edges) = codegraph_to_graph_nodes(&cg, Path::new("/tmp"));
385
386 let defined_in = edges.iter().find(|e| e.relation == "defined_in").unwrap();
387 assert_eq!(defined_in.source(), Some("extract"));
388 assert!(defined_in.confidence.is_some(), "confidence must be set on Edge");
390 assert!(defined_in.weight.is_some(), "weight must be set on Edge");
391
392 let calls = edges.iter().find(|e| e.relation == "calls").unwrap();
393 assert_eq!(calls.source(), Some("extract"));
394 assert!(calls.confidence.is_some(), "confidence must be set on Edge");
395 assert!(calls.weight.is_some(), "weight must be set on Edge");
396 assert_eq!(calls.confidence, Some(1.0));
398 assert_eq!(calls.weight, Some(0.5));
399 }
400
401 #[test]
402 fn test_merge_code_layer() {
403 let mut graph = Graph::new();
404 let mut task = Node::new("task-1", "My Task");
406 task.source = Some("project".to_string());
407 graph.add_node(task);
408 graph.add_edge(Edge::new("task-1", "task-2", "depends_on"));
409
410 let mut old_code = Node::new("file:old.rs", "old file");
412 old_code.source = Some("extract".to_string());
413 graph.add_node(old_code);
414
415 let cg = sample_codegraph();
416 let (code_nodes, code_edges) = codegraph_to_graph_nodes(&cg, Path::new("/tmp"));
417 merge_code_layer(&mut graph, code_nodes, code_edges);
418
419 assert!(graph.nodes.iter().any(|n| n.id == "task-1"));
421 assert!(!graph.nodes.iter().any(|n| n.id == "file:old.rs"));
423 assert!(graph.nodes.iter().any(|n| n.id == "file:src/main.rs"));
425 assert!(graph.edges.iter().any(|e| e.relation == "depends_on"));
427 }
428
429 #[test]
430 fn test_merge_project_layer() {
431 let mut existing = Graph::new();
432 let mut code = Node::new("file:src/main.rs", "main.rs");
434 code.source = Some("extract".to_string());
435 existing.add_node(code);
436
437 let mut code_edge = Edge::new("file:src/main.rs", "func:main", "defined_in");
438 code_edge.metadata = Some(serde_json::json!({"source": "extract"}));
439 existing.add_edge(code_edge);
440
441 let mut old_task = Node::new("old-task", "Old");
443 old_task.source = Some("project".to_string());
444 existing.add_node(old_task);
445
446 let mut new_project = Graph::new();
448 new_project.add_node(Node::new("task-1", "New Task"));
449 new_project.add_edge(Edge::new("task-1", "task-2", "depends_on"));
450
451 merge_project_layer(&mut existing, new_project);
452
453 assert!(existing.nodes.iter().any(|n| n.id == "file:src/main.rs"));
455 assert!(!existing.nodes.iter().any(|n| n.id == "old-task"));
457 let task = existing.nodes.iter().find(|n| n.id == "task-1").unwrap();
459 assert_eq!(task.source.as_deref(), Some("project"));
460 }
461
462 #[test]
463 fn test_all_node_kinds() {
464 let mut cg = CodeGraph::default();
465 cg.nodes = vec![
466 CodeNode::new_file("src/lib.rs"),
467 CodeNode::new_class("src/lib.rs", "Foo", 1),
468 CodeNode::new_function("src/lib.rs", "bar", 10, false),
469 CodeNode::new_module("src/mod"),
470 CodeNode::new_constant("src/lib.rs", "MAX", 1),
471 CodeNode::new_interface("src/lib.rs", "IService", 20),
472 CodeNode::new_enum("src/e.rs", "Color", 1),
473 CodeNode::new_type_alias("src/t.rs", "Id", 1),
474 CodeNode::new_trait("src/tr.rs", "Storage", 1),
475 ];
476 let (nodes, _) = codegraph_to_graph_nodes(&cg, Path::new("/tmp"));
477 assert_eq!(nodes.len(), 9);
478 assert!(nodes.iter().all(|n| n.source.as_deref() == Some("extract")));
480 }
481
482 #[test]
483 fn test_edge_metadata_fields() {
484 let mut cg = CodeGraph::default();
485 cg.nodes = vec![
486 CodeNode::new_function("src/a.rs", "foo", 1, false),
487 CodeNode::new_function("src/b.rs", "bar", 1, false),
488 ];
489 let mut edge = CodeEdge::new("func:src/a.rs:foo", "func:src/b.rs:bar", EdgeRelation::Calls);
490 edge.call_count = 5;
491 edge.in_error_path = true;
492 edge.confidence = 0.8;
493 edge.weight = 0.9;
494 cg.edges = vec![edge];
495
496 let (_, edges) = codegraph_to_graph_nodes(&cg, Path::new("/tmp"));
497 assert_eq!(edges.len(), 1);
498 let e = &edges[0];
499 assert!((e.confidence.unwrap() - 0.8).abs() < 1e-6, "confidence should be ~0.8");
502 assert!((e.weight.unwrap() - 0.9).abs() < 1e-6, "weight should be ~0.9");
503 let meta = e.metadata.as_ref().unwrap();
504 assert_eq!(meta.get("source").unwrap(), "extract");
505 assert_eq!(meta.get("call_count").unwrap(), 5);
506 assert_eq!(meta.get("in_error_path").unwrap(), true);
507 assert!(meta.get("confidence").is_none());
509 assert!(meta.get("weight").is_none());
510 }
511
512 #[test]
513 fn test_node_metadata_fields() {
514 let mut cg = CodeGraph::default();
515 let mut func = CodeNode::new_function("src/test.rs", "test_foo", 10, false);
516 func.is_test = true;
517 func.line_count = 25;
518 func.decorators = vec!["#[test]".to_string()];
519 func.signature = Some("fn test_foo()".to_string());
520 func.docstring = Some("A test function".to_string());
521 cg.nodes = vec![func];
522
523 let (nodes, _) = codegraph_to_graph_nodes(&cg, Path::new("/tmp"));
524 let n = &nodes[0];
525 assert_eq!(n.signature.as_deref(), Some("fn test_foo()"));
526 assert_eq!(n.doc_comment.as_deref(), Some("A test function"));
527 assert_eq!(n.metadata.get("is_test"), Some(&serde_json::json!(true)));
528 assert_eq!(n.metadata.get("line_count"), Some(&serde_json::json!(25)));
529 assert_eq!(n.metadata.get("decorators"), Some(&serde_json::json!(["#[test]"])));
530 }
531
532 #[test]
533 fn test_merge_code_layer_removes_bridge_edges() {
534 let mut graph = Graph::new();
535 let mut bridge_edge = Edge::new("task-1", "file:src/main.rs", "touches");
537 bridge_edge.metadata = Some(serde_json::json!({"source": "auto-bridge"}));
538 graph.add_edge(bridge_edge);
539
540 graph.add_edge(Edge::new("task-1", "task-2", "depends_on"));
542
543 let (code_nodes, code_edges) = codegraph_to_graph_nodes(&CodeGraph::default(), Path::new("/tmp"));
544 merge_code_layer(&mut graph, code_nodes, code_edges);
545
546 assert!(!graph.edges.iter().any(|e| e.source() == Some("auto-bridge")));
548 assert!(graph.edges.iter().any(|e| e.relation == "depends_on"));
550 }
551
552 #[test]
555 fn test_cross_layer_query_traversal() {
556 use crate::query::QueryEngine;
557
558 let mut graph = Graph::new();
559
560 let mut feature = Node::new("feat-auth", "Auth Feature");
562 feature.source = Some("project".to_string());
563 feature.node_type = Some("feature".to_string());
564 graph.add_node(feature);
565
566 let mut task = Node::new("task-impl-auth", "Implement auth middleware");
567 task.source = Some("project".to_string());
568 task.node_type = Some("task".to_string());
569 task.description = Some("Implement JWT auth in src/auth.rs".to_string());
570 graph.add_node(task);
571
572 graph.add_edge(Edge::new("task-impl-auth", "feat-auth", "implements"));
574
575 let mut code_file = Node::new("file:src/auth.rs", "src/auth.rs");
577 code_file.source = Some("extract".to_string());
578 code_file.node_type = Some("code".to_string());
579 code_file.file_path = Some("src/auth.rs".to_string());
580 code_file.status = NodeStatus::Done;
581 graph.add_node(code_file);
582
583 let mut code_fn = Node::new("fn:verify_jwt", "verify_jwt");
584 code_fn.source = Some("extract".to_string());
585 code_fn.node_type = Some("code".to_string());
586 code_fn.file_path = Some("src/auth.rs".to_string());
587 code_fn.status = NodeStatus::Done;
588 graph.add_node(code_fn);
589
590 let mut code_edge = Edge::new("fn:verify_jwt", "file:src/auth.rs", "belongs_to");
592 code_edge.metadata = Some(serde_json::json!({"source": "extract"}));
593 graph.add_edge(code_edge);
594
595 let mut bridge = Edge::new("task-impl-auth", "file:src/auth.rs", "touches");
597 bridge.metadata = Some(serde_json::json!({"source": "auto-bridge", "confidence": 1.0}));
598 graph.add_edge(bridge);
599
600 let engine = QueryEngine::new(&graph);
602
603 let impacted = engine.impact("file:src/auth.rs");
605 let impacted_ids: Vec<&str> = impacted.iter().map(|n| n.id.as_str()).collect();
606 assert!(impacted_ids.contains(&"task-impl-auth"), "task should be impacted by code file change, got: {:?}", impacted_ids);
607
608 let deps = engine.deps("task-impl-auth", true);
615 let dep_ids: Vec<&str> = deps.iter().map(|n| n.id.as_str()).collect();
616 assert!(dep_ids.contains(&"feat-auth"), "feature should be a dep of task via implements, got: {:?}", dep_ids);
617 assert!(dep_ids.contains(&"file:src/auth.rs"), "code file should be a dep of task via touches, got: {:?}", dep_ids);
618
619 assert_eq!(graph.project_nodes().len(), 2);
621 assert_eq!(graph.code_nodes().len(), 2);
622 }
623
624 #[test]
627 fn test_perf_tasks_with_code_nodes() {
628 let mut project_graph = Graph::new();
630 for i in 0..50 {
631 let mut n = Node::new(&format!("task-{}", i), &format!("Task {}", i));
632 n.source = Some("project".to_string());
633 n.status = NodeStatus::Todo;
634 project_graph.add_node(n);
635 }
636 for i in 1..50 {
637 project_graph.add_edge(Edge::new(&format!("task-{}", i), &format!("task-{}", i - 1), "depends_on"));
638 }
639
640 let mut mixed_graph = project_graph.clone();
642 for i in 0..2000 {
643 let mut n = Node::new(&format!("fn:func_{}", i), &format!("func_{}", i));
644 n.source = Some("extract".to_string());
645 n.node_type = Some("code".to_string());
646 n.file_path = Some(format!("src/mod_{}.rs", i / 10));
647 n.status = NodeStatus::Done;
648 mixed_graph.add_node(n);
649 }
650 for i in 1..2000 {
651 let mut e = Edge::new(&format!("fn:func_{}", i), &format!("fn:func_{}", i - 1), "calls");
652 e.metadata = Some(serde_json::json!({"source": "extract"}));
653 mixed_graph.add_edge(e);
654 }
655
656 let iterations = 100;
658
659 let start = std::time::Instant::now();
660 for _ in 0..iterations {
661 let _ = project_graph.project_nodes();
662 }
663 let project_only_time = start.elapsed();
664
665 let start = std::time::Instant::now();
666 for _ in 0..iterations {
667 let _ = mixed_graph.project_nodes();
668 }
669 let mixed_time = start.elapsed();
670
671 let ratio = mixed_time.as_nanos() as f64 / project_only_time.as_nanos() as f64;
672 println!("project_nodes() — project_only: {:?}, mixed(+2000 code): {:?}, ratio: {:.2}x", project_only_time, mixed_time, ratio);
673
674 assert!(mixed_time.as_millis() < 100, "project_nodes() on 2050-node graph should be < 100ms for {} iters, got {:?}", iterations, mixed_time);
678
679 let start = std::time::Instant::now();
681 for _ in 0..iterations {
682 let _ = project_graph.summary();
683 }
684 let summary_project = start.elapsed();
685
686 let start = std::time::Instant::now();
687 for _ in 0..iterations {
688 let _ = mixed_graph.summary();
689 }
690 let summary_mixed = start.elapsed();
691
692 let summary_ratio = summary_mixed.as_nanos() as f64 / summary_project.as_nanos() as f64;
693 println!("summary() — project_only: {:?}, mixed: {:?}, ratio: {:.2}x", summary_project, summary_mixed, summary_ratio);
694
695 assert!(summary_mixed.as_millis() < 100, "summary() on 2050-node graph should be < 100ms for {} iters", iterations);
696 }
697
698 #[test]
699 fn test_graph_to_codegraph_roundtrip() {
700 use crate::code_graph::{CodeGraph, CodeNode, CodeEdge, NodeKind, EdgeRelation};
701
702 let mut cg = CodeGraph {
704 nodes: vec![
705 CodeNode {
706 id: "file:src/main.rs".to_string(),
707 kind: NodeKind::File,
708 name: "main.rs".to_string(),
709 file_path: "src/main.rs".to_string(),
710 line: None,
711 decorators: vec![],
712 signature: None,
713 docstring: None,
714 line_count: 100,
715 is_test: false,
716 visibility: None,
717 lang: Some("rust".to_string()),
718 body_hash: None,
719 end_line: None,
720 complexity: None,
721 },
722 CodeNode {
723 id: "fn:src/main.rs:main".to_string(),
724 kind: NodeKind::Function,
725 name: "main".to_string(),
726 file_path: "src/main.rs".to_string(),
727 line: Some(10),
728 decorators: vec!["#[tokio::main]".to_string()],
729 signature: Some("async fn main() -> Result<()>".to_string()),
730 docstring: Some("Entry point".to_string()),
731 line_count: 50,
732 is_test: false,
733 visibility: Some("pub".to_string()),
734 lang: Some("rust".to_string()),
735 body_hash: Some("abc123".to_string()),
736 end_line: Some(60),
737 complexity: None,
738 },
739 CodeNode {
740 id: "class:src/lib.rs:Config".to_string(),
741 kind: NodeKind::Class,
742 name: "Config".to_string(),
743 file_path: "src/lib.rs".to_string(),
744 line: Some(1),
745 decorators: vec![],
746 signature: None,
747 docstring: None,
748 line_count: 20,
749 is_test: true,
750 visibility: Some("pub(crate)".to_string()),
751 lang: Some("rust".to_string()),
752 body_hash: Some("def456".to_string()),
753 end_line: Some(20),
754 complexity: None,
755 },
756 ],
757 edges: vec![
758 CodeEdge {
759 from: "fn:src/main.rs:main".to_string(),
760 to: "file:src/main.rs".to_string(),
761 relation: EdgeRelation::DefinedIn,
762 weight: 0.5,
763 call_count: 1,
764 in_error_path: false,
765 confidence: 1.0,
766 call_site_line: None,
767 call_site_column: None,
768 },
769 CodeEdge {
770 from: "fn:src/main.rs:main".to_string(),
771 to: "class:src/lib.rs:Config".to_string(),
772 relation: EdgeRelation::Calls,
773 weight: 0.8,
774 call_count: 3,
775 in_error_path: true,
776 confidence: 0.9,
777 call_site_line: None,
778 call_site_column: None,
779 },
780 ],
781 outgoing: Default::default(),
782 incoming: Default::default(),
783 node_index: Default::default(),
784 };
785 cg.build_indexes();
786
787 let (graph_nodes, graph_edges) = codegraph_to_graph_nodes(&cg, std::path::Path::new("."));
789 let mut graph = Graph::new();
790 graph.nodes = graph_nodes;
791 graph.edges = graph_edges;
792
793 let roundtrip = graph_to_codegraph(&graph);
795
796 assert_eq!(roundtrip.nodes.len(), 3);
798 let file_node = roundtrip.nodes.iter().find(|n| n.id == "file:src/main.rs").unwrap();
799 assert_eq!(file_node.kind, NodeKind::File);
800 assert_eq!(file_node.name, "main.rs");
801 assert_eq!(file_node.line_count, 100);
802 assert!(!file_node.is_test);
803 assert_eq!(file_node.lang.as_deref(), Some("rust"));
804
805 let fn_node = roundtrip.nodes.iter().find(|n| n.id == "fn:src/main.rs:main").unwrap();
806 assert_eq!(fn_node.kind, NodeKind::Function);
807 assert_eq!(fn_node.signature.as_deref(), Some("async fn main() -> Result<()>"));
808 assert_eq!(fn_node.docstring.as_deref(), Some("Entry point"));
809 assert_eq!(fn_node.line, Some(10));
810 assert_eq!(fn_node.decorators, vec!["#[tokio::main]".to_string()]);
811 assert_eq!(fn_node.visibility.as_deref(), Some("pub"));
812 assert_eq!(fn_node.lang.as_deref(), Some("rust"));
813 assert_eq!(fn_node.body_hash.as_deref(), Some("abc123"));
814 assert_eq!(fn_node.end_line, Some(60));
815
816 let class_node = roundtrip.nodes.iter().find(|n| n.id == "class:src/lib.rs:Config").unwrap();
817 assert_eq!(class_node.kind, NodeKind::Class);
818 assert!(class_node.is_test);
819 assert_eq!(class_node.visibility.as_deref(), Some("pub(crate)"));
820 assert_eq!(class_node.body_hash.as_deref(), Some("def456"));
821
822 assert_eq!(roundtrip.edges.len(), 2);
824 let defined_edge = roundtrip.edges.iter().find(|e| e.relation == EdgeRelation::DefinedIn).unwrap();
825 assert_eq!(defined_edge.from, "fn:src/main.rs:main");
826 assert_eq!(defined_edge.to, "file:src/main.rs");
827
828 let calls_edge = roundtrip.edges.iter().find(|e| e.relation == EdgeRelation::Calls).unwrap();
829 assert_eq!(calls_edge.call_count, 3);
830 assert!(calls_edge.in_error_path);
831 assert!((calls_edge.weight - 0.8).abs() < 0.01);
832 assert!((calls_edge.confidence - 0.9).abs() < 0.01);
833
834 assert!(!roundtrip.node_index.is_empty());
836 assert!(!roundtrip.outgoing.is_empty());
837 }
838}