1use crate::code_graph::{CodeGraph, NodeKind, EdgeRelation};
8use std::collections::HashSet;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Complexity {
13 Simple,
14 Medium,
15 Complex,
16}
17
18impl std::fmt::Display for Complexity {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 match self {
21 Complexity::Simple => write!(f, "simple"),
22 Complexity::Medium => write!(f, "medium"),
23 Complexity::Complex => write!(f, "complex"),
24 }
25 }
26}
27
28#[derive(Debug, Clone)]
30pub struct ComplexityReport {
31 pub complexity: Complexity,
32 pub relevant_nodes: usize,
33 pub relevant_files: usize,
34 pub class_count: usize,
35 pub inheritance_edges: usize,
36 pub import_edges: usize,
37 pub test_count: usize,
38 pub summary: String,
39}
40
41pub fn assess_complexity_from_graph(
47 code_graph: &CodeGraph,
48 keywords: &[&str],
49 test_count: usize,
50) -> ComplexityReport {
51 let relevant = code_graph.find_relevant_nodes(keywords);
52 let relevant_count = relevant.len();
53
54 let relevant_files: HashSet<&str> = relevant.iter()
56 .map(|n| n.file_path.as_str())
57 .collect();
58 let file_count = relevant_files.len();
59
60 let class_count = relevant.iter()
62 .filter(|n| n.kind == NodeKind::Class)
63 .count();
64
65 let relevant_ids: HashSet<&str> = relevant.iter()
67 .map(|n| n.id.as_str())
68 .collect();
69 let inheritance_edges = code_graph.edges.iter()
70 .filter(|e| {
71 e.relation == EdgeRelation::Inherits
72 && (relevant_ids.contains(e.from.as_str()) || relevant_ids.contains(e.to.as_str()))
73 })
74 .count();
75
76 let import_edges = code_graph.edges.iter()
78 .filter(|e| {
79 e.relation == EdgeRelation::Imports
80 && (relevant_ids.contains(e.from.as_str()) || relevant_ids.contains(e.to.as_str()))
81 })
82 .count();
83
84 tracing::debug!(
85 "Graph complexity metrics: relevant_nodes={}, files={}, classes={}, inheritance={}, imports={}, tests={}",
86 relevant_count, file_count, class_count, inheritance_edges, import_edges, test_count
87 );
88
89 let complexity = if relevant_count <= 2 && file_count <= 1 && class_count == 0 && test_count <= 1 {
91 Complexity::Simple
92 } else if relevant_count >= 6 || file_count >= 3 || inheritance_edges >= 2 || import_edges >= 4 || test_count > 3 {
93 Complexity::Complex
94 } else {
95 Complexity::Medium
96 };
97
98 let summary = format!(
99 "Complexity: {:?} (nodes={}, files={}, classes={}, inherit={}, imports={}, tests={})",
100 complexity, relevant_count, file_count, class_count, inheritance_edges, import_edges, test_count
101 );
102
103 tracing::info!("{}", summary);
104
105 ComplexityReport {
106 complexity,
107 relevant_nodes: relevant_count,
108 relevant_files: file_count,
109 class_count,
110 inheritance_edges,
111 import_edges,
112 test_count,
113 summary,
114 }
115}
116
117pub fn assess_complexity(
119 code_graph: &CodeGraph,
120 problem_statement: &str,
121 test_count: usize,
122) -> ComplexityReport {
123 let keywords = CodeGraph::extract_keywords(problem_statement);
124 let keyword_refs: Vec<&str> = keywords.iter().map(|s| *s).collect();
125 assess_complexity_from_graph(code_graph, &keyword_refs, test_count)
126}
127
128pub fn is_high_risk_change(code_graph: &CodeGraph, node_ids: &[&str]) -> bool {
130 let total_callers: usize = node_ids.iter()
131 .map(|id| code_graph.get_callers(id).len())
132 .sum();
133
134 let max_callers = node_ids.iter()
136 .map(|id| code_graph.get_callers(id).len())
137 .max()
138 .unwrap_or(0);
139
140 total_callers > 10 || max_callers > 5
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum RiskLevel {
146 Low, Medium, High, Critical, }
151
152impl std::fmt::Display for RiskLevel {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 match self {
155 RiskLevel::Low => write!(f, "low"),
156 RiskLevel::Medium => write!(f, "medium"),
157 RiskLevel::High => write!(f, "high"),
158 RiskLevel::Critical => write!(f, "critical"),
159 }
160 }
161}
162
163pub fn assess_risk_level(code_graph: &CodeGraph, node_ids: &[&str]) -> RiskLevel {
164 let total_callers: usize = node_ids.iter()
165 .map(|id| code_graph.get_callers(id).len())
166 .sum();
167
168 match total_callers {
169 0..=5 => RiskLevel::Low,
170 6..=20 => RiskLevel::Medium,
171 21..=50 => RiskLevel::High,
172 _ => RiskLevel::Critical,
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::code_graph::{CodeGraph, CodeNode, CodeEdge};
180
181 #[test]
182 fn test_empty_graph_defaults_simple() {
183 let graph = CodeGraph::default();
184 let report = assess_complexity_from_graph(&graph, &["test"], 0);
185 assert_eq!(report.complexity, Complexity::Simple);
186 }
187
188 #[test]
189 fn test_complex_with_many_files() {
190 let mut graph = CodeGraph::default();
191
192 for i in 0..5 {
194 let file_path = format!("file{}.py", i);
195 graph.nodes.push(CodeNode {
196 id: format!("class:{}:TestClass{}", file_path, i),
197 kind: NodeKind::Class,
198 name: format!("TestClass{}", i),
199 file_path,
200 line: Some(1),
201 decorators: vec![],
202 signature: None,
203 docstring: None,
204 line_count: 10,
205 is_test: false,
206 });
207 }
208
209 graph.build_indexes();
210
211 let report = assess_complexity_from_graph(&graph, &["TestClass"], 0);
212 assert_eq!(report.complexity, Complexity::Complex);
213 assert!(report.relevant_files >= 3);
214 }
215
216 #[test]
217 fn test_risk_level() {
218 let mut graph = CodeGraph::default();
219
220 graph.nodes.push(CodeNode {
222 id: "func:core.py:hot_func".into(),
223 kind: NodeKind::Function,
224 name: "hot_func".into(),
225 file_path: "core.py".into(),
226 line: Some(10),
227 decorators: vec![],
228 signature: None,
229 docstring: None,
230 line_count: 20,
231 is_test: false,
232 });
233
234 for i in 0..30 {
236 let caller_id = format!("func:caller{}.py:caller_{}", i, i);
237 graph.nodes.push(CodeNode {
238 id: caller_id.clone(),
239 kind: NodeKind::Function,
240 name: format!("caller_{}", i),
241 file_path: format!("caller{}.py", i),
242 line: Some(1),
243 decorators: vec![],
244 signature: None,
245 docstring: None,
246 line_count: 5,
247 is_test: false,
248 });
249 graph.edges.push(CodeEdge::new(&caller_id, "func:core.py:hot_func", EdgeRelation::Calls));
250 }
251
252 graph.build_indexes();
253
254 let risk = assess_risk_level(&graph, &["func:core.py:hot_func"]);
255 assert_eq!(risk, RiskLevel::High);
256 }
257}