Skip to main content

boundary_core/
forensics.rs

1use std::path::{Path, PathBuf};
2
3use walkdir::WalkDir;
4
5use crate::metrics::ArchitectureScore;
6use crate::pipeline::FullAnalysis;
7use crate::types::*;
8
9/// Full forensics analysis for a module.
10pub 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
25/// An entry in the directory tree.
26pub struct DirEntry {
27    pub rel_path: String,
28    pub is_dir: bool,
29    pub depth: usize,
30}
31
32/// Analysis of a single aggregate (entity).
33pub 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
40/// A DDD pattern detection result.
41pub struct DddPattern {
42    pub name: String,
43    pub detected: bool,
44}
45
46/// Audit of an aggregate's dependencies.
47pub 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
55/// Mapping from an adapter to the ports it implements.
56pub struct AdapterMapping {
57    pub adapter: Component,
58    pub implements_ports: Vec<String>,
59}
60
61/// A classified import.
62pub struct ClassifiedImport {
63    pub import_path: String,
64    pub category: ImportCategory,
65    pub source_file: PathBuf,
66}
67
68/// Category of an import path.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum ImportCategory {
71    Stdlib,
72    InternalDomain,
73    InternalApplication,
74    InternalInfrastructure,
75    External,
76}
77
78/// Build a forensics analysis from a full analysis and module path.
79pub 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    // Build directory tree
90    let directory_tree = build_directory_tree(module_path);
91
92    // Classify imports
93    let classified_imports = classify_all_imports(&full_analysis.dependencies);
94
95    // Group components by kind
96    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    // Build aggregate analyses
130    let aggregates = build_aggregates(&entities, &value_objects, &classified_imports);
131
132    // Generate improvement suggestions
133    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    // Go: no dots in path segment -> stdlib
188    if !import_path.contains('.') && !import_path.starts_with("./") {
189        return ImportCategory::Stdlib;
190    }
191
192    // Rust stdlib
193    if import_path.starts_with("std::") || import_path.starts_with("core::") {
194        return ImportCategory::Stdlib;
195    }
196
197    // Java stdlib
198    if import_path.starts_with("java.") || import_path.starts_with("javax.") {
199        return ImportCategory::Stdlib;
200    }
201
202    // Internal detection by path patterns
203    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    // Relative imports (TS) or crate-local (Rust)
227    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; // conservative default for relative imports
233    }
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            // Find value objects that might be used by this entity
262            let associated_vos: Vec<Component> =
263                if let ComponentKind::Entity(ref info) = entity.kind {
264                    value_objects
265                        .iter()
266                        .filter(|vo| {
267                            // Check if any field type matches a value object name
268                            info.fields.iter().any(|f| f.type_name.contains(&vo.name))
269                        })
270                        .cloned()
271                        .collect()
272                } else {
273                    Vec::new()
274                };
275
276            // Build dependency audit from the entity's file imports
277            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            // Detect DDD patterns
317            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        // Rich domain model
336        patterns.push(DddPattern {
337            name: format!("Rich domain model ({method_count} methods)"),
338            detected: method_count > 0,
339        });
340
341        // Factory method
342        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        // Has identity field
352        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        // Encapsulation (methods exist to manipulate state)
362        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    // Anemic domain models
381    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    // No domain events
393    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    // Adapters without ports
401    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    // Infrastructure leaks from violations
411    for violation in violations {
412        if let ViolationKind::DomainInfrastructureLeak { ref detail } = violation.kind {
413            suggestions.push(format!("Infrastructure leak: {detail}"));
414        }
415    }
416
417    // Large entities
418    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    // Check port-to-adapter coverage
431    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}