rta-core 0.1.0

Core analyzers and report model for the Rust Technical Audit Toolkit
Documentation
use crate::{
    analyzers::Analyzer,
    collector::RepositorySnapshot,
    model::{
        ArchitectureAssessment, CodeQuality, DependencyHealth, Overview, RiskFinding, RiskReport,
        Severity, TestingMaturity,
    },
};

pub struct RiskAnalyzer<'a> {
    overview: &'a Overview,
    dependencies: &'a DependencyHealth,
    code_quality: &'a CodeQuality,
    architecture: &'a ArchitectureAssessment,
    testing: &'a TestingMaturity,
}

impl<'a> RiskAnalyzer<'a> {
    pub fn new(
        overview: &'a Overview,
        dependencies: &'a DependencyHealth,
        code_quality: &'a CodeQuality,
        architecture: &'a ArchitectureAssessment,
        testing: &'a TestingMaturity,
    ) -> Self {
        Self {
            overview,
            dependencies,
            code_quality,
            architecture,
            testing,
        }
    }
}

impl Analyzer<RiskReport> for RiskAnalyzer<'_> {
    fn analyze(&self, _snapshot: &RepositorySnapshot) -> RiskReport {
        let mut findings = Vec::new();

        if self.overview.package_count <= 1 && self.code_quality.lines_of_code > 8_000 {
            findings.push(RiskFinding {
                severity: Severity::Medium,
                title: "Bus factor and ownership concentration".into(),
                evidence: "Large codebase concentrated in a single package.".into(),
                recommendation:
                    "Review ownership boundaries and split high-change domains into explicit crates."
                        .into(),
            });
        }
        if !self.code_quality.god_module_candidates.is_empty() {
            findings.push(RiskFinding {
                severity: Severity::High,
                title: "Potential God modules".into(),
                evidence: self.code_quality.god_module_candidates.join(", "),
                recommendation:
                    "Extract cohesive submodules and isolate orchestration from domain behavior."
                        .into(),
            });
        }
        if !self.dependencies.maintenance_risks.is_empty() {
            findings.push(RiskFinding {
                severity: Severity::Medium,
                title: "Dependency maintenance risk".into(),
                evidence: self.dependencies.maintenance_risks.join("; "),
                recommendation:
                    "Review dependency sourcing, version policy, and upgrade ownership.".into(),
            });
        }
        if !self.testing.has_tests {
            findings.push(RiskFinding {
                severity: Severity::High,
                title: "Lack of automated tests".into(),
                evidence: "No unit or integration tests were detected.".into(),
                recommendation:
                    "Introduce smoke, integration, and critical-path unit tests before major investment."
                        .into(),
            });
        }
        if self.dependencies.total_dependencies > 60 {
            findings.push(RiskFinding {
                severity: Severity::Medium,
                title: "Excessive dependency concentration".into(),
                evidence: format!(
                    "{} direct dependencies were detected.",
                    self.dependencies.total_dependencies
                ),
                recommendation:
                    "Identify strategic dependencies and remove low-value transitive surface area."
                        .into(),
            });
        }
        if !self.architecture.circular_dependency_risks.is_empty() {
            findings.push(RiskFinding {
                severity: Severity::Medium,
                title: "Circular dependency risk".into(),
                evidence: self.architecture.circular_dependency_risks.join("; "),
                recommendation:
                    "Review module directionality and enforce dependency rules at crate boundaries."
                        .into(),
            });
        }

        let mut score = 100_i32;
        for finding in &findings {
            score -= match finding.severity {
                Severity::Low => 4,
                Severity::Medium => 10,
                Severity::High => 18,
            };
        }

        RiskReport {
            findings,
            score: score.clamp(0, 100) as u8,
        }
    }
}