Skip to main content

rta_core/analyzers/
risks.rs

1use crate::{
2    analyzers::Analyzer,
3    collector::RepositorySnapshot,
4    model::{
5        ArchitectureAssessment, CodeQuality, DependencyHealth, Overview, RiskFinding, RiskReport,
6        Severity, TestingMaturity,
7    },
8};
9
10pub struct RiskAnalyzer<'a> {
11    overview: &'a Overview,
12    dependencies: &'a DependencyHealth,
13    code_quality: &'a CodeQuality,
14    architecture: &'a ArchitectureAssessment,
15    testing: &'a TestingMaturity,
16}
17
18impl<'a> RiskAnalyzer<'a> {
19    pub fn new(
20        overview: &'a Overview,
21        dependencies: &'a DependencyHealth,
22        code_quality: &'a CodeQuality,
23        architecture: &'a ArchitectureAssessment,
24        testing: &'a TestingMaturity,
25    ) -> Self {
26        Self {
27            overview,
28            dependencies,
29            code_quality,
30            architecture,
31            testing,
32        }
33    }
34}
35
36impl Analyzer<RiskReport> for RiskAnalyzer<'_> {
37    fn analyze(&self, _snapshot: &RepositorySnapshot) -> RiskReport {
38        let mut findings = Vec::new();
39
40        if self.overview.package_count <= 1 && self.code_quality.lines_of_code > 8_000 {
41            findings.push(RiskFinding {
42                severity: Severity::Medium,
43                title: "Bus factor and ownership concentration".into(),
44                evidence: "Large codebase concentrated in a single package.".into(),
45                recommendation:
46                    "Review ownership boundaries and split high-change domains into explicit crates."
47                        .into(),
48            });
49        }
50        if !self.code_quality.god_module_candidates.is_empty() {
51            findings.push(RiskFinding {
52                severity: Severity::High,
53                title: "Potential God modules".into(),
54                evidence: self.code_quality.god_module_candidates.join(", "),
55                recommendation:
56                    "Extract cohesive submodules and isolate orchestration from domain behavior."
57                        .into(),
58            });
59        }
60        if !self.dependencies.maintenance_risks.is_empty() {
61            findings.push(RiskFinding {
62                severity: Severity::Medium,
63                title: "Dependency maintenance risk".into(),
64                evidence: self.dependencies.maintenance_risks.join("; "),
65                recommendation:
66                    "Review dependency sourcing, version policy, and upgrade ownership.".into(),
67            });
68        }
69        if !self.testing.has_tests {
70            findings.push(RiskFinding {
71                severity: Severity::High,
72                title: "Lack of automated tests".into(),
73                evidence: "No unit or integration tests were detected.".into(),
74                recommendation:
75                    "Introduce smoke, integration, and critical-path unit tests before major investment."
76                        .into(),
77            });
78        }
79        if self.dependencies.total_dependencies > 60 {
80            findings.push(RiskFinding {
81                severity: Severity::Medium,
82                title: "Excessive dependency concentration".into(),
83                evidence: format!(
84                    "{} direct dependencies were detected.",
85                    self.dependencies.total_dependencies
86                ),
87                recommendation:
88                    "Identify strategic dependencies and remove low-value transitive surface area."
89                        .into(),
90            });
91        }
92        if !self.architecture.module_centralization_risks.is_empty() {
93            findings.push(RiskFinding {
94                severity: Severity::Medium,
95                title: "Module centralization risk".into(),
96                evidence: self.architecture.module_centralization_risks.join("; "),
97                recommendation:
98                    "Review module directionality and enforce dependency rules at crate boundaries."
99                        .into(),
100            });
101        }
102        if !self.architecture.circular_dependencies.is_empty() {
103            findings.push(RiskFinding {
104                severity: Severity::Medium,
105                title: "Circular module dependency".into(),
106                evidence: self.architecture.circular_dependencies.join("; "),
107                recommendation:
108                    "Break top-level module cycles or move shared contracts behind an explicit boundary."
109                        .into(),
110            });
111        }
112
113        let mut score = 100_i32;
114        for finding in &findings {
115            score -= match finding.severity {
116                Severity::Low => 4,
117                Severity::Medium => 10,
118                Severity::High => 18,
119            };
120        }
121
122        RiskReport {
123            findings,
124            score: score.clamp(0, 100) as u8,
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use std::path::PathBuf;
132
133    use crate::{
134        analyzers::{risks::RiskAnalyzer, Analyzer},
135        collector::{CargoManifest, RepositorySnapshot},
136        model::{ArchitectureAssessment, CodeQuality, DependencyHealth, Overview, TestingMaturity},
137    };
138
139    #[test]
140    fn empty_low_risk_inputs_generate_no_findings() {
141        let overview = overview(2, 1);
142        let dependencies = dependencies(4, Vec::new());
143        let code_quality = code_quality(1_000, Vec::new());
144        let architecture = architecture(Vec::new(), Vec::new());
145        let testing = testing(true);
146
147        let report = RiskAnalyzer::new(
148            &overview,
149            &dependencies,
150            &code_quality,
151            &architecture,
152            &testing,
153        )
154        .analyze(&snapshot());
155
156        assert!(report.findings.is_empty());
157        assert_eq!(report.score, 100);
158    }
159
160    #[test]
161    fn typical_dependency_and_testing_gaps_generate_findings() {
162        let overview = overview(1, 1);
163        let dependencies = dependencies(4, vec!["path dependency".into()]);
164        let code_quality = code_quality(1_000, Vec::new());
165        let architecture = architecture(Vec::new(), Vec::new());
166        let testing = testing(false);
167
168        let report = RiskAnalyzer::new(
169            &overview,
170            &dependencies,
171            &code_quality,
172            &architecture,
173            &testing,
174        )
175        .analyze(&snapshot());
176
177        assert_eq!(report.findings.len(), 2);
178        assert!(report.score < 80);
179    }
180
181    #[test]
182    fn extreme_single_package_and_god_modules_generate_high_findings() {
183        let overview = overview(1, 1);
184        let dependencies = dependencies(70, Vec::new());
185        let code_quality = code_quality(9_000, vec!["src/large.rs".into()]);
186        let architecture = architecture(Vec::new(), Vec::new());
187        let testing = testing(true);
188
189        let report = RiskAnalyzer::new(
190            &overview,
191            &dependencies,
192            &code_quality,
193            &architecture,
194            &testing,
195        )
196        .analyze(&snapshot());
197
198        assert!(report
199            .findings
200            .iter()
201            .any(|finding| finding.title == "Potential God modules"));
202        assert!(report
203            .findings
204            .iter()
205            .any(|finding| finding.title == "Excessive dependency concentration"));
206    }
207
208    #[test]
209    fn adversarial_architecture_names_are_not_conflated() {
210        let overview = overview(2, 1);
211        let dependencies = dependencies(4, Vec::new());
212        let code_quality = code_quality(1_000, Vec::new());
213        let architecture = architecture(
214            vec!["src/lib.rs centralizes 25 module declarations".into()],
215            vec!["cycle among top-level modules: a -> b".into()],
216        );
217        let testing = testing(true);
218
219        let report = RiskAnalyzer::new(
220            &overview,
221            &dependencies,
222            &code_quality,
223            &architecture,
224            &testing,
225        )
226        .analyze(&snapshot());
227
228        assert!(report
229            .findings
230            .iter()
231            .any(|finding| finding.title == "Module centralization risk"));
232        assert!(report
233            .findings
234            .iter()
235            .any(|finding| finding.title == "Circular module dependency"));
236    }
237
238    fn overview(package_count: usize, crate_count: usize) -> Overview {
239        Overview {
240            crate_count,
241            package_count,
242            workspace_members: Vec::new(),
243            total_files: 0,
244            total_bytes: 0,
245            languages: Vec::new(),
246            cargo_configs: Vec::new(),
247            summary: String::new(),
248        }
249    }
250
251    fn dependencies(total_dependencies: usize, maintenance_risks: Vec<String>) -> DependencyHealth {
252        DependencyHealth {
253            total_dependencies,
254            direct_dependencies: total_dependencies,
255            critical_dependencies: Vec::new(),
256            outdated_indicators: Vec::new(),
257            maintenance_risks,
258            score: 100,
259        }
260    }
261
262    fn code_quality(lines_of_code: usize, god_module_candidates: Vec<String>) -> CodeQuality {
263        CodeQuality {
264            lines_of_code,
265            module_count: 1,
266            function_count: 0,
267            average_function_size: 0.0,
268            complexity_indicators: Vec::new(),
269            large_modules: Vec::new(),
270            god_module_candidates,
271            score: 100,
272        }
273    }
274
275    fn architecture(
276        module_centralization_risks: Vec<String>,
277        circular_dependencies: Vec<String>,
278    ) -> ArchitectureAssessment {
279        ArchitectureAssessment {
280            detected_layers: Vec::new(),
281            domain_boundaries: Vec::new(),
282            module_centralization_risks,
283            circular_dependencies,
284            architecture_style: String::new(),
285            separation_of_concerns: String::new(),
286            score: 100,
287        }
288    }
289
290    fn testing(has_tests: bool) -> TestingMaturity {
291        TestingMaturity {
292            has_tests,
293            unit_test_files: usize::from(has_tests),
294            integration_test_files: 0,
295            test_function_count: usize::from(has_tests),
296            testing_structure: String::new(),
297            score: 100,
298        }
299    }
300
301    fn snapshot() -> RepositorySnapshot {
302        RepositorySnapshot {
303            root: PathBuf::from("/tmp/repo"),
304            files: Vec::new(),
305            manifests: Vec::<CargoManifest>::new(),
306        }
307    }
308}