1use std::path::{Path, PathBuf};
2
3use walkdir::WalkDir;
4
5use crate::metrics::ArchitectureScore;
6use crate::pipeline::FullAnalysis;
7use crate::types::*;
8
9pub struct ForensicsAnalysis {
11 pub module_name: String,
12 pub module_path: PathBuf,
13 pub directory_tree: Vec<DirEntry>,
14 pub aggregates: Vec<AggregateAnalysis>,
15 pub domain_events: Vec<Component>,
16 pub ports: Vec<Component>,
17 pub application_services: Vec<Component>,
18 pub infrastructure_adapters: Vec<AdapterMapping>,
19 pub violations: Vec<Violation>,
20 pub score: ArchitectureScore,
21 pub classified_imports: Vec<ClassifiedImport>,
22 pub improvements: Vec<String>,
23}
24
25pub struct DirEntry {
27 pub rel_path: String,
28 pub is_dir: bool,
29 pub depth: usize,
30}
31
32pub struct AggregateAnalysis {
34 pub component: Component,
35 pub value_objects: Vec<Component>,
36 pub dependency_audit: DependencyAudit,
37 pub ddd_patterns: Vec<DddPattern>,
38}
39
40pub struct DddPattern {
42 pub name: String,
43 pub detected: bool,
44}
45
46pub struct DependencyAudit {
48 pub stdlib_imports: Vec<String>,
49 pub internal_domain_imports: Vec<String>,
50 pub external_imports: Vec<String>,
51 pub infrastructure_leaks: Vec<String>,
52 pub is_clean: bool,
53}
54
55pub struct AdapterMapping {
57 pub adapter: Component,
58 pub implements_ports: Vec<String>,
59}
60
61pub struct ClassifiedImport {
63 pub import_path: String,
64 pub category: ImportCategory,
65 pub source_file: PathBuf,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum ImportCategory {
71 Stdlib,
72 InternalDomain,
73 InternalApplication,
74 InternalInfrastructure,
75 External,
76}
77
78pub fn build_forensics(
80 full_analysis: &FullAnalysis,
81 module_path: &Path,
82 _project_root: &Path,
83) -> ForensicsAnalysis {
84 let module_name = module_path
85 .file_name()
86 .map(|n| n.to_string_lossy().to_string())
87 .unwrap_or_else(|| "unknown".to_string());
88
89 let directory_tree = build_directory_tree(module_path);
91
92 let classified_imports = classify_all_imports(&full_analysis.dependencies);
94
95 let mut domain_events = Vec::new();
97 let mut ports = Vec::new();
98 let mut entities = Vec::new();
99 let mut value_objects = Vec::new();
100 let mut application_services = Vec::new();
101 let mut infrastructure_adapters = Vec::new();
102
103 for comp in &full_analysis.components {
104 match &comp.kind {
105 ComponentKind::DomainEvent(_) => domain_events.push(comp.clone()),
106 ComponentKind::Port(_) => ports.push(comp.clone()),
107 ComponentKind::Entity(_) => entities.push(comp.clone()),
108 ComponentKind::ValueObject => value_objects.push(comp.clone()),
109 ComponentKind::Service if comp.layer == Some(ArchLayer::Application) => {
110 application_services.push(comp.clone());
111 }
112 ComponentKind::UseCase => application_services.push(comp.clone()),
113 ComponentKind::Adapter(info) => {
114 infrastructure_adapters.push(AdapterMapping {
115 adapter: comp.clone(),
116 implements_ports: info.implements.clone(),
117 });
118 }
119 ComponentKind::Repository if comp.layer == Some(ArchLayer::Infrastructure) => {
120 infrastructure_adapters.push(AdapterMapping {
121 adapter: comp.clone(),
122 implements_ports: Vec::new(),
123 });
124 }
125 _ => {}
126 }
127 }
128
129 let aggregates = build_aggregates(&entities, &value_objects, &classified_imports);
131
132 let improvements = generate_improvements(
134 &entities,
135 &domain_events,
136 &infrastructure_adapters,
137 &ports,
138 &full_analysis.result.violations,
139 );
140
141 ForensicsAnalysis {
142 module_name,
143 module_path: module_path.to_path_buf(),
144 directory_tree,
145 aggregates,
146 domain_events,
147 ports,
148 application_services,
149 infrastructure_adapters,
150 violations: full_analysis.result.violations.clone(),
151 score: full_analysis.result.score.clone(),
152 classified_imports,
153 improvements,
154 }
155}
156
157fn build_directory_tree(module_path: &Path) -> Vec<DirEntry> {
158 let mut entries = Vec::new();
159
160 for entry in WalkDir::new(module_path)
161 .sort_by_file_name()
162 .into_iter()
163 .filter_map(|e| e.ok())
164 {
165 let rel_path = entry
166 .path()
167 .strip_prefix(module_path)
168 .unwrap_or(entry.path())
169 .to_string_lossy()
170 .to_string();
171
172 if rel_path.is_empty() {
173 continue;
174 }
175
176 entries.push(DirEntry {
177 rel_path,
178 is_dir: entry.path().is_dir(),
179 depth: entry.depth(),
180 });
181 }
182
183 entries
184}
185
186fn classify_import(import_path: &str) -> ImportCategory {
187 if !import_path.contains('.') && !import_path.starts_with("./") {
189 return ImportCategory::Stdlib;
190 }
191
192 if import_path.starts_with("std::") || import_path.starts_with("core::") {
194 return ImportCategory::Stdlib;
195 }
196
197 if import_path.starts_with("java.") || import_path.starts_with("javax.") {
199 return ImportCategory::Stdlib;
200 }
201
202 let lower = import_path.to_lowercase();
204 if lower.contains("/domain/")
205 || lower.contains("::domain::")
206 || lower.contains(".domain.")
207 || lower.contains("/domain")
208 {
209 return ImportCategory::InternalDomain;
210 }
211 if lower.contains("/application/")
212 || lower.contains("::application::")
213 || lower.contains(".application.")
214 || lower.contains("/usecase/")
215 {
216 return ImportCategory::InternalApplication;
217 }
218 if lower.contains("/infrastructure/")
219 || lower.contains("::infrastructure::")
220 || lower.contains(".infrastructure.")
221 || lower.contains("/adapter/")
222 {
223 return ImportCategory::InternalInfrastructure;
224 }
225
226 if import_path.starts_with("./")
228 || import_path.starts_with("../")
229 || import_path.starts_with("crate::")
230 || import_path.starts_with("super::")
231 {
232 return ImportCategory::InternalDomain; }
234
235 ImportCategory::External
236}
237
238fn classify_all_imports(dependencies: &[Dependency]) -> Vec<ClassifiedImport> {
239 dependencies
240 .iter()
241 .filter_map(|dep| {
242 dep.import_path.as_ref().map(|path| ClassifiedImport {
243 import_path: path.clone(),
244 category: classify_import(path),
245 source_file: dep.location.file.clone(),
246 })
247 })
248 .collect()
249}
250
251fn build_aggregates(
252 entities: &[Component],
253 value_objects: &[Component],
254 classified_imports: &[ClassifiedImport],
255) -> Vec<AggregateAnalysis> {
256 entities
257 .iter()
258 .map(|entity| {
259 let entity_file = &entity.location.file;
260
261 let associated_vos: Vec<Component> =
263 if let ComponentKind::Entity(ref info) = entity.kind {
264 value_objects
265 .iter()
266 .filter(|vo| {
267 info.fields.iter().any(|f| f.type_name.contains(&vo.name))
269 })
270 .cloned()
271 .collect()
272 } else {
273 Vec::new()
274 };
275
276 let file_imports: Vec<&ClassifiedImport> = classified_imports
278 .iter()
279 .filter(|ci| ci.source_file == *entity_file)
280 .collect();
281
282 let stdlib_imports: Vec<String> = file_imports
283 .iter()
284 .filter(|ci| ci.category == ImportCategory::Stdlib)
285 .map(|ci| ci.import_path.clone())
286 .collect();
287
288 let internal_domain_imports: Vec<String> = file_imports
289 .iter()
290 .filter(|ci| ci.category == ImportCategory::InternalDomain)
291 .map(|ci| ci.import_path.clone())
292 .collect();
293
294 let external_imports: Vec<String> = file_imports
295 .iter()
296 .filter(|ci| ci.category == ImportCategory::External)
297 .map(|ci| ci.import_path.clone())
298 .collect();
299
300 let infrastructure_leaks: Vec<String> = file_imports
301 .iter()
302 .filter(|ci| ci.category == ImportCategory::InternalInfrastructure)
303 .map(|ci| ci.import_path.clone())
304 .collect();
305
306 let is_clean = infrastructure_leaks.is_empty();
307
308 let dependency_audit = DependencyAudit {
309 stdlib_imports,
310 internal_domain_imports,
311 external_imports,
312 infrastructure_leaks,
313 is_clean,
314 };
315
316 let ddd_patterns = detect_ddd_patterns(entity);
318
319 AggregateAnalysis {
320 component: entity.clone(),
321 value_objects: associated_vos,
322 dependency_audit,
323 ddd_patterns,
324 }
325 })
326 .collect()
327}
328
329fn detect_ddd_patterns(entity: &Component) -> Vec<DddPattern> {
330 let mut patterns = Vec::new();
331
332 if let ComponentKind::Entity(ref info) = entity.kind {
333 let method_count = info.methods.len();
334
335 patterns.push(DddPattern {
337 name: format!("Rich domain model ({method_count} methods)"),
338 detected: method_count > 0,
339 });
340
341 let has_factory = info
343 .methods
344 .iter()
345 .any(|m| m.name.starts_with("New") || m.name.starts_with("Create"));
346 patterns.push(DddPattern {
347 name: "Factory method".to_string(),
348 detected: has_factory,
349 });
350
351 let has_id = info.fields.iter().any(|f| {
353 let fl = f.name.to_lowercase();
354 fl == "id" || fl == "uuid"
355 });
356 patterns.push(DddPattern {
357 name: "Identity field".to_string(),
358 detected: has_id,
359 });
360
361 patterns.push(DddPattern {
363 name: "Encapsulation (methods)".to_string(),
364 detected: method_count >= 2,
365 });
366 }
367
368 patterns
369}
370
371fn generate_improvements(
372 entities: &[Component],
373 domain_events: &[Component],
374 adapters: &[AdapterMapping],
375 ports: &[Component],
376 violations: &[Violation],
377) -> Vec<String> {
378 let mut suggestions = Vec::new();
379
380 for entity in entities {
382 if let ComponentKind::Entity(ref info) = entity.kind {
383 if info.methods.is_empty() {
384 suggestions.push(format!(
385 "Anemic domain model: `{}` has no business methods. Consider adding domain logic.",
386 entity.name
387 ));
388 }
389 }
390 }
391
392 if domain_events.is_empty() && !entities.is_empty() {
394 suggestions.push(
395 "No domain events found. Consider adding domain events for aggregate state changes."
396 .to_string(),
397 );
398 }
399
400 for adapter in adapters {
402 if adapter.implements_ports.is_empty() {
403 suggestions.push(format!(
404 "Missing port interface for adapter `{}`.",
405 adapter.adapter.name
406 ));
407 }
408 }
409
410 for violation in violations {
412 if let ViolationKind::DomainInfrastructureLeak { ref detail } = violation.kind {
413 suggestions.push(format!("Infrastructure leak: {detail}"));
414 }
415 }
416
417 for entity in entities {
419 if let ComponentKind::Entity(ref info) = entity.kind {
420 if info.fields.len() > 10 {
421 suggestions.push(format!(
422 "`{}` has {} fields. Consider breaking into smaller value objects.",
423 entity.name,
424 info.fields.len()
425 ));
426 }
427 }
428 }
429
430 let adapter_port_names: Vec<&str> = adapters
432 .iter()
433 .flat_map(|a| a.implements_ports.iter().map(|s| s.as_str()))
434 .collect();
435
436 for port in ports {
437 if !adapter_port_names.iter().any(|name| *name == port.name) {
438 suggestions.push(format!(
439 "Port `{}` has no known adapter implementation.",
440 port.name
441 ));
442 }
443 }
444
445 suggestions
446}