debtmap 0.16.4

Code complexity and technical debt analyzer
Documentation
//! Output formatting and rendering for analysis results.
//!
//! This module provides multiple output formats for debtmap analysis results,
//! including terminal display, JSON for machine consumption, and Markdown for
//! documentation and LLM integration.
//!
//! # Output Formats
//!
//! - **Terminal**: Colorized output for interactive use
//! - **JSON**: Structured data for tool integration
//! - **Markdown**: Human-readable reports and LLM prompts
//! - **DOT**: Graphviz format for call graph visualization
//!
//! # View Pipeline
//!
//! Output uses a unified view pipeline that prepares data once and formats it
//! for the requested output format, ensuring consistency across formats.

pub mod dot;
pub mod evidence_formatter;
pub mod formatters;
pub mod json;
pub mod llm_markdown;
pub mod markdown;
pub mod pattern_analysis;
pub mod pattern_formatter;
pub mod terminal;
pub mod unified;

use crate::io::view_formatters;
use crate::priority::tiers::TierConfig;
use crate::priority::view::{PreparedDebtView, SortCriteria, ViewConfig};
use crate::priority::view_pipeline;
use crate::{core::AnalysisResults, formatting::FormattingConfig, io, priority, risk};
use anyhow::Result;
use std::path::PathBuf;

pub struct OutputConfig {
    pub top: Option<usize>,
    pub tail: Option<usize>,
    pub summary: bool,
    pub verbosity: u8,
    pub output_file: Option<PathBuf>,
    pub output_format: Option<crate::cli::OutputFormat>,
    pub formatting_config: FormattingConfig,
    pub show_filter_stats: bool,
}

// ============================================================================
// SPEC 252: UNIFIED VIEW OUTPUT (New API)
// ============================================================================

/// Output unified priorities using the new view pipeline (Spec 252).
///
/// This function prepares a view once and passes it to the appropriate formatter,
/// ensuring consistent data across all output formats.
///
/// # Arguments
///
/// * `analysis` - The analysis results to output
/// * `config` - Output configuration
///
/// # Returns
///
/// Result indicating success or failure.
pub fn output_with_prepared_view(
    analysis: &priority::UnifiedAnalysis,
    config: &OutputConfig,
) -> Result<()> {
    // Build ViewConfig from OutputConfig
    let view_config = build_view_config(config);
    let tier_config = TierConfig::default();

    // Single view preparation (the key to spec 252)
    let view = view_pipeline::prepare_view(analysis, &view_config, &tier_config);

    // Route to appropriate formatter based on output format
    output_prepared_view(&view, config)
}

/// Outputs a prepared view using the specified format.
///
/// This is the core routing function that dispatches to format-specific handlers.
fn output_prepared_view(view: &PreparedDebtView, config: &OutputConfig) -> Result<()> {
    match &config.output_format {
        Some(crate::cli::OutputFormat::Json) => {
            let include_scoring_details = config.verbosity >= 2;
            let json = view_formatters::format_json(view, include_scoring_details);
            write_output(&json, &config.output_file)
        }
        Some(crate::cli::OutputFormat::Markdown) => {
            let md_config = view_formatters::MarkdownConfig {
                verbosity: config.verbosity,
                show_filter_stats: config.show_filter_stats,
            };
            let markdown = view_formatters::format_markdown(view, &md_config);
            write_output(&markdown, &config.output_file)
        }
        _ => {
            // Terminal output (default)
            if is_markdown_file(&config.output_file) {
                let md_config = view_formatters::MarkdownConfig {
                    verbosity: config.verbosity,
                    show_filter_stats: config.show_filter_stats,
                };
                let markdown = view_formatters::format_markdown(view, &md_config);
                write_output(&markdown, &config.output_file)
            } else {
                let term_config = view_formatters::TerminalConfig {
                    verbosity: config.verbosity,
                    use_color: config.formatting_config.color.should_use_color(),
                    summary_mode: config.summary,
                };
                let terminal = view_formatters::format_terminal(view, &term_config);
                write_output(&terminal, &config.output_file)
            }
        }
    }
}

/// Builds ViewConfig from OutputConfig.
fn build_view_config(config: &OutputConfig) -> ViewConfig {
    // Calculate the limit from top/tail
    let limit = match (config.top, config.tail) {
        (Some(n), _) => Some(n),
        (_, Some(n)) => Some(n),
        _ => None,
    };

    // Get threshold from environment or default
    let min_score_threshold = std::env::var("DEBTMAP_MIN_SCORE_THRESHOLD")
        .ok()
        .and_then(|s| s.parse::<f64>().ok())
        .unwrap_or(3.0);

    ViewConfig {
        min_score_threshold,
        exclude_t4_maintenance: true, // Default for terminal output
        limit,
        sort_by: SortCriteria::Score, // Default sort
        compute_groups: false,        // No grouping for non-TUI output
    }
}

/// Writes output to file or stdout.
fn write_output(content: &str, output_file: &Option<PathBuf>) -> Result<()> {
    use crate::progress::ProgressManager;

    // Clear progress bars before output
    if let Some(pm) = ProgressManager::global() {
        let _ = pm.clear();
    }

    if let Some(path) = output_file {
        if let Some(parent) = path.parent() {
            io::ensure_dir(parent)?;
        }
        std::fs::write(path, content)?;
    } else {
        println!("{content}");
    }
    Ok(())
}

pub use dot::*;
pub use evidence_formatter::*;
pub use formatters::*;
pub use json::*;
pub use llm_markdown::*;
pub use markdown::*;
pub use pattern_analysis::*;
pub use pattern_formatter::*;
pub use terminal::*;
pub use unified::*;

pub fn output_results_with_risk(
    results: AnalysisResults,
    risk_insights: Option<risk::RiskInsight>,
    format: io::output::OutputFormat,
    output_file: Option<PathBuf>,
) -> Result<()> {
    match output_file {
        Some(path) => {
            let content = format_results_to_string(&results, &risk_insights, format)?;
            io::write_file(&path, &content)?;
        }
        None => {
            let mut writer = io::output::create_writer(format);
            writer.write_results(&results)?;
            if let Some(insights) = risk_insights {
                writer.write_risk_insights(&insights)?;
            }
        }
    }
    Ok(())
}

pub fn output_unified_priorities_with_config(
    analysis: priority::UnifiedAnalysis,
    config: OutputConfig,
    results: &AnalysisResults,
    _coverage_file: Option<&PathBuf>,
) -> Result<()> {
    output_unified_priorities_with_summary(
        analysis,
        config.top,
        config.tail,
        config.summary,
        config.verbosity,
        config.output_file,
        config.output_format,
        config.formatting_config,
        results,
        config.show_filter_stats,
    )
}

#[allow(clippy::too_many_arguments)]
pub fn output_unified_priorities(
    analysis: priority::UnifiedAnalysis,
    top: Option<usize>,
    tail: Option<usize>,
    verbosity: u8,
    output_file: Option<PathBuf>,
    output_format: Option<crate::cli::OutputFormat>,
    formatting_config: FormattingConfig,
    results: &AnalysisResults,
) -> Result<()> {
    output_unified_priorities_with_summary(
        analysis,
        top,
        tail,
        false, // default to detailed format
        verbosity,
        output_file,
        output_format,
        formatting_config,
        results,
        false, // default to not showing filter stats
    )
}

#[allow(clippy::too_many_arguments)]
pub fn output_unified_priorities_with_summary(
    analysis: priority::UnifiedAnalysis,
    top: Option<usize>,
    tail: Option<usize>,
    summary: bool,
    verbosity: u8,
    output_file: Option<PathBuf>,
    output_format: Option<crate::cli::OutputFormat>,
    formatting_config: FormattingConfig,
    _results: &AnalysisResults,
    show_filter_stats: bool,
) -> Result<()> {
    match output_format {
        Some(crate::cli::OutputFormat::Json) => {
            let include_scoring_details = verbosity >= 2;
            json::output_json_with_format(
                &analysis,
                top,
                tail,
                output_file,
                include_scoring_details,
            )
        }
        Some(crate::cli::OutputFormat::Markdown) => {
            let include_scoring_details = verbosity >= 2;
            llm_markdown::output_llm_markdown_with_format(
                &analysis,
                top,
                tail,
                output_file,
                include_scoring_details,
            )
        }
        Some(crate::cli::OutputFormat::Dot) => {
            // DOT format for Graphviz visualization (Spec 204)
            dot::output_dot_default(&analysis, output_file)
        }
        _ => {
            if is_markdown_file(&output_file) {
                markdown::output_markdown(
                    &analysis,
                    top,
                    tail,
                    verbosity,
                    output_file,
                    formatting_config,
                    show_filter_stats,
                )
            } else {
                terminal::output_terminal_with_mode(
                    &analysis,
                    top,
                    tail,
                    verbosity,
                    output_file,
                    formatting_config,
                    summary,
                )
            }
        }
    }
}

fn is_markdown_file(output_file: &Option<PathBuf>) -> bool {
    output_file
        .as_ref()
        .and_then(|p| p.extension())
        .map(|ext| ext == "md")
        .unwrap_or(false)
}