1use std::collections::{HashMap, HashSet};
8
9use serde::{Deserialize, Serialize};
10
11use crate::graph::traversal::{self, Direction, TraversalOptions};
12use crate::graph::CodeGraph;
13use crate::types::{CodeUnitType, EdgeType};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ProposedChange {
20 pub target: u64,
22 pub change_type: ChangeType,
24 pub description: String,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30pub enum ChangeType {
31 Signature,
33 Behavior,
35 Deletion,
37 Rename,
39 Move,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum ImpactType {
46 WillBreak,
48 MightBreak,
50 NeedsReview,
52 Safe,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ImpactedNode {
59 pub node_id: u64,
61 pub impact_path: Vec<u64>,
63 pub distance: u32,
65 pub impact_type: ImpactType,
67 pub break_probability: f64,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct BlastRadius {
74 pub files_affected: usize,
76 pub functions_affected: usize,
78 pub modules_affected: usize,
80 pub loc_affected: usize,
82 pub tests_affected: usize,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub enum RiskLevel {
89 Low,
91 Medium,
93 High,
95 Critical,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct Mitigation {
102 pub description: String,
104 pub effort: String,
106 pub risk_reduction: f64,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct EnhancedImpactResult {
113 pub change: ProposedChange,
115 pub direct_impact: Vec<ImpactedNode>,
117 pub transitive_impact: Vec<ImpactedNode>,
119 pub risk_level: RiskLevel,
121 pub blast_radius: BlastRadius,
123 pub mitigations: Vec<Mitigation>,
125}
126
127pub struct ImpactAnalyzer<'g> {
131 graph: &'g CodeGraph,
132}
133
134impl<'g> ImpactAnalyzer<'g> {
135 pub fn new(graph: &'g CodeGraph) -> Self {
136 Self { graph }
137 }
138
139 pub fn analyze(&self, change: ProposedChange, max_depth: u32) -> EnhancedImpactResult {
141 let dependency_edges = vec![
142 EdgeType::Calls,
143 EdgeType::Imports,
144 EdgeType::Inherits,
145 EdgeType::Implements,
146 EdgeType::UsesType,
147 EdgeType::References,
148 EdgeType::Returns,
149 EdgeType::ParamType,
150 EdgeType::Overrides,
151 ];
152
153 let options = TraversalOptions {
155 max_depth: max_depth as i32,
156 edge_types: dependency_edges,
157 direction: Direction::Backward,
158 };
159
160 let traversal = traversal::bfs(self.graph, change.target, &options);
161
162 let mut parent_map: HashMap<u64, u64> = HashMap::new();
164 let mut visited_order: Vec<(u64, u32)> = Vec::new();
165
166 {
168 let opts = TraversalOptions {
169 max_depth: max_depth as i32,
170 edge_types: vec![
171 EdgeType::Calls,
172 EdgeType::Imports,
173 EdgeType::Inherits,
174 EdgeType::Implements,
175 EdgeType::UsesType,
176 EdgeType::References,
177 ],
178 direction: Direction::Backward,
179 };
180 let mut visited = HashSet::new();
181 let mut queue = std::collections::VecDeque::new();
182 visited.insert(change.target);
183 queue.push_back((change.target, 0u32));
184
185 while let Some((current, depth)) = queue.pop_front() {
186 visited_order.push((current, depth));
187 if opts.max_depth >= 0 && depth >= opts.max_depth as u32 {
188 continue;
189 }
190 for edge in self.graph.edges_to(current) {
191 if !opts.edge_types.is_empty() && !opts.edge_types.contains(&edge.edge_type) {
192 continue;
193 }
194 if visited.insert(edge.source_id) {
195 parent_map.insert(edge.source_id, current);
196 queue.push_back((edge.source_id, depth + 1));
197 }
198 }
199 }
200 }
201
202 let mut direct_impact = Vec::new();
203 let mut transitive_impact = Vec::new();
204
205 for &(node_id, depth) in &traversal {
206 if node_id == change.target {
207 continue;
208 }
209
210 let impact_path = self.reconstruct_path(node_id, change.target, &parent_map);
211 let impact_type = self.classify_impact(&change, node_id, depth);
212 let break_probability = self.compute_break_probability(&change, node_id, depth);
213
214 let impacted = ImpactedNode {
215 node_id,
216 impact_path,
217 distance: depth,
218 impact_type,
219 break_probability,
220 };
221
222 if depth == 1 {
223 direct_impact.push(impacted);
224 } else {
225 transitive_impact.push(impacted);
226 }
227 }
228
229 let blast_radius = self.compute_blast_radius(&direct_impact, &transitive_impact);
230 let risk_level = self.assess_risk(&blast_radius, &direct_impact, &transitive_impact);
231 let mitigations = self.generate_mitigations(&change, &risk_level, &blast_radius);
232
233 EnhancedImpactResult {
234 change,
235 direct_impact,
236 transitive_impact,
237 risk_level,
238 blast_radius,
239 mitigations,
240 }
241 }
242
243 pub fn impact_path(&self, from: u64, to: u64) -> Option<Vec<u64>> {
245 traversal::shortest_path(self.graph, from, to, &[])
246 }
247
248 pub fn visualize(&self, result: &EnhancedImpactResult) -> serde_json::Value {
250 let mut nodes = Vec::new();
251 let mut edges = Vec::new();
252
253 if let Some(unit) = self.graph.get_unit(result.change.target) {
255 nodes.push(serde_json::json!({
256 "id": result.change.target,
257 "name": unit.name,
258 "type": "change_target",
259 "risk": "source",
260 }));
261 }
262
263 for impacted in result
265 .direct_impact
266 .iter()
267 .chain(result.transitive_impact.iter())
268 {
269 if let Some(unit) = self.graph.get_unit(impacted.node_id) {
270 nodes.push(serde_json::json!({
271 "id": impacted.node_id,
272 "name": unit.name,
273 "type": format!("{:?}", impacted.impact_type),
274 "distance": impacted.distance,
275 "break_probability": impacted.break_probability,
276 }));
277
278 if impacted.impact_path.len() >= 2 {
279 let from = impacted.impact_path[impacted.impact_path.len() - 2];
280 edges.push(serde_json::json!({
281 "from": from,
282 "to": impacted.node_id,
283 }));
284 }
285 }
286 }
287
288 serde_json::json!({
289 "nodes": nodes,
290 "edges": edges,
291 "blast_radius": {
292 "files": result.blast_radius.files_affected,
293 "functions": result.blast_radius.functions_affected,
294 "modules": result.blast_radius.modules_affected,
295 "tests": result.blast_radius.tests_affected,
296 },
297 "risk_level": format!("{:?}", result.risk_level),
298 })
299 }
300
301 fn reconstruct_path(&self, from: u64, to: u64, parent_map: &HashMap<u64, u64>) -> Vec<u64> {
304 let mut path = vec![from];
305 let mut current = from;
306 let mut seen = HashSet::new();
307 seen.insert(current);
308 while let Some(&parent) = parent_map.get(¤t) {
309 if !seen.insert(parent) {
310 break;
311 }
312 path.push(parent);
313 if parent == to {
314 break;
315 }
316 current = parent;
317 }
318 path
319 }
320
321 fn classify_impact(&self, change: &ProposedChange, node_id: u64, distance: u32) -> ImpactType {
322 let has_test = self
323 .graph
324 .edges_to(node_id)
325 .iter()
326 .any(|e| e.edge_type == EdgeType::Tests);
327
328 match change.change_type {
329 ChangeType::Deletion => {
330 if distance == 1 {
331 ImpactType::WillBreak
332 } else {
333 ImpactType::MightBreak
334 }
335 }
336 ChangeType::Signature => {
337 if distance == 1 {
338 ImpactType::WillBreak
339 } else {
340 ImpactType::NeedsReview
341 }
342 }
343 ChangeType::Rename => {
344 if distance == 1 {
345 ImpactType::WillBreak
346 } else {
347 ImpactType::NeedsReview
348 }
349 }
350 ChangeType::Behavior => {
351 if has_test {
352 ImpactType::NeedsReview
353 } else {
354 ImpactType::MightBreak
355 }
356 }
357 ChangeType::Move => {
358 if distance == 1 {
359 ImpactType::MightBreak
360 } else {
361 ImpactType::Safe
362 }
363 }
364 }
365 }
366
367 fn compute_break_probability(
368 &self,
369 change: &ProposedChange,
370 _node_id: u64,
371 distance: u32,
372 ) -> f64 {
373 let base = match change.change_type {
374 ChangeType::Deletion => 0.95,
375 ChangeType::Signature => 0.85,
376 ChangeType::Rename => 0.80,
377 ChangeType::Behavior => 0.50,
378 ChangeType::Move => 0.40,
379 };
380
381 let decay = 1.0 / (1.0 + distance as f64 * 0.5);
383 (base * decay).min(1.0)
384 }
385
386 fn compute_blast_radius(
387 &self,
388 direct: &[ImpactedNode],
389 transitive: &[ImpactedNode],
390 ) -> BlastRadius {
391 let all_nodes: Vec<u64> = direct
392 .iter()
393 .chain(transitive.iter())
394 .map(|n| n.node_id)
395 .collect();
396
397 let mut files = HashSet::new();
398 let mut modules = HashSet::new();
399 let mut functions = 0usize;
400 let mut loc = 0usize;
401 let mut tests = 0usize;
402
403 for &node_id in &all_nodes {
404 if let Some(unit) = self.graph.get_unit(node_id) {
405 files.insert(unit.file_path.display().to_string());
406
407 if let Some(last_dot) = unit.qualified_name.rfind('.') {
409 modules.insert(unit.qualified_name[..last_dot].to_string());
410 } else if let Some(last_sep) = unit.qualified_name.rfind("::") {
411 modules.insert(unit.qualified_name[..last_sep].to_string());
412 }
413
414 if unit.unit_type == CodeUnitType::Function {
415 functions += 1;
416 }
417 if unit.unit_type == CodeUnitType::Test {
418 tests += 1;
419 }
420
421 let lines = if unit.span.end_line > unit.span.start_line {
422 (unit.span.end_line - unit.span.start_line) as usize
423 } else {
424 1
425 };
426 loc += lines;
427 }
428 }
429
430 BlastRadius {
431 files_affected: files.len(),
432 functions_affected: functions,
433 modules_affected: modules.len(),
434 loc_affected: loc,
435 tests_affected: tests,
436 }
437 }
438
439 fn assess_risk(
440 &self,
441 blast_radius: &BlastRadius,
442 direct: &[ImpactedNode],
443 transitive: &[ImpactedNode],
444 ) -> RiskLevel {
445 let total = direct.len() + transitive.len();
446 let will_break = direct
447 .iter()
448 .chain(transitive.iter())
449 .filter(|n| n.impact_type == ImpactType::WillBreak)
450 .count();
451
452 if total == 0 {
453 return RiskLevel::Low;
454 }
455
456 if will_break > 10 || blast_radius.files_affected > 20 {
457 RiskLevel::Critical
458 } else if will_break > 3 || blast_radius.files_affected > 10 || total > 30 {
459 RiskLevel::High
460 } else if will_break > 0 || total > 10 {
461 RiskLevel::Medium
462 } else {
463 RiskLevel::Low
464 }
465 }
466
467 fn generate_mitigations(
468 &self,
469 change: &ProposedChange,
470 risk_level: &RiskLevel,
471 blast_radius: &BlastRadius,
472 ) -> Vec<Mitigation> {
473 let mut mitigations = Vec::new();
474
475 match change.change_type {
476 ChangeType::Signature => {
477 mitigations.push(Mitigation {
478 description: "Add compatibility wrapper that delegates to new signature"
479 .to_string(),
480 effort: "Low".to_string(),
481 risk_reduction: 0.7,
482 });
483 mitigations.push(Mitigation {
484 description: "Deprecate old signature with migration period".to_string(),
485 effort: "Medium".to_string(),
486 risk_reduction: 0.9,
487 });
488 }
489 ChangeType::Deletion => {
490 mitigations.push(Mitigation {
491 description: "Replace with deprecation warning first".to_string(),
492 effort: "Low".to_string(),
493 risk_reduction: 0.5,
494 });
495 }
496 ChangeType::Rename => {
497 mitigations.push(Mitigation {
498 description: "Add type alias or re-export from old name".to_string(),
499 effort: "Low".to_string(),
500 risk_reduction: 0.8,
501 });
502 }
503 _ => {}
504 }
505
506 if *risk_level == RiskLevel::High || *risk_level == RiskLevel::Critical {
507 mitigations.push(Mitigation {
508 description: "Deploy incrementally with feature flags".to_string(),
509 effort: "Medium".to_string(),
510 risk_reduction: 0.6,
511 });
512 }
513
514 if blast_radius.tests_affected == 0 {
515 mitigations.push(Mitigation {
516 description: "Add tests before making the change".to_string(),
517 effort: "Medium".to_string(),
518 risk_reduction: 0.4,
519 });
520 }
521
522 mitigations
523 }
524}
525
526#[cfg(test)]
529mod tests {
530 use super::*;
531 use crate::types::{CodeUnit, CodeUnitType, Edge, Language, Span};
532 use std::path::PathBuf;
533
534 fn test_graph() -> CodeGraph {
535 let mut graph = CodeGraph::with_default_dimension();
536
537 let a = graph.add_unit(CodeUnit::new(
539 CodeUnitType::Function,
540 Language::Rust,
541 "func_a".to_string(),
542 "mod::func_a".to_string(),
543 PathBuf::from("src/a.rs"),
544 Span::new(1, 0, 10, 0),
545 ));
546 let b = graph.add_unit(CodeUnit::new(
547 CodeUnitType::Function,
548 Language::Rust,
549 "func_b".to_string(),
550 "mod::func_b".to_string(),
551 PathBuf::from("src/b.rs"),
552 Span::new(1, 0, 20, 0),
553 ));
554 let c = graph.add_unit(CodeUnit::new(
555 CodeUnitType::Function,
556 Language::Rust,
557 "func_c".to_string(),
558 "mod::func_c".to_string(),
559 PathBuf::from("src/c.rs"),
560 Span::new(1, 0, 15, 0),
561 ));
562
563 let _ = graph.add_edge(Edge::new(b, a, EdgeType::Calls));
565 let _ = graph.add_edge(Edge::new(c, b, EdgeType::Calls));
566
567 graph
568 }
569
570 #[test]
571 fn analyze_deletion_impact() {
572 let graph = test_graph();
573 let analyzer = ImpactAnalyzer::new(&graph);
574 let change = ProposedChange {
575 target: 0, change_type: ChangeType::Deletion,
577 description: "Delete func_a".to_string(),
578 };
579 let result = analyzer.analyze(change, 5);
580 assert!(!result.direct_impact.is_empty());
581 assert_eq!(result.direct_impact[0].impact_type, ImpactType::WillBreak);
582 }
583
584 #[test]
585 fn blast_radius_computed() {
586 let graph = test_graph();
587 let analyzer = ImpactAnalyzer::new(&graph);
588 let change = ProposedChange {
589 target: 0,
590 change_type: ChangeType::Signature,
591 description: "Change signature".to_string(),
592 };
593 let result = analyzer.analyze(change, 5);
594 assert!(result.blast_radius.files_affected > 0);
595 }
596
597 #[test]
598 fn mitigations_generated() {
599 let graph = test_graph();
600 let analyzer = ImpactAnalyzer::new(&graph);
601 let change = ProposedChange {
602 target: 0,
603 change_type: ChangeType::Signature,
604 description: "Change params".to_string(),
605 };
606 let result = analyzer.analyze(change, 5);
607 assert!(!result.mitigations.is_empty());
608 }
609
610 #[test]
611 fn visualize_produces_json() {
612 let graph = test_graph();
613 let analyzer = ImpactAnalyzer::new(&graph);
614 let change = ProposedChange {
615 target: 0,
616 change_type: ChangeType::Behavior,
617 description: "Change behavior".to_string(),
618 };
619 let result = analyzer.analyze(change, 3);
620 let viz = analyzer.visualize(&result);
621 assert!(viz.get("nodes").is_some());
622 assert!(viz.get("edges").is_some());
623 }
624}