1use crate::priority::view::{PreparedDebtView, ViewItem, ViewSummary};
24use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct JsonOutput {
35 pub format_version: String,
36 pub metadata: JsonMetadata,
37 pub summary: JsonSummary,
38 pub items: Vec<JsonItem>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct JsonMetadata {
44 pub debtmap_version: String,
45 pub generated_at: String,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub project_root: Option<String>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct JsonSummary {
53 pub total_items: usize,
54 pub total_items_before_filter: usize,
55 pub total_debt_score: f64,
56 pub debt_density: f64,
57 pub total_lines_of_code: usize,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub overall_coverage: Option<f64>,
60 pub score_distribution: JsonScoreDistribution,
61 pub category_counts: JsonCategoryCounts,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct JsonScoreDistribution {
67 pub critical: usize,
68 pub high: usize,
69 pub medium: usize,
70 pub low: usize,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct JsonCategoryCounts {
76 pub architecture: usize,
77 pub testing: usize,
78 pub performance: usize,
79 pub code_quality: usize,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(tag = "type")]
85pub enum JsonItem {
86 Function(Box<JsonFunctionItem>),
87 File(Box<JsonFileItem>),
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct JsonFunctionItem {
93 pub score: f64,
94 pub severity: String,
95 pub category: String,
96 pub location: JsonLocation,
97 pub metrics: JsonFunctionMetrics,
98 pub recommendation: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub tier: Option<String>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub scoring_details: Option<JsonScoringDetails>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct JsonFileItem {
108 pub score: f64,
109 pub severity: String,
110 pub category: String,
111 pub location: JsonLocation,
112 pub metrics: JsonFileMetrics,
113 pub recommendation: String,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct JsonLocation {
119 pub file: String,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub line: Option<usize>,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub function: Option<String>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct JsonFunctionMetrics {
129 pub cyclomatic_complexity: u32,
130 pub cognitive_complexity: u32,
131 pub function_length: usize,
132 pub nesting_depth: u32,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub coverage: Option<f64>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct JsonFileMetrics {
140 pub total_lines: usize,
141 pub function_count: usize,
142 pub avg_complexity: f64,
143 pub max_complexity: u32,
144 pub coverage_percent: f64,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct JsonScoringDetails {
150 pub complexity_factor: f64,
151 pub coverage_factor: f64,
152 pub dependency_factor: f64,
153 pub role_multiplier: f64,
154 pub final_score: f64,
155}
156
157pub fn format_json(view: &PreparedDebtView, include_scoring_details: bool) -> String {
172 let output = to_json_output(view, include_scoring_details);
173 serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string())
174}
175
176pub fn to_json_output(view: &PreparedDebtView, include_scoring_details: bool) -> JsonOutput {
178 JsonOutput {
179 format_version: "3.0".to_string(),
180 metadata: JsonMetadata {
181 debtmap_version: env!("CARGO_PKG_VERSION").to_string(),
182 generated_at: chrono::Utc::now().to_rfc3339(),
183 project_root: None,
184 },
185 summary: convert_summary(&view.summary),
186 items: view
187 .items
188 .iter()
189 .map(|item| convert_item(item, include_scoring_details))
190 .collect(),
191 }
192}
193
194fn convert_summary(summary: &ViewSummary) -> JsonSummary {
195 JsonSummary {
196 total_items: summary.total_items_after_filter,
197 total_items_before_filter: summary.total_items_before_filter,
198 total_debt_score: summary.total_debt_score,
199 debt_density: summary.debt_density,
200 total_lines_of_code: summary.total_lines_of_code,
201 overall_coverage: summary.overall_coverage,
202 score_distribution: JsonScoreDistribution {
203 critical: summary.score_distribution.critical,
204 high: summary.score_distribution.high,
205 medium: summary.score_distribution.medium,
206 low: summary.score_distribution.low,
207 },
208 category_counts: JsonCategoryCounts {
209 architecture: summary.category_counts.architecture,
210 testing: summary.category_counts.testing,
211 performance: summary.category_counts.performance,
212 code_quality: summary.category_counts.code_quality,
213 },
214 }
215}
216
217fn convert_item(item: &ViewItem, include_scoring_details: bool) -> JsonItem {
218 match item {
219 ViewItem::Function(func) => {
220 let loc = item.location();
221 JsonItem::Function(Box::new(JsonFunctionItem {
222 score: func.unified_score.final_score.value(),
223 severity: item.severity().as_str().to_lowercase(),
224 category: item.category().to_string(),
225 location: JsonLocation {
226 file: loc.file.to_string_lossy().to_string(),
227 line: Some(loc.line.unwrap_or(0)),
228 function: loc.function.clone(),
229 },
230 metrics: JsonFunctionMetrics {
231 cyclomatic_complexity: func.cyclomatic_complexity,
232 cognitive_complexity: func.cognitive_complexity,
233 function_length: func.function_length,
234 nesting_depth: func.nesting_depth,
235 coverage: func.transitive_coverage.as_ref().map(|c| c.direct),
236 },
237 recommendation: func.recommendation.primary_action.clone(),
238 tier: func
239 .tier
240 .as_ref()
241 .map(|t| format!("{:?}", t).to_lowercase()),
242 scoring_details: if include_scoring_details {
243 Some(JsonScoringDetails {
244 complexity_factor: func.unified_score.complexity_factor,
245 coverage_factor: func.unified_score.coverage_factor,
246 dependency_factor: func.unified_score.dependency_factor,
247 role_multiplier: func.unified_score.role_multiplier,
248 final_score: func.unified_score.final_score.value(),
249 })
250 } else {
251 None
252 },
253 }))
254 }
255 ViewItem::File(file) => {
256 let loc = item.location();
257 JsonItem::File(Box::new(JsonFileItem {
258 score: file.score,
259 severity: item.severity().as_str().to_lowercase(),
260 category: item.category().to_string(),
261 location: JsonLocation {
262 file: loc.file.to_string_lossy().to_string(),
263 line: None,
264 function: None,
265 },
266 metrics: JsonFileMetrics {
267 total_lines: file.metrics.total_lines,
268 function_count: file.metrics.function_count,
269 avg_complexity: file.metrics.avg_complexity,
270 max_complexity: file.metrics.max_complexity,
271 coverage_percent: file.metrics.coverage_percent,
272 },
273 recommendation: file.recommendation.clone(),
274 }))
275 }
276 }
277}
278
279pub struct TerminalConfig {
285 pub verbosity: u8,
286 pub use_color: bool,
287 pub summary_mode: bool,
288}
289
290impl Default for TerminalConfig {
291 fn default() -> Self {
292 Self {
293 verbosity: 0,
294 use_color: true,
295 summary_mode: false,
296 }
297 }
298}
299
300pub fn format_terminal(view: &PreparedDebtView, config: &TerminalConfig) -> String {
311 use std::fmt::Write;
312 let mut output = String::new();
313
314 writeln!(
316 output,
317 "\n═══════════════════════════════════════════════════════════════════════════════"
318 )
319 .ok();
320 writeln!(output, " TECHNICAL DEBT ANALYSIS").ok();
321 writeln!(
322 output,
323 "═══════════════════════════════════════════════════════════════════════════════\n"
324 )
325 .ok();
326
327 format_terminal_summary(&mut output, &view.summary);
329
330 if view.is_empty() {
331 writeln!(
332 output,
333 "\nNo technical debt items found matching current thresholds."
334 )
335 .ok();
336 return output;
337 }
338
339 if config.summary_mode {
341 format_terminal_summary_mode(&mut output, view);
342 } else {
343 format_terminal_items(&mut output, view, config.verbosity);
344 }
345
346 output
347}
348
349fn format_terminal_summary(output: &mut String, summary: &ViewSummary) {
350 use std::fmt::Write;
351
352 writeln!(output, "Summary:").ok();
353 writeln!(
354 output,
355 " Total items: {} (of {} analyzed)",
356 summary.total_items_after_filter, summary.total_items_before_filter
357 )
358 .ok();
359 writeln!(
360 output,
361 " Total debt score: {:.1}",
362 summary.total_debt_score
363 )
364 .ok();
365 writeln!(
366 output,
367 " Debt density: {:.2} per 1k LOC",
368 summary.debt_density
369 )
370 .ok();
371 if let Some(coverage) = summary.overall_coverage {
372 writeln!(output, " Overall coverage: {:.1}%", coverage * 100.0).ok();
373 }
374 writeln!(output).ok();
375
376 writeln!(
377 output,
378 " By severity: {} critical, {} high, {} medium, {} low",
379 summary.score_distribution.critical,
380 summary.score_distribution.high,
381 summary.score_distribution.medium,
382 summary.score_distribution.low
383 )
384 .ok();
385 writeln!(output).ok();
386}
387
388fn format_terminal_summary_mode(output: &mut String, view: &PreparedDebtView) {
389 use std::fmt::Write;
390
391 let mut tier_counts: std::collections::HashMap<String, usize> =
393 std::collections::HashMap::new();
394
395 for item in &view.items {
396 let tier = item
397 .tier()
398 .map(|t| format!("{:?}", t))
399 .unwrap_or_else(|| "Unclassified".to_string());
400 *tier_counts.entry(tier).or_insert(0) += 1;
401 }
402
403 writeln!(output, "Items by Recommendation Tier:").ok();
404 for (tier, count) in &tier_counts {
405 writeln!(output, " {}: {}", tier, count).ok();
406 }
407}
408
409fn format_terminal_items(output: &mut String, view: &PreparedDebtView, verbosity: u8) {
410 use std::fmt::Write;
411
412 writeln!(
413 output,
414 "───────────────────────────────────────────────────────────────────────────────"
415 )
416 .ok();
417 writeln!(output, "Top Priority Items:").ok();
418 writeln!(
419 output,
420 "───────────────────────────────────────────────────────────────────────────────\n"
421 )
422 .ok();
423
424 for (i, item) in view.items.iter().enumerate() {
425 format_terminal_item(output, item, i + 1, verbosity);
426 }
427}
428
429fn format_terminal_item(output: &mut String, item: &ViewItem, rank: usize, verbosity: u8) {
430 use std::fmt::Write;
431
432 let loc = item.location();
433 let severity = item.severity().as_str().to_uppercase();
434
435 writeln!(
436 output,
437 "{}. [{}] {:.1} - {}",
438 rank,
439 severity,
440 item.score(),
441 loc.file.display()
442 )
443 .ok();
444
445 if let Some(func) = &loc.function {
446 writeln!(
447 output,
448 " Function: {} (line {})",
449 func,
450 loc.line.unwrap_or(0)
451 )
452 .ok();
453 }
454
455 match item {
456 ViewItem::Function(f) => {
457 writeln!(
458 output,
459 " Complexity: cyclomatic={}, cognitive={}, nesting={}",
460 f.cyclomatic_complexity, f.cognitive_complexity, f.nesting_depth
461 )
462 .ok();
463 if verbosity >= 1 {
464 writeln!(
465 output,
466 " Recommendation: {}",
467 f.recommendation.primary_action
468 )
469 .ok();
470 }
471 }
472 ViewItem::File(f) => {
473 writeln!(
474 output,
475 " File metrics: {} lines, {} functions, avg complexity={:.1}",
476 f.metrics.total_lines, f.metrics.function_count, f.metrics.avg_complexity
477 )
478 .ok();
479 if verbosity >= 1 {
480 writeln!(output, " Recommendation: {}", f.recommendation).ok();
481 }
482 }
483 }
484
485 writeln!(output).ok();
486}
487
488#[derive(Default)]
494pub struct MarkdownConfig {
495 pub verbosity: u8,
496 pub show_filter_stats: bool,
497}
498
499pub fn format_markdown(view: &PreparedDebtView, config: &MarkdownConfig) -> String {
510 use std::fmt::Write;
511 let mut output = String::new();
512
513 writeln!(output, "# Technical Debt Analysis Report\n").ok();
515
516 format_markdown_summary(&mut output, &view.summary);
518
519 if view.is_empty() {
520 writeln!(
521 output,
522 "\n*No technical debt items found matching current thresholds.*"
523 )
524 .ok();
525 return output;
526 }
527
528 writeln!(output, "## Debt Items\n").ok();
530 for (i, item) in view.items.iter().enumerate() {
531 format_markdown_item(&mut output, item, i + 1, config.verbosity);
532 }
533
534 if config.show_filter_stats {
536 format_markdown_filter_stats(&mut output, &view.summary);
537 }
538
539 output
540}
541
542fn format_markdown_summary(output: &mut String, summary: &ViewSummary) {
543 use std::fmt::Write;
544
545 writeln!(output, "## Summary\n").ok();
546 writeln!(
547 output,
548 "**Total Debt Items:** {}\n",
549 summary.total_items_after_filter
550 )
551 .ok();
552 writeln!(output, "| Metric | Value |").ok();
553 writeln!(output, "|--------|-------|").ok();
554 writeln!(
555 output,
556 "| Total Debt Score | {:.1} |",
557 summary.total_debt_score
558 )
559 .ok();
560 writeln!(
561 output,
562 "| Debt Density | {:.2} per 1k LOC |",
563 summary.debt_density
564 )
565 .ok();
566 writeln!(
567 output,
568 "| Lines of Code | {} |",
569 summary.total_lines_of_code
570 )
571 .ok();
572 if let Some(coverage) = summary.overall_coverage {
573 writeln!(output, "| Overall Coverage | {:.1}% |", coverage * 100.0).ok();
574 }
575 writeln!(output).ok();
576
577 writeln!(output, "### Score Distribution\n").ok();
579 writeln!(output, "| Severity | Count |").ok();
580 writeln!(output, "|----------|-------|").ok();
581 writeln!(
582 output,
583 "| Critical | {} |",
584 summary.score_distribution.critical
585 )
586 .ok();
587 writeln!(output, "| High | {} |", summary.score_distribution.high).ok();
588 writeln!(output, "| Medium | {} |", summary.score_distribution.medium).ok();
589 writeln!(output, "| Low | {} |", summary.score_distribution.low).ok();
590 writeln!(output).ok();
591}
592
593fn format_markdown_item(output: &mut String, item: &ViewItem, rank: usize, verbosity: u8) {
594 use std::fmt::Write;
595
596 let loc = item.location();
597 let severity = item.severity().as_str();
598
599 writeln!(
600 output,
601 "### {}. {} (Score: {:.1})\n",
602 rank,
603 severity,
604 item.score()
605 )
606 .ok();
607 writeln!(output, "**File:** `{}`", loc.file.display()).ok();
608 if let Some(func) = &loc.function {
609 writeln!(
610 output,
611 "**Function:** `{}` (line {})",
612 func,
613 loc.line.unwrap_or(0)
614 )
615 .ok();
616 }
617 writeln!(output).ok();
618
619 match item {
620 ViewItem::Function(f) => {
621 writeln!(output, "| Metric | Value |").ok();
622 writeln!(output, "|--------|-------|").ok();
623 writeln!(
624 output,
625 "| Cyclomatic Complexity | {} |",
626 f.cyclomatic_complexity
627 )
628 .ok();
629 writeln!(
630 output,
631 "| Cognitive Complexity | {} |",
632 f.cognitive_complexity
633 )
634 .ok();
635 writeln!(output, "| Nesting Depth | {} |", f.nesting_depth).ok();
636 writeln!(output, "| Function Length | {} lines |", f.function_length).ok();
637 if let Some(cov) = f.transitive_coverage.as_ref() {
638 writeln!(output, "| Coverage | {:.1}% |", cov.direct * 100.0).ok();
639 }
640 writeln!(output).ok();
641
642 if verbosity >= 1 {
643 writeln!(
644 output,
645 "**Recommendation:** {}\n",
646 f.recommendation.primary_action
647 )
648 .ok();
649 }
650 }
651 ViewItem::File(f) => {
652 writeln!(output, "| Metric | Value |").ok();
653 writeln!(output, "|--------|-------|").ok();
654 writeln!(output, "| Total Lines | {} |", f.metrics.total_lines).ok();
655 writeln!(output, "| Function Count | {} |", f.metrics.function_count).ok();
656 writeln!(
657 output,
658 "| Avg Complexity | {:.1} |",
659 f.metrics.avg_complexity
660 )
661 .ok();
662 writeln!(output, "| Max Complexity | {} |", f.metrics.max_complexity).ok();
663 writeln!(
664 output,
665 "| Coverage | {:.1}% |",
666 f.metrics.coverage_percent * 100.0
667 )
668 .ok();
669 writeln!(output).ok();
670
671 if verbosity >= 1 {
672 writeln!(output, "**Recommendation:** {}\n", f.recommendation).ok();
673 }
674 }
675 }
676}
677
678fn format_markdown_filter_stats(output: &mut String, summary: &ViewSummary) {
679 use std::fmt::Write;
680
681 writeln!(output, "## Filtering Summary\n").ok();
682 writeln!(
683 output,
684 "- Total items analyzed: {}",
685 summary.total_items_before_filter
686 )
687 .ok();
688 writeln!(
689 output,
690 "- Items included: {}",
691 summary.total_items_after_filter
692 )
693 .ok();
694 writeln!(output, "- Filtered by score: {}", summary.filtered_by_score).ok();
695 writeln!(output, "- Filtered by tier: {}", summary.filtered_by_tier).ok();
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701 use crate::priority::call_graph::CallGraph;
702 use crate::priority::tiers::TierConfig;
703 use crate::priority::view::ViewConfig;
704 use crate::priority::view_pipeline::prepare_view;
705 use crate::priority::UnifiedAnalysis;
706
707 fn create_empty_view() -> PreparedDebtView {
708 let call_graph = CallGraph::new();
709 let analysis = UnifiedAnalysis::new(call_graph);
710 prepare_view(&analysis, &ViewConfig::default(), &TierConfig::default())
711 }
712
713 #[test]
714 fn test_format_json_empty_view() {
715 let view = create_empty_view();
716 let json = format_json(&view, false);
717 assert!(json.contains("\"format_version\": \"3.0\""));
719 assert!(json.contains("\"total_items\": 0"));
720 }
721
722 #[test]
723 fn test_format_terminal_empty_view() {
724 let view = create_empty_view();
725 let config = TerminalConfig::default();
726 let output = format_terminal(&view, &config);
727 assert!(output.contains("TECHNICAL DEBT ANALYSIS"));
728 assert!(output.contains("No technical debt items found"));
729 }
730
731 #[test]
732 fn test_format_markdown_empty_view() {
733 let view = create_empty_view();
734 let config = MarkdownConfig::default();
735 let output = format_markdown(&view, &config);
736 assert!(output.contains("# Technical Debt Analysis Report"));
737 assert!(output.contains("No technical debt items found"));
738 }
739
740 #[test]
741 fn test_to_json_output_structure() {
742 let view = create_empty_view();
743 let output = to_json_output(&view, false);
744 assert_eq!(output.format_version, "3.0");
745 assert!(output.items.is_empty());
746 }
747
748 #[test]
749 fn test_format_json_with_scoring_details() {
750 let view = create_empty_view();
751 let json_without = format_json(&view, false);
752 let json_with = format_json(&view, true);
753 assert!(json_without.contains("format_version"));
755 assert!(json_with.contains("format_version"));
756 }
757
758 #[test]
759 fn test_terminal_config_default() {
760 let config = TerminalConfig::default();
761 assert_eq!(config.verbosity, 0);
762 assert!(config.use_color);
763 assert!(!config.summary_mode);
764 }
765
766 #[test]
767 fn test_markdown_config_default() {
768 let config = MarkdownConfig::default();
769 assert_eq!(config.verbosity, 0);
770 assert!(!config.show_filter_stats);
771 }
772}