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 visibility: None,
207 lang: None,
208 body_hash: None,
209 end_line: None,
210 complexity: None,
211 });
212 }
213
214 graph.build_indexes();
215
216 let report = assess_complexity_from_graph(&graph, &["TestClass"], 0);
217 assert_eq!(report.complexity, Complexity::Complex);
218 assert!(report.relevant_files >= 3);
219 }
220
221 #[test]
222 fn test_risk_level() {
223 let mut graph = CodeGraph::default();
224
225 graph.nodes.push(CodeNode {
227 id: "func:core.py:hot_func".into(),
228 kind: NodeKind::Function,
229 name: "hot_func".into(),
230 file_path: "core.py".into(),
231 line: Some(10),
232 decorators: vec![],
233 signature: None,
234 docstring: None,
235 line_count: 20,
236 is_test: false,
237 visibility: None,
238 lang: None,
239 body_hash: None,
240 end_line: None,
241 complexity: None,
242 });
243
244 for i in 0..30 {
246 let caller_id = format!("func:caller{}.py:caller_{}", i, i);
247 graph.nodes.push(CodeNode {
248 id: caller_id.clone(),
249 kind: NodeKind::Function,
250 name: format!("caller_{}", i),
251 file_path: format!("caller{}.py", i),
252 line: Some(1),
253 decorators: vec![],
254 signature: None,
255 docstring: None,
256 line_count: 5,
257 is_test: false,
258 visibility: None,
259 lang: None,
260 body_hash: None,
261 end_line: None,
262 complexity: None,
263 });
264 graph.edges.push(CodeEdge::new(&caller_id, "func:core.py:hot_func", EdgeRelation::Calls));
265 }
266
267 graph.build_indexes();
268
269 let risk = assess_risk_level(&graph, &["func:core.py:hot_func"]);
270 assert_eq!(risk, RiskLevel::High);
271 }
272}