use std::collections::HashMap; use std::path::Path; use chrono::Utc; use compliance_core::error::CoreError; use compliance_core::models::graph::{ CodeEdge, CodeEdgeKind, CodeNode, GraphBuildRun, GraphBuildStatus, }; use compliance_core::traits::graph_builder::ParseOutput; use petgraph::graph::{DiGraph, NodeIndex}; use tracing::info; use crate::parsers::registry::ParserRegistry; use super::community::detect_communities; use super::impact::ImpactAnalyzer; /// The main graph engine that builds and manages code knowledge graphs pub struct GraphEngine { parser_registry: ParserRegistry, max_nodes: u32, } /// In-memory representation of a built code graph pub struct CodeGraph { pub graph: DiGraph, pub node_map: HashMap, pub nodes: Vec, pub edges: Vec, } impl GraphEngine { pub fn new(max_nodes: u32) -> Self { Self { parser_registry: ParserRegistry::new(), max_nodes, } } /// Build a code graph from a repository directory pub fn build_graph( &self, repo_path: &Path, repo_id: &str, graph_build_id: &str, ) -> Result<(CodeGraph, GraphBuildRun), CoreError> { let mut build_run = GraphBuildRun::new(repo_id.to_string()); info!(repo_id, path = %repo_path.display(), "Starting graph build"); // Phase 1: Parse all files let parse_output = self.parser_registry.parse_directory( repo_path, repo_id, graph_build_id, self.max_nodes, )?; // Phase 2: Build petgraph let code_graph = self.build_petgraph(parse_output)?; // Phase 3: Run community detection let community_count = detect_communities(&code_graph); // Collect language stats let mut languages: Vec = code_graph .nodes .iter() .map(|n| n.language.clone()) .collect::>() .into_iter() .collect(); languages.sort(); build_run.node_count = code_graph.nodes.len() as u32; build_run.edge_count = code_graph.edges.len() as u32; build_run.community_count = community_count; build_run.languages_parsed = languages; build_run.status = GraphBuildStatus::Completed; build_run.completed_at = Some(Utc::now()); info!( nodes = build_run.node_count, edges = build_run.edge_count, communities = build_run.community_count, "Graph build complete" ); Ok((code_graph, build_run)) } /// Build petgraph from parsed output, resolving edges to node indices fn build_petgraph(&self, parse_output: ParseOutput) -> Result { let mut graph = DiGraph::new(); let mut node_map: HashMap = HashMap::new(); let mut nodes = parse_output.nodes; // Add all nodes to the graph for node in &mut nodes { let idx = graph.add_node(node.qualified_name.clone()); node.graph_index = Some(idx.index() as u32); node_map.insert(node.qualified_name.clone(), idx); } // Resolve and add edges — rewrite target to the resolved qualified name // so the persisted edge references match node qualified_names. let mut resolved_edges = Vec::new(); for mut edge in parse_output.edges { let source_idx = node_map.get(&edge.source); let resolved = self.resolve_edge_target(&edge.target, &node_map); if let (Some(&src), Some(tgt)) = (source_idx, resolved) { // Update target to the resolved qualified name let resolved_name = node_map .iter() .find(|(_, &idx)| idx == tgt) .map(|(name, _)| name.clone()); if let Some(name) = resolved_name { edge.target = name; } graph.add_edge(src, tgt, edge.kind.clone()); resolved_edges.push(edge); } // Skip unresolved edges (cross-file, external deps) — conservative approach } Ok(CodeGraph { graph, node_map, nodes, edges: resolved_edges, }) } /// Try to resolve an edge target to a known node fn resolve_edge_target( &self, target: &str, node_map: &HashMap, ) -> Option { // Direct match if let Some(idx) = node_map.get(target) { return Some(*idx); } // Try matching just the function/type name (intra-file resolution) for (qualified, idx) in node_map { // Match "foo" to "path/file.rs::foo" or "path/file.rs::Type::foo" if qualified.ends_with(&format!("::{target}")) || qualified.ends_with(&format!(".{target}")) { return Some(*idx); } } // Try matching method calls like "self.method" -> look for "::method" if let Some(method_name) = target.strip_prefix("self.") { for (qualified, idx) in node_map { if qualified.ends_with(&format!("::{method_name}")) || qualified.ends_with(&format!(".{method_name}")) { return Some(*idx); } } } None } /// Get the impact analyzer for a built graph pub fn impact_analyzer(code_graph: &CodeGraph) -> ImpactAnalyzer<'_> { ImpactAnalyzer::new(code_graph) } }