tokmd-analysis 1.10.0

Analysis logic and enrichers for tokmd receipts.
Documentation
use std::cmp::Ordering;

use tokmd_analysis_types::{
    ApiSurfaceReport, ComplexityReport, DerivedReport, DuplicateReport, EffortDriver,
    EffortDriverDirection, EffortSizeBasis, GitReport,
};

pub fn build_drivers(
    basis: &EffortSizeBasis,
    derived: &DerivedReport,
    git: Option<&GitReport>,
    complexity: Option<&ComplexityReport>,
    api_surface: Option<&ApiSurfaceReport>,
    dup: Option<&DuplicateReport>,
) -> Vec<EffortDriver> {
    let mut drivers: Vec<EffortDriver> = Vec::new();

    if basis.generated_pct > 0.30 {
        drivers.push(EffortDriver {
            key: "generated_files".to_string(),
            label: "Large generated surface increases cleanup overhead".to_string(),
            weight: (basis.generated_pct * 0.8).clamp(0.05, 0.75),
            direction: EffortDriverDirection::Raises,
            evidence: format!("{}% generated", round_pct(basis.generated_pct)),
        });
    }

    if basis.vendored_pct > 0.25 {
        drivers.push(EffortDriver {
            key: "vendored_files".to_string(),
            label: "Large vendored surface increases integration friction".to_string(),
            weight: (basis.vendored_pct * 0.9).clamp(0.05, 0.65),
            direction: EffortDriverDirection::Raises,
            evidence: format!("{}% vendored", round_pct(basis.vendored_pct)),
        });
    }

    if let Some(git) = git {
        if git.freshness.stale_pct > 0.30 {
            drivers.push(EffortDriver {
                key: "freshness_staleness".to_string(),
                label: "Stale files may increase rework probability".to_string(),
                weight: (git.freshness.stale_pct * 0.95).clamp(0.06, 0.95),
                direction: EffortDriverDirection::Raises,
                evidence: format!("{}% files stale", round_pct(git.freshness.stale_pct)),
            });
        }

        let coupling_links = git.coupling.len();
        if coupling_links > 0 {
            drivers.push(EffortDriver {
                key: "module_coupling".to_string(),
                label: "High coupling increases integration effort".to_string(),
                weight: (coupling_links as f64 / 120.0).clamp(0.1, 0.7),
                direction: EffortDriverDirection::Raises,
                evidence: format!("{} coupling edges", coupling_links),
            });
        }
    }

    if let Some(complexity) = complexity {
        if complexity.max_cyclomatic > 30 {
            drivers.push(EffortDriver {
                key: "complexity_hotspots".to_string(),
                label: "High cyclomatic complexity raises effort".to_string(),
                weight: ((complexity.max_cyclomatic as f64 - 20.0) / 200.0).clamp(0.15, 0.75),
                direction: EffortDriverDirection::Raises,
                evidence: format!("max cyclomatic {}", complexity.max_cyclomatic),
            });
        }
        if complexity.avg_cyclomatic > 4.0 {
            drivers.push(EffortDriver {
                key: "complexity_breadth".to_string(),
                label: "Average cyclomatic complexity suggests added control flow overhead"
                    .to_string(),
                weight: ((complexity.avg_cyclomatic - 1.0) / 30.0).clamp(0.08, 0.45),
                direction: EffortDriverDirection::Raises,
                evidence: format!("avg cyclomatic {:.2}", complexity.avg_cyclomatic),
            });
        }
    }

    if let Some(api) = api_surface {
        if api.public_ratio < 0.20 {
            drivers.push(EffortDriver {
                key: "api_documentation".to_string(),
                label: "Low public/API documentation ratio may slow reuse".to_string(),
                weight: 0.35,
                direction: EffortDriverDirection::Raises,
                evidence: format!("public ratio {:.2}", api.public_ratio),
            });
        }
        if api.documented_ratio < 0.50 {
            drivers.push(EffortDriver {
                key: "api_documented_ratio".to_string(),
                label: "API documentation coverage is low".to_string(),
                weight: 0.20,
                direction: EffortDriverDirection::Raises,
                evidence: format!("documented ratio {:.2}", api.documented_ratio),
            });
        }
    }

    if let Some(dup) = dup
        && dup.wasted_bytes > 0
    {
        drivers.push(EffortDriver {
            key: "duplication".to_string(),
            label: "Duplicate blocks add review and refactor overhead".to_string(),
            weight: (dup.wasted_bytes as f64 / 12_000.0).clamp(0.1, 0.7),
            direction: EffortDriverDirection::Raises,
            evidence: format!("{} bytes wasted by duplication", dup.wasted_bytes),
        });
    }

    if derived.test_density.ratio < 0.10 {
        drivers.push(EffortDriver {
            key: "test_density".to_string(),
            label: "Limited test coverage increases implied verification effort".to_string(),
            weight: 0.55,
            direction: EffortDriverDirection::Raises,
            evidence: format!("test ratio {:.2}", derived.test_density.ratio),
        });
    }

    if derived.polyglot.lang_count > 8 {
        drivers.push(EffortDriver {
            key: "polyglot_spread".to_string(),
            label: "Wide language spread increases onboarding effort".to_string(),
            weight: (derived.polyglot.lang_count as f64 / 20.0).clamp(0.1, 0.45),
            direction: EffortDriverDirection::Raises,
            evidence: format!("{} languages", derived.polyglot.lang_count),
        });
    }

    if !basis.warnings.is_empty() {
        drivers.push(EffortDriver {
            key: "classification_fallback".to_string(),
            label: "Heuristic file classification used for some files".to_string(),
            weight: 0.08,
            direction: EffortDriverDirection::Raises,
            evidence: format!("{} classification warnings", basis.warnings.len()),
        });
    }

    drivers.sort_by(|a, b| {
        b.weight
            .partial_cmp(&a.weight)
            .unwrap_or(Ordering::Equal)
            .then_with(|| a.key.cmp(&b.key))
    });
    drivers
}

fn round_pct(value: f64) -> String {
    format!("{:.1}", value * 100.0)
}