ricecoder_orchestration/analyzers/
impact_analyzer.rs1use crate::error::Result;
4use crate::models::{ImpactDetail, ImpactLevel, ImpactReport};
5use std::collections::{HashMap, HashSet, VecDeque};
6
7#[derive(Debug, Clone)]
9pub struct ProjectChange {
10 pub change_id: String,
12
13 pub project: String,
15
16 pub change_type: String,
18
19 pub description: String,
21
22 pub is_breaking: bool,
24}
25
26#[derive(Debug, Clone)]
28pub struct ImpactAnalyzer {
29 dependents_map: HashMap<String, Vec<String>>,
31
32 projects: HashSet<String>,
34}
35
36impl ImpactAnalyzer {
37 pub fn new() -> Self {
39 Self {
40 dependents_map: HashMap::new(),
41 projects: HashSet::new(),
42 }
43 }
44
45 pub fn add_project(&mut self, project_name: String) {
47 self.projects.insert(project_name.clone());
48 self.dependents_map.entry(project_name).or_default();
49 }
50
51 pub fn add_dependency(&mut self, from: String, to: String) {
53 self.projects.insert(from.clone());
55 self.projects.insert(to.clone());
56
57 let dependents = self.dependents_map.entry(to).or_default();
59
60 if !dependents.contains(&from) {
62 dependents.push(from);
63 }
64 }
65
66 pub fn analyze_impact(&self, change: &ProjectChange) -> Result<ImpactReport> {
68 let affected_projects = self.find_affected_projects(&change.project);
70
71 let impact_level = self.determine_impact_level(change);
73
74 let details = self.generate_impact_details(change, &affected_projects);
76
77 Ok(ImpactReport {
78 change_id: change.change_id.clone(),
79 affected_projects: affected_projects.clone(),
80 impact_level,
81 details,
82 })
83 }
84
85 fn find_affected_projects(&self, changed_project: &str) -> Vec<String> {
87 let mut affected = HashSet::new();
88 let mut visited = HashSet::new();
89 let mut queue = VecDeque::new();
90
91 queue.push_back(changed_project.to_string());
92
93 while let Some(current) = queue.pop_front() {
94 if visited.contains(¤t) {
95 continue;
96 }
97 visited.insert(current.clone());
98
99 if let Some(dependents) = self.dependents_map.get(¤t) {
101 for dependent in dependents {
102 if !visited.contains(dependent) {
103 affected.insert(dependent.clone());
104 queue.push_back(dependent.clone());
105 }
106 }
107 }
108 }
109
110 affected.into_iter().collect()
111 }
112
113 fn determine_impact_level(&self, change: &ProjectChange) -> ImpactLevel {
115 match (change.change_type.as_str(), change.is_breaking) {
116 ("api", true) => ImpactLevel::Critical,
118 ("dependency", true) => ImpactLevel::High,
120 ("config", true) => ImpactLevel::High,
122 (_, true) => ImpactLevel::High,
124 ("api", false) => ImpactLevel::Medium,
126 ("dependency", false) => ImpactLevel::Medium,
128 ("config", false) => ImpactLevel::Low,
130 _ => ImpactLevel::Low,
132 }
133 }
134
135 fn generate_impact_details(
137 &self,
138 change: &ProjectChange,
139 affected_projects: &[String],
140 ) -> Vec<ImpactDetail> {
141 affected_projects
142 .iter()
143 .map(|project| {
144 let reason = self.generate_impact_reason(change);
145 let required_actions = self.generate_required_actions(change);
146
147 ImpactDetail {
148 project: project.clone(),
149 reason,
150 required_actions,
151 }
152 })
153 .collect()
154 }
155
156 fn generate_impact_reason(&self, change: &ProjectChange) -> String {
158 if change.is_breaking {
159 format!(
160 "Breaking {} change in {}: {}",
161 change.change_type, change.project, change.description
162 )
163 } else {
164 format!(
165 "Non-breaking {} change in {}: {}",
166 change.change_type, change.project, change.description
167 )
168 }
169 }
170
171 fn generate_required_actions(&self, change: &ProjectChange) -> Vec<String> {
173 let mut actions = vec!["Review the change".to_string()];
174
175 match change.change_type.as_str() {
176 "api" => {
177 actions.push("Update API usage".to_string());
178 if change.is_breaking {
179 actions.push("Update imports and function calls".to_string());
180 }
181 }
182 "dependency" => {
183 actions.push("Update dependency version".to_string());
184 if change.is_breaking {
185 actions.push("Review breaking changes in dependency".to_string());
186 }
187 }
188 "config" => {
189 actions.push("Update configuration".to_string());
190 }
191 _ => {
192 actions.push("Verify compatibility".to_string());
193 }
194 }
195
196 actions.push("Run tests".to_string());
197
198 actions
199 }
200
201 pub fn analyze_multiple_impacts(
203 &self,
204 changes: &[ProjectChange],
205 ) -> Result<Vec<ImpactReport>> {
206 changes
207 .iter()
208 .map(|change| self.analyze_impact(change))
209 .collect()
210 }
211
212 pub fn get_affected_projects(&self, project: &str) -> Vec<String> {
214 self.find_affected_projects(project)
215 }
216
217 pub fn count_affected_projects(&self, project: &str) -> usize {
219 self.find_affected_projects(project).len()
220 }
221
222 pub fn is_affected(&self, changed_project: &str, target_project: &str) -> bool {
224 self.find_affected_projects(changed_project)
225 .contains(&target_project.to_string())
226 }
227
228 pub fn get_projects(&self) -> Vec<String> {
230 self.projects.iter().cloned().collect()
231 }
232
233 pub fn get_dependents_map(&self) -> &HashMap<String, Vec<String>> {
235 &self.dependents_map
236 }
237
238 pub fn clear(&mut self) {
240 self.dependents_map.clear();
241 self.projects.clear();
242 }
243}
244
245impl Default for ImpactAnalyzer {
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_create_analyzer() {
257 let analyzer = ImpactAnalyzer::new();
258 assert_eq!(analyzer.get_projects().len(), 0);
259 }
260
261 #[test]
262 fn test_add_project() {
263 let mut analyzer = ImpactAnalyzer::new();
264 analyzer.add_project("project-a".to_string());
265
266 assert_eq!(analyzer.get_projects().len(), 1);
267 assert!(analyzer.get_projects().contains(&"project-a".to_string()));
268 }
269
270 #[test]
271 fn test_add_dependency() {
272 let mut analyzer = ImpactAnalyzer::new();
273 analyzer.add_project("project-a".to_string());
274 analyzer.add_project("project-b".to_string());
275 analyzer.add_dependency("project-a".to_string(), "project-b".to_string());
276
277 let affected = analyzer.get_affected_projects("project-b");
278 assert_eq!(affected.len(), 1);
279 assert!(affected.contains(&"project-a".to_string()));
280 }
281
282 #[test]
283 fn test_analyze_breaking_api_change() {
284 let mut analyzer = ImpactAnalyzer::new();
285 analyzer.add_project("project-a".to_string());
286 analyzer.add_project("project-b".to_string());
287 analyzer.add_dependency("project-a".to_string(), "project-b".to_string());
288
289 let change = ProjectChange {
290 change_id: "change-1".to_string(),
291 project: "project-b".to_string(),
292 change_type: "api".to_string(),
293 description: "Removed deprecated function".to_string(),
294 is_breaking: true,
295 };
296
297 let report = analyzer.analyze_impact(&change).unwrap();
298
299 assert_eq!(report.change_id, "change-1");
300 assert_eq!(report.affected_projects.len(), 1);
301 assert_eq!(report.impact_level, ImpactLevel::Critical);
302 assert!(report.details[0].required_actions.len() > 0);
303 }
304
305 #[test]
306 fn test_analyze_non_breaking_api_change() {
307 let mut analyzer = ImpactAnalyzer::new();
308 analyzer.add_project("project-a".to_string());
309 analyzer.add_project("project-b".to_string());
310 analyzer.add_dependency("project-a".to_string(), "project-b".to_string());
311
312 let change = ProjectChange {
313 change_id: "change-1".to_string(),
314 project: "project-b".to_string(),
315 change_type: "api".to_string(),
316 description: "Added new function".to_string(),
317 is_breaking: false,
318 };
319
320 let report = analyzer.analyze_impact(&change).unwrap();
321
322 assert_eq!(report.affected_projects.len(), 1);
323 assert_eq!(report.impact_level, ImpactLevel::Medium);
324 }
325
326 #[test]
327 fn test_analyze_config_change() {
328 let mut analyzer = ImpactAnalyzer::new();
329 analyzer.add_project("project-a".to_string());
330 analyzer.add_project("project-b".to_string());
331 analyzer.add_dependency("project-a".to_string(), "project-b".to_string());
332
333 let change = ProjectChange {
334 change_id: "change-1".to_string(),
335 project: "project-b".to_string(),
336 change_type: "config".to_string(),
337 description: "Updated configuration".to_string(),
338 is_breaking: false,
339 };
340
341 let report = analyzer.analyze_impact(&change).unwrap();
342
343 assert_eq!(report.affected_projects.len(), 1);
344 assert_eq!(report.impact_level, ImpactLevel::Low);
345 }
346
347 #[test]
348 fn test_transitive_impact() {
349 let mut analyzer = ImpactAnalyzer::new();
350 analyzer.add_project("project-a".to_string());
351 analyzer.add_project("project-b".to_string());
352 analyzer.add_project("project-c".to_string());
353
354 analyzer.add_dependency("project-a".to_string(), "project-b".to_string());
356 analyzer.add_dependency("project-b".to_string(), "project-c".to_string());
357
358 let change = ProjectChange {
359 change_id: "change-1".to_string(),
360 project: "project-c".to_string(),
361 change_type: "api".to_string(),
362 description: "API change".to_string(),
363 is_breaking: true,
364 };
365
366 let report = analyzer.analyze_impact(&change).unwrap();
367
368 assert_eq!(report.affected_projects.len(), 2);
370 assert!(report.affected_projects.contains(&"project-a".to_string()));
371 assert!(report.affected_projects.contains(&"project-b".to_string()));
372 }
373
374 #[test]
375 fn test_no_affected_projects() {
376 let mut analyzer = ImpactAnalyzer::new();
377 analyzer.add_project("project-a".to_string());
378 analyzer.add_project("project-b".to_string());
379
380 let change = ProjectChange {
381 change_id: "change-1".to_string(),
382 project: "project-a".to_string(),
383 change_type: "api".to_string(),
384 description: "API change".to_string(),
385 is_breaking: true,
386 };
387
388 let report = analyzer.analyze_impact(&change).unwrap();
389
390 assert_eq!(report.affected_projects.len(), 0);
391 }
392
393 #[test]
394 fn test_is_affected() {
395 let mut analyzer = ImpactAnalyzer::new();
396 analyzer.add_project("project-a".to_string());
397 analyzer.add_project("project-b".to_string());
398 analyzer.add_dependency("project-a".to_string(), "project-b".to_string());
399
400 assert!(analyzer.is_affected("project-b", "project-a"));
401 assert!(!analyzer.is_affected("project-a", "project-b"));
402 }
403
404 #[test]
405 fn test_count_affected_projects() {
406 let mut analyzer = ImpactAnalyzer::new();
407 analyzer.add_project("project-a".to_string());
408 analyzer.add_project("project-b".to_string());
409 analyzer.add_project("project-c".to_string());
410
411 analyzer.add_dependency("project-a".to_string(), "project-c".to_string());
412 analyzer.add_dependency("project-b".to_string(), "project-c".to_string());
413
414 assert_eq!(analyzer.count_affected_projects("project-c"), 2);
415 }
416
417 #[test]
418 fn test_analyze_multiple_impacts() {
419 let mut analyzer = ImpactAnalyzer::new();
420 analyzer.add_project("project-a".to_string());
421 analyzer.add_project("project-b".to_string());
422 analyzer.add_project("project-c".to_string());
423
424 analyzer.add_dependency("project-a".to_string(), "project-b".to_string());
425 analyzer.add_dependency("project-b".to_string(), "project-c".to_string());
426
427 let changes = vec![
428 ProjectChange {
429 change_id: "change-1".to_string(),
430 project: "project-b".to_string(),
431 change_type: "api".to_string(),
432 description: "API change".to_string(),
433 is_breaking: true,
434 },
435 ProjectChange {
436 change_id: "change-2".to_string(),
437 project: "project-c".to_string(),
438 change_type: "config".to_string(),
439 description: "Config change".to_string(),
440 is_breaking: false,
441 },
442 ];
443
444 let reports = analyzer.analyze_multiple_impacts(&changes).unwrap();
445
446 assert_eq!(reports.len(), 2);
447 assert_eq!(reports[0].affected_projects.len(), 1);
449 assert_eq!(reports[1].affected_projects.len(), 2);
451 }
452
453 #[test]
454 fn test_clear_analyzer() {
455 let mut analyzer = ImpactAnalyzer::new();
456 analyzer.add_project("project-a".to_string());
457 analyzer.add_project("project-b".to_string());
458
459 assert_eq!(analyzer.get_projects().len(), 2);
460
461 analyzer.clear();
462
463 assert_eq!(analyzer.get_projects().len(), 0);
464 }
465
466 #[test]
467 fn test_impact_detail_generation() {
468 let mut analyzer = ImpactAnalyzer::new();
469 analyzer.add_project("project-a".to_string());
470 analyzer.add_project("project-b".to_string());
471 analyzer.add_dependency("project-a".to_string(), "project-b".to_string());
472
473 let change = ProjectChange {
474 change_id: "change-1".to_string(),
475 project: "project-b".to_string(),
476 change_type: "api".to_string(),
477 description: "Removed function".to_string(),
478 is_breaking: true,
479 };
480
481 let report = analyzer.analyze_impact(&change).unwrap();
482
483 assert_eq!(report.details.len(), 1);
484 assert_eq!(report.details[0].project, "project-a");
485 assert!(report.details[0].reason.contains("Breaking"));
486 assert!(report.details[0].required_actions.len() > 0);
487 }
488
489 #[test]
490 fn test_dependency_change_impact() {
491 let mut analyzer = ImpactAnalyzer::new();
492 analyzer.add_project("project-a".to_string());
493 analyzer.add_project("project-b".to_string());
494 analyzer.add_dependency("project-a".to_string(), "project-b".to_string());
495
496 let change = ProjectChange {
497 change_id: "change-1".to_string(),
498 project: "project-b".to_string(),
499 change_type: "dependency".to_string(),
500 description: "Updated dependency".to_string(),
501 is_breaking: true,
502 };
503
504 let report = analyzer.analyze_impact(&change).unwrap();
505
506 assert_eq!(report.impact_level, ImpactLevel::High);
507 }
508
509 #[test]
510 fn test_complex_dependency_graph() {
511 let mut analyzer = ImpactAnalyzer::new();
512
513 analyzer.add_project("project-a".to_string());
517 analyzer.add_project("project-b".to_string());
518 analyzer.add_project("project-c".to_string());
519 analyzer.add_project("project-d".to_string());
520
521 analyzer.add_dependency("project-a".to_string(), "project-b".to_string());
522 analyzer.add_dependency("project-a".to_string(), "project-c".to_string());
523 analyzer.add_dependency("project-b".to_string(), "project-d".to_string());
524 analyzer.add_dependency("project-c".to_string(), "project-d".to_string());
525
526 let change = ProjectChange {
527 change_id: "change-1".to_string(),
528 project: "project-d".to_string(),
529 change_type: "api".to_string(),
530 description: "API change".to_string(),
531 is_breaking: true,
532 };
533
534 let report = analyzer.analyze_impact(&change).unwrap();
535
536 assert_eq!(report.affected_projects.len(), 3);
538 assert!(report.affected_projects.contains(&"project-a".to_string()));
539 assert!(report.affected_projects.contains(&"project-b".to_string()));
540 assert!(report.affected_projects.contains(&"project-c".to_string()));
541 }
542}