debtmap 0.17.0

Code complexity and technical debt analyzer
Documentation
use crate::{core::*, debt, risk};
use anyhow::{Context, Result};
use im::Vector;
use std::path::Path;

pub fn analyze_risk_with_coverage(
    results: &AnalysisResults,
    lcov_path: &Path,
    project_path: &Path,
    enable_context: bool,
    context_providers: Option<Vec<String>>,
    disable_context: Option<Vec<String>>,
) -> Result<Option<risk::RiskInsight>> {
    let lcov_data = risk::lcov::parse_lcov_file(lcov_path).context("Failed to parse LCOV file")?;
    let debt_score = debt::total_debt_score(&results.technical_debt.items) as f64;
    let debt_threshold = 100.0;

    let mut analyzer = risk::RiskAnalyzer::default().with_debt_context(debt_score, debt_threshold);

    if let Some(aggregator) = build_context_aggregator(
        project_path,
        enable_context,
        context_providers,
        disable_context,
        // Preload runs once inside perform_unified_analysis_with_options
        // (TUI stage 4); skip here so we don't block before call-graph stage.
        None,
    ) {
        analyzer = analyzer.with_context_aggregator(aggregator);
    }

    let mut function_risks = Vector::new();
    let has_context = analyzer.has_context();

    for func in &results.complexity.metrics {
        let complexity_metrics = ComplexityMetrics::from_function(func);
        let coverage = lcov_data.get_function_coverage_with_line(&func.file, &func.name, func.line);

        let risk = if has_context {
            // Use context-aware analysis when context providers are enabled
            let (risk_with_ctx, _) = analyzer.analyze_function_with_context(
                func.file.clone(),
                func.name.clone(),
                (func.line, func.line + func.length),
                &complexity_metrics,
                coverage,
                func.is_test,
                project_path.to_path_buf(),
            );
            risk_with_ctx
        } else {
            analyzer.analyze_function(
                func.file.clone(),
                func.name.clone(),
                (func.line, func.line + func.length),
                &complexity_metrics,
                coverage,
                func.is_test,
            )
        };

        function_risks.push_back(risk);
    }

    let insights = risk::insights::generate_risk_insights(function_risks, &analyzer);
    Ok(Some(insights))
}

pub fn analyze_risk_without_coverage(
    results: &AnalysisResults,
    enable_context: bool,
    context_providers: Option<Vec<String>>,
    disable_context: Option<Vec<String>>,
    project_path: &Path,
) -> Result<Option<risk::RiskInsight>> {
    let debt_score = debt::total_debt_score(&results.technical_debt.items) as f64;
    let debt_threshold = 100.0;

    let mut analyzer = risk::RiskAnalyzer::default().with_debt_context(debt_score, debt_threshold);

    if let Some(aggregator) = build_context_aggregator(
        project_path,
        enable_context,
        context_providers,
        disable_context,
        // Preload runs once inside perform_unified_analysis_with_options
        // (TUI stage 4); skip here so we don't block before call-graph stage.
        None,
    ) {
        analyzer = analyzer.with_context_aggregator(aggregator);
    }

    let mut function_risks = Vector::new();
    let has_context = analyzer.has_context();

    for func in &results.complexity.metrics {
        let complexity_metrics = ComplexityMetrics::from_function(func);

        let risk = if has_context {
            // Use context-aware analysis when context providers are enabled
            let (risk_with_ctx, _) = analyzer.analyze_function_with_context(
                func.file.clone(),
                func.name.clone(),
                (func.line, func.line + func.length),
                &complexity_metrics,
                None,
                func.is_test,
                project_path.to_path_buf(),
            );
            risk_with_ctx
        } else {
            analyzer.analyze_function(
                func.file.clone(),
                func.name.clone(),
                (func.line, func.line + func.length),
                &complexity_metrics,
                None,
                func.is_test,
            )
        };

        function_risks.push_back(risk);
    }

    let insights = risk::insights::generate_risk_insights(function_risks, &analyzer);
    Ok(Some(insights))
}

pub fn build_context_aggregator(
    project_path: &Path,
    enable_context: bool,
    context_providers: Option<Vec<String>>,
    disable_context: Option<Vec<String>>,
    function_metrics: Option<&[FunctionMetrics]>,
) -> Option<risk::context::ContextAggregator> {
    if !enable_context {
        return None;
    }

    let enabled_providers = context_providers.unwrap_or_else(get_default_providers);
    let disabled = disable_context.unwrap_or_default();

    let aggregator = enabled_providers
        .into_iter()
        .filter(|name| !disabled.contains(name))
        .fold(
            risk::context::ContextAggregator::new(),
            |acc, provider_name| {
                add_provider_to_aggregator(acc, &provider_name, project_path, function_metrics)
            },
        );

    Some(aggregator)
}

fn get_default_providers() -> Vec<String> {
    vec![
        "critical_path".to_string(),
        "dependency".to_string(),
        "git_history".to_string(),
    ]
}

fn add_provider_to_aggregator(
    aggregator: risk::context::ContextAggregator,
    provider_name: &str,
    project_path: &Path,
    function_metrics: Option<&[FunctionMetrics]>,
) -> risk::context::ContextAggregator {
    // Map provider names to subsection indices (spec 219)
    // 0 = critical_path, 1 = dependency, 2 = git_history
    let subsection_index = match provider_name {
        "critical_path" => Some(0),
        "dependency" => Some(1),
        "git_history" => Some(2),
        _ => None,
    };

    // Update subsection to Active state
    // Context stage is at index 4 (0=files, 1=call graph, 2=coverage, 3=purity, 4=context, 5=debt scoring)
    if let Some(index) = subsection_index {
        if let Some(manager) = crate::progress::ProgressManager::global() {
            manager.tui_update_subtask(4, index, crate::tui::app::StageStatus::Active, None);
        }
    }

    let result = match create_provider(provider_name, project_path, function_metrics) {
        Some(provider) => aggregator.with_provider(provider),
        None => {
            eprintln!("Warning: Unknown context provider: {provider_name}");
            aggregator
        }
    };

    // Update subsection to Completed state and add visibility pause (spec 219)
    // Context stage is at index 4 (0=files, 1=call graph, 2=coverage, 3=purity, 4=context, 5=debt scoring)
    if let Some(index) = subsection_index {
        if let Some(manager) = crate::progress::ProgressManager::global() {
            manager.tui_update_subtask(4, index, crate::tui::app::StageStatus::Completed, None);
            if index == 2 {
                manager.tui_update_subtask_labeled(
                    4,
                    2,
                    crate::tui::app::StageStatus::Completed,
                    None,
                    Some("git history"),
                );
            }
            // 150ms visibility pause for user feedback
            std::thread::sleep(std::time::Duration::from_millis(150));
        }
    }

    result
}

fn create_provider(
    provider_name: &str,
    project_path: &Path,
    function_metrics: Option<&[FunctionMetrics]>,
) -> Option<Box<dyn risk::context::ContextProvider>> {
    match provider_name {
        "critical_path" => Some(create_critical_path_provider()),
        "dependency" => Some(create_dependency_provider()),
        "git_history" => create_git_history_provider(project_path, function_metrics),
        _ => None,
    }
}

fn create_critical_path_provider() -> Box<dyn risk::context::ContextProvider> {
    let analyzer = risk::context::critical_path::CriticalPathAnalyzer::new();
    Box::new(risk::context::critical_path::CriticalPathProvider::new(
        analyzer,
    ))
}

fn create_dependency_provider() -> Box<dyn risk::context::ContextProvider> {
    let graph = risk::context::dependency::DependencyGraph::new();
    Box::new(risk::context::dependency::DependencyRiskProvider::new(
        graph,
    ))
}

fn create_git_history_provider(
    project_path: &Path,
    function_metrics: Option<&[FunctionMetrics]>,
) -> Option<Box<dyn risk::context::ContextProvider>> {
    use risk::context::git_history::batched_function::{GitPreloadPhase, ProgressCallback};

    if let Some(manager) = crate::progress::ProgressManager::global() {
        manager.tui_update_subtask_labeled(
            4,
            2,
            crate::tui::app::StageStatus::Active,
            Some((0, 0)),
            Some("git history · commits"),
        );
    }

    let mut provider =
        risk::context::git_history::GitHistoryProvider::new(project_path.to_path_buf()).ok()?;
    if let Some(metrics) = function_metrics {
        let last_logged = std::sync::atomic::AtomicUsize::new(0);
        let progress = move |phase: GitPreloadPhase, done: usize, total: usize| {
            let label = match phase {
                GitPreloadPhase::Commits => Some("git history · commits"),
                GitPreloadPhase::BlameFiles => Some("git history · authors"),
            };
            if let Some(manager) = crate::progress::ProgressManager::global() {
                manager.tui_update_subtask_labeled(
                    4,
                    2,
                    crate::tui::app::StageStatus::Active,
                    Some((done, total)),
                    label,
                );
            }
            let prev = last_logged.load(std::sync::atomic::Ordering::Relaxed);
            let log_interval = match phase {
                GitPreloadPhase::Commits => 200,
                GitPreloadPhase::BlameFiles => 20,
            };
            if done == total || done.saturating_sub(prev) >= log_interval {
                last_logged.store(done, std::sync::atomic::Ordering::Relaxed);
                let unit = match phase {
                    GitPreloadPhase::Commits => "commits",
                    GitPreloadPhase::BlameFiles => "files",
                };
                log::info!("Git history {unit}: {done}/{total}");
            }
        };
        let cb: ProgressCallback<'_> = &progress;
        if let Err(e) = provider.preload_function_histories_with_progress(metrics, Some(cb)) {
            log::warn!("Function git history preload failed: {e}");
        }
    }
    Some(Box::new(provider) as Box<dyn risk::context::ContextProvider>)
}