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}