rta-core 0.1.0

Core analyzers and report model for the Rust Technical Audit Toolkit
Documentation
use std::collections::BTreeSet;

use crate::{analyzers::Analyzer, collector::RepositorySnapshot, model::ArchitectureAssessment};

pub struct ArchitectureAnalyzer;

impl Analyzer<ArchitectureAssessment> for ArchitectureAnalyzer {
    fn analyze(&self, snapshot: &RepositorySnapshot) -> ArchitectureAssessment {
        let mut detected_layers = BTreeSet::new();
        let mut domain_boundaries = BTreeSet::new();
        let mut circular_dependency_risks = Vec::new();

        for file in snapshot.rust_files() {
            let path = file.relative_path.to_ascii_lowercase();
            for layer in [
                "api",
                "application",
                "domain",
                "infrastructure",
                "adapter",
                "repository",
                "service",
                "transport",
                "persistence",
            ] {
                if path.contains(layer) {
                    detected_layers.insert(layer.to_string());
                }
            }
            if let Some(boundary) = path.split('/').nth(1) {
                if (path.starts_with("src/") || path.starts_with("crates/"))
                    && !boundary.ends_with(".rs")
                {
                    domain_boundaries.insert(boundary.to_string());
                }
            }
            if path.ends_with("lib.rs") || path.ends_with("main.rs") {
                let content = file.content.as_deref().unwrap_or_default();
                let mod_count =
                    content.matches("mod ").count() + content.matches("pub mod ").count();
                if mod_count > 20 {
                    circular_dependency_risks.push(format!(
                        "{} centralizes {mod_count} module declarations",
                        file.relative_path
                    ));
                }
            }
        }

        let detected_layers = detected_layers.into_iter().collect::<Vec<_>>();
        let domain_boundaries = domain_boundaries.into_iter().collect::<Vec<_>>();
        let architecture_style = classify_architecture(snapshot, &detected_layers);
        let separation_of_concerns = match detected_layers.len() {
            0..=1 => "Limited explicit layer separation detected.".to_string(),
            2..=3 => "Some separation of concerns is visible, but boundaries should be reviewed."
                .to_string(),
            _ => "Clear layer vocabulary detected across the repository.".to_string(),
        };

        let mut score = 70_i32 + (detected_layers.len() as i32 * 5).min(20);
        score -= (circular_dependency_risks.len() as i32 * 10).min(30);
        if architecture_style.contains("monolith") {
            score -= 8;
        }

        ArchitectureAssessment {
            detected_layers,
            domain_boundaries,
            circular_dependency_risks,
            architecture_style,
            separation_of_concerns,
            score: score.clamp(0, 100) as u8,
        }
    }
}

fn classify_architecture(snapshot: &RepositorySnapshot, layers: &[String]) -> String {
    let package_count = snapshot
        .manifests
        .iter()
        .filter(|manifest| manifest.package_name.is_some())
        .count();
    if package_count > 3 {
        "modular Cargo workspace".to_string()
    } else if package_count == 1 && layers.len() <= 1 {
        "single-crate monolith".to_string()
    } else {
        "layered single service or compact workspace".to_string()
    }
}