Skip to main content

debtmap/output/
mod.rs

1//! Output formatting and rendering for analysis results.
2//!
3//! This module provides multiple output formats for debtmap analysis results,
4//! including terminal display, JSON for machine consumption, and Markdown for
5//! documentation and LLM integration.
6//!
7//! # Output Formats
8//!
9//! - **Terminal**: Colorized output for interactive use
10//! - **JSON**: Structured data for tool integration
11//! - **Markdown**: Human-readable reports and LLM prompts
12//! - **DOT**: Graphviz format for call graph visualization
13//!
14//! # View Pipeline
15//!
16//! Output uses a unified view pipeline that prepares data once and formats it
17//! for the requested output format, ensuring consistency across formats.
18
19pub mod dot;
20pub mod evidence_formatter;
21pub mod formatters;
22pub mod json;
23pub mod llm_markdown;
24pub mod markdown;
25pub mod pattern_analysis;
26pub mod pattern_formatter;
27pub mod terminal;
28pub mod unified;
29
30use crate::io::view_formatters;
31use crate::priority::tiers::TierConfig;
32use crate::priority::view::{PreparedDebtView, SortCriteria, ViewConfig};
33use crate::priority::view_pipeline;
34use crate::{core::AnalysisResults, formatting::FormattingConfig, io, priority, risk};
35use anyhow::Result;
36use std::path::PathBuf;
37
38pub struct OutputConfig {
39    pub top: Option<usize>,
40    pub tail: Option<usize>,
41    pub summary: bool,
42    pub verbosity: u8,
43    pub output_file: Option<PathBuf>,
44    pub output_format: Option<crate::cli::OutputFormat>,
45    pub formatting_config: FormattingConfig,
46    pub show_filter_stats: bool,
47}
48
49// ============================================================================
50// SPEC 252: UNIFIED VIEW OUTPUT (New API)
51// ============================================================================
52
53/// Output unified priorities using the new view pipeline (Spec 252).
54///
55/// This function prepares a view once and passes it to the appropriate formatter,
56/// ensuring consistent data across all output formats.
57///
58/// # Arguments
59///
60/// * `analysis` - The analysis results to output
61/// * `config` - Output configuration
62///
63/// # Returns
64///
65/// Result indicating success or failure.
66pub fn output_with_prepared_view(
67    analysis: &priority::UnifiedAnalysis,
68    config: &OutputConfig,
69) -> Result<()> {
70    // Build ViewConfig from OutputConfig
71    let view_config = build_view_config(config);
72    let tier_config = TierConfig::default();
73
74    // Single view preparation (the key to spec 252)
75    let view = view_pipeline::prepare_view(analysis, &view_config, &tier_config);
76
77    // Route to appropriate formatter based on output format
78    output_prepared_view(&view, config)
79}
80
81/// Outputs a prepared view using the specified format.
82///
83/// This is the core routing function that dispatches to format-specific handlers.
84fn output_prepared_view(view: &PreparedDebtView, config: &OutputConfig) -> Result<()> {
85    match &config.output_format {
86        Some(crate::cli::OutputFormat::Json) => {
87            let include_scoring_details = config.verbosity >= 2;
88            let json = view_formatters::format_json(view, include_scoring_details);
89            write_output(&json, &config.output_file)
90        }
91        Some(crate::cli::OutputFormat::Markdown) => {
92            let md_config = view_formatters::MarkdownConfig {
93                verbosity: config.verbosity,
94                show_filter_stats: config.show_filter_stats,
95            };
96            let markdown = view_formatters::format_markdown(view, &md_config);
97            write_output(&markdown, &config.output_file)
98        }
99        _ => {
100            // Terminal output (default)
101            if is_markdown_file(&config.output_file) {
102                let md_config = view_formatters::MarkdownConfig {
103                    verbosity: config.verbosity,
104                    show_filter_stats: config.show_filter_stats,
105                };
106                let markdown = view_formatters::format_markdown(view, &md_config);
107                write_output(&markdown, &config.output_file)
108            } else {
109                let term_config = view_formatters::TerminalConfig {
110                    verbosity: config.verbosity,
111                    use_color: config.formatting_config.color.should_use_color(),
112                    summary_mode: config.summary,
113                };
114                let terminal = view_formatters::format_terminal(view, &term_config);
115                write_output(&terminal, &config.output_file)
116            }
117        }
118    }
119}
120
121/// Builds ViewConfig from OutputConfig.
122fn build_view_config(config: &OutputConfig) -> ViewConfig {
123    // Calculate the limit from top/tail
124    let limit = match (config.top, config.tail) {
125        (Some(n), _) => Some(n),
126        (_, Some(n)) => Some(n),
127        _ => None,
128    };
129
130    // Get threshold from environment or default
131    let min_score_threshold = std::env::var("DEBTMAP_MIN_SCORE_THRESHOLD")
132        .ok()
133        .and_then(|s| s.parse::<f64>().ok())
134        .unwrap_or(3.0);
135
136    ViewConfig {
137        min_score_threshold,
138        exclude_t4_maintenance: true, // Default for terminal output
139        limit,
140        sort_by: SortCriteria::Score, // Default sort
141        compute_groups: false,        // No grouping for non-TUI output
142    }
143}
144
145/// Writes output to file or stdout.
146fn write_output(content: &str, output_file: &Option<PathBuf>) -> Result<()> {
147    use crate::progress::ProgressManager;
148
149    // Clear progress bars before output
150    if let Some(pm) = ProgressManager::global() {
151        let _ = pm.clear();
152    }
153
154    if let Some(path) = output_file {
155        if let Some(parent) = path.parent() {
156            io::ensure_dir(parent)?;
157        }
158        std::fs::write(path, content)?;
159    } else {
160        println!("{content}");
161    }
162    Ok(())
163}
164
165pub use dot::*;
166pub use evidence_formatter::*;
167pub use formatters::*;
168pub use json::*;
169pub use llm_markdown::*;
170pub use markdown::*;
171pub use pattern_analysis::*;
172pub use pattern_formatter::*;
173pub use terminal::*;
174pub use unified::*;
175
176pub fn output_results_with_risk(
177    results: AnalysisResults,
178    risk_insights: Option<risk::RiskInsight>,
179    format: io::output::OutputFormat,
180    output_file: Option<PathBuf>,
181) -> Result<()> {
182    match output_file {
183        Some(path) => {
184            let content = format_results_to_string(&results, &risk_insights, format)?;
185            io::write_file(&path, &content)?;
186        }
187        None => {
188            let mut writer = io::output::create_writer(format);
189            writer.write_results(&results)?;
190            if let Some(insights) = risk_insights {
191                writer.write_risk_insights(&insights)?;
192            }
193        }
194    }
195    Ok(())
196}
197
198pub fn output_unified_priorities_with_config(
199    analysis: priority::UnifiedAnalysis,
200    config: OutputConfig,
201    results: &AnalysisResults,
202    _coverage_file: Option<&PathBuf>,
203) -> Result<()> {
204    output_unified_priorities_with_summary(
205        analysis,
206        config.top,
207        config.tail,
208        config.summary,
209        config.verbosity,
210        config.output_file,
211        config.output_format,
212        config.formatting_config,
213        results,
214        config.show_filter_stats,
215    )
216}
217
218#[allow(clippy::too_many_arguments)]
219pub fn output_unified_priorities(
220    analysis: priority::UnifiedAnalysis,
221    top: Option<usize>,
222    tail: Option<usize>,
223    verbosity: u8,
224    output_file: Option<PathBuf>,
225    output_format: Option<crate::cli::OutputFormat>,
226    formatting_config: FormattingConfig,
227    results: &AnalysisResults,
228) -> Result<()> {
229    output_unified_priorities_with_summary(
230        analysis,
231        top,
232        tail,
233        false, // default to detailed format
234        verbosity,
235        output_file,
236        output_format,
237        formatting_config,
238        results,
239        false, // default to not showing filter stats
240    )
241}
242
243#[allow(clippy::too_many_arguments)]
244pub fn output_unified_priorities_with_summary(
245    analysis: priority::UnifiedAnalysis,
246    top: Option<usize>,
247    tail: Option<usize>,
248    summary: bool,
249    verbosity: u8,
250    output_file: Option<PathBuf>,
251    output_format: Option<crate::cli::OutputFormat>,
252    formatting_config: FormattingConfig,
253    _results: &AnalysisResults,
254    show_filter_stats: bool,
255) -> Result<()> {
256    match output_format {
257        Some(crate::cli::OutputFormat::Json) => {
258            let include_scoring_details = verbosity >= 2;
259            json::output_json_with_format(
260                &analysis,
261                top,
262                tail,
263                output_file,
264                include_scoring_details,
265            )
266        }
267        Some(crate::cli::OutputFormat::Markdown) => {
268            let include_scoring_details = verbosity >= 2;
269            llm_markdown::output_llm_markdown_with_format(
270                &analysis,
271                top,
272                tail,
273                output_file,
274                include_scoring_details,
275            )
276        }
277        Some(crate::cli::OutputFormat::Dot) => {
278            // DOT format for Graphviz visualization (Spec 204)
279            dot::output_dot_default(&analysis, output_file)
280        }
281        _ => {
282            if is_markdown_file(&output_file) {
283                markdown::output_markdown(
284                    &analysis,
285                    top,
286                    tail,
287                    verbosity,
288                    output_file,
289                    formatting_config,
290                    show_filter_stats,
291                )
292            } else {
293                terminal::output_terminal_with_mode(
294                    &analysis,
295                    top,
296                    tail,
297                    verbosity,
298                    output_file,
299                    formatting_config,
300                    summary,
301                )
302            }
303        }
304    }
305}
306
307fn is_markdown_file(output_file: &Option<PathBuf>) -> bool {
308    output_file
309        .as_ref()
310        .and_then(|p| p.extension())
311        .map(|ext| ext == "md")
312        .unwrap_or(false)
313}