Skip to main content

debtmap/priority/formatter_verbosity/
sections.rs

1use crate::complexity::EntropyAnalysis;
2use crate::priority::UnifiedDebtItem;
3use colored::*;
4use std::fmt::Write;
5
6/// Pure function to classify dependency contribution
7pub fn classify_dependency_contribution(dependency_factor: f64) -> &'static str {
8    match dependency_factor {
9        d if d > 10.0 => "CRITICAL PATH",
10        d if d > 5.0 => "HIGH",
11        d if d > 2.0 => "MEDIUM",
12        _ => "LOW",
13    }
14}
15
16/// Pure function to format callers display
17pub fn format_callers_display(callers: &[String], max_display: usize) -> String {
18    if callers.len() <= max_display {
19        callers.join(", ")
20    } else {
21        format!(
22            "{}, ... ({} more)",
23            callers[..max_display].join(", "),
24            callers.len() - max_display
25        )
26    }
27}
28
29/// Pure function to format callees display
30pub fn format_callees_display(callees: &[String], max_display: usize) -> String {
31    if callees.len() <= max_display {
32        callees.join(", ")
33    } else {
34        format!(
35            "{}, ... ({} more)",
36            callees[..max_display].join(", "),
37            callees.len() - max_display
38        )
39    }
40}
41
42/// Pure function to calculate score factors
43pub struct ScoreFactors {
44    pub coverage_gap: f64,
45    pub coverage_pct: f64,
46    pub coverage_factor: f64,
47    pub complexity_factor: f64,
48    pub dependency_factor: f64,
49}
50
51pub fn calculate_score_factors(item: &UnifiedDebtItem) -> ScoreFactors {
52    let (coverage_gap, coverage_pct) = if let Some(ref trans_cov) = item.transitive_coverage {
53        let pct = trans_cov.direct;
54        (1.0 - pct, pct)
55    } else {
56        (1.0, 0.0)
57    };
58
59    ScoreFactors {
60        coverage_gap,
61        coverage_pct,
62        coverage_factor: (coverage_gap.powf(1.5) + 0.1).max(0.1),
63        complexity_factor: item.unified_score.complexity_factor,
64        dependency_factor: ((item.unified_score.dependency_factor + 1.0).sqrt() / 2.0).min(1.0),
65    }
66}
67
68/// Pure function to format coverage detail string
69pub fn format_coverage_detail(has_coverage: bool, gap: f64, pct: f64) -> String {
70    if has_coverage {
71        format!(" (gap: {:.1}%, coverage: {:.1}%)", gap * 100.0, pct * 100.0)
72    } else {
73        " (no coverage data)".to_string()
74    }
75}
76
77/// Pure function to format complexity detail
78pub fn format_complexity_detail(entropy: &Option<EntropyAnalysis>) -> String {
79    if let Some(e) = entropy {
80        format!(" (entropy-adjusted from {})", e.original_complexity)
81    } else {
82        String::new()
83    }
84}
85
86/// Format score calculation section for verbosity >= 2
87pub fn format_score_calculation_section(
88    item: &UnifiedDebtItem,
89    _formatter: &crate::formatting::ColoredFormatter,
90) -> Vec<String> {
91    let mut lines = Vec::new();
92    let tree_branch = "-";
93    let tree_sub_branch = "  -";
94    let tree_pipe = " ";
95
96    lines.push(format!(
97        "{} {}",
98        tree_branch,
99        "SCORE CALCULATION:".bright_blue()
100    ));
101    lines.push(format!("{} Weighted Sum Model:", tree_sub_branch));
102
103    // Calculate multiplicative factors for display
104    let factors = calculate_score_factors(item);
105    let coverage_detail = format_coverage_detail(
106        item.transitive_coverage.is_some(),
107        factors.coverage_gap,
108        factors.coverage_pct,
109    );
110
111    // Add role-based coverage adjustment indicator for entry points
112    let role_coverage_indicator = if matches!(
113        item.function_role,
114        crate::priority::FunctionRole::EntryPoint
115    ) {
116        " (entry point - integration tested, lower unit coverage expected)"
117    } else {
118        ""
119    };
120
121    lines.push(format!(
122        "{}  {} Coverage Score: {:.1} × 40% = {:.2}{}{}",
123        tree_pipe,
124        "-",
125        factors.coverage_factor * 10.0, // Convert to 0-100 scale
126        factors.coverage_factor * 10.0 * 0.4,
127        coverage_detail,
128        role_coverage_indicator
129    ));
130
131    // Show complexity score
132    let complexity_detail = format_complexity_detail(&item.entropy_analysis);
133    lines.push(format!(
134        "{}  {} Complexity Score: {:.1} × 40% = {:.2}{}",
135        tree_pipe,
136        "-",
137        factors.complexity_factor * 10.0, // Convert to 0-100 scale
138        factors.complexity_factor * 10.0 * 0.40,
139        complexity_detail
140    ));
141
142    // Show dependency score
143    lines.push(format!(
144        "{}  {} Dependency Score: {:.1} × 20% = {:.2} ({} callers)",
145        tree_pipe,
146        "-",
147        factors.dependency_factor * 10.0, // Convert to 0-100 scale
148        factors.dependency_factor * 10.0 * 0.20,
149        item.upstream_callers.len() // Display actual caller count, not normalized score
150    ));
151
152    // Calculate weighted sum base score
153    let coverage_contribution = factors.coverage_factor * 10.0 * 0.4;
154    let complexity_contribution = factors.complexity_factor * 10.0 * 0.4;
155    let dependency_contribution = factors.dependency_factor * 10.0 * 0.2;
156    let base_score = coverage_contribution + complexity_contribution + dependency_contribution;
157
158    lines.push(format!(
159        "{}  {} Base Score: {:.2} + {:.2} + {:.2} = {:.2}",
160        tree_pipe,
161        "-",
162        coverage_contribution,
163        complexity_contribution,
164        dependency_contribution,
165        base_score
166    ));
167
168    // Show entropy impact if present
169    if let Some(ref entropy) = item.entropy_analysis {
170        lines.push(format!(
171            "{}  {} Entropy Impact: {:.0}% dampening (entropy: {:.2}, repetition: {:.0}%)",
172            tree_pipe,
173            "-",
174            (1.0 - entropy.dampening_factor) * 100.0,
175            entropy.entropy_score,
176            entropy.pattern_repetition * 100.0
177        ));
178    }
179
180    lines.push(format!(
181        "{}  {} Role Adjustment: ×{:.2}",
182        tree_pipe, "-", item.unified_score.role_multiplier
183    ));
184
185    lines.push(format!(
186        "{}  {} Final Score: {:.2}",
187        tree_pipe, "-", item.unified_score.final_score
188    ));
189
190    lines
191}
192
193/// Format call graph section for verbosity >= 2
194pub fn format_call_graph_section(
195    item: &UnifiedDebtItem,
196    _formatter: &crate::formatting::ColoredFormatter,
197) -> Vec<String> {
198    let mut lines = Vec::new();
199    let tree_pipe = " ";
200
201    if !item.upstream_callers.is_empty() || !item.downstream_callees.is_empty() {
202        lines.push(format!("{} {}", "-", "CALL GRAPH:".bright_blue()));
203
204        if !item.upstream_callers.is_empty() {
205            let callers = format_callers_display(&item.upstream_callers, 5);
206            lines.push(format!("{}  {} Called by: {}", tree_pipe, "-", callers));
207        }
208
209        if !item.downstream_callees.is_empty() {
210            let callees = format_callees_display(&item.downstream_callees, 5);
211            lines.push(format!("{}  {} Calls: {}", tree_pipe, "-", callees));
212        } else if !item.upstream_callers.is_empty() {
213            // Change the last caller line to use └─ if there are no callees
214            lines.push(format!(
215                "{}  {} Dependencies: {} upstream, {} downstream",
216                tree_pipe, "-", item.upstream_dependencies, item.downstream_dependencies
217            ));
218        }
219    }
220
221    lines
222}
223
224/// Format basic call graph info for verbosity level 0
225pub fn format_basic_call_graph(
226    output: &mut String,
227    item: &UnifiedDebtItem,
228    _formatter: &crate::formatting::ColoredFormatter,
229) {
230    let caller_count = item.upstream_callers.len();
231    let callee_count = item.downstream_callees.len();
232
233    // Only show if there's interesting call graph info
234    if caller_count > 0 || callee_count > 0 {
235        writeln!(
236            output,
237            "- {} {} caller{}, {} callee{}",
238            "CALLS:".bright_blue(),
239            caller_count,
240            if caller_count == 1 { "" } else { "s" },
241            callee_count,
242            if callee_count == 1 { "" } else { "s" }
243        )
244        .unwrap();
245
246        // Show if function is potentially dead code (no callers)
247        if caller_count == 0 && callee_count > 0 {
248            writeln!(
249                output,
250                "    ! {}",
251                "No callers detected - may be dead code".yellow()
252            )
253            .unwrap();
254        }
255    }
256}
257
258/// Format implementation steps
259pub fn format_implementation_steps(
260    output: &mut String,
261    steps: &[String],
262    _formatter: &crate::formatting::ColoredFormatter,
263) {
264    if !steps.is_empty() {
265        for (i, step) in steps.iter().enumerate() {
266            let prefix = "   -";
267            writeln!(
268                output,
269                "{} {}. {}",
270                prefix,
271                (i + 1).to_string().cyan(),
272                step.bright_white()
273            )
274            .unwrap();
275        }
276    }
277}
278
279/// Format dependencies summary
280pub fn format_dependencies_summary(
281    output: &mut String,
282    item: &UnifiedDebtItem,
283    _formatter: &crate::formatting::ColoredFormatter,
284    tree_pipe: &str,
285) {
286    let (upstream, downstream) = crate::priority::formatter::extract_dependency_info(item);
287
288    if upstream > 0 || downstream > 0 {
289        writeln!(
290            output,
291            "- {} {} upstream, {} downstream",
292            "DEPENDENCIES:".bright_blue(),
293            upstream.to_string().cyan(),
294            downstream.to_string().cyan()
295        )
296        .unwrap();
297
298        if !item.upstream_callers.is_empty() {
299            let callers_display = format_callers_display(&item.upstream_callers, 3);
300            writeln!(
301                output,
302                "{}  - CALLERS: {}",
303                tree_pipe,
304                callers_display.cyan()
305            )
306            .unwrap();
307        }
308
309        if !item.downstream_callees.is_empty() {
310            let callees_display = format_callees_display(&item.downstream_callees, 3);
311            writeln!(
312                output,
313                "{}  - CALLS: {}",
314                tree_pipe,
315                callees_display.bright_magenta()
316            )
317            .unwrap();
318        }
319    }
320}
321
322/// Format scoring breakdown for verbosity 1
323pub fn format_scoring_breakdown(
324    output: &mut String,
325    item: &UnifiedDebtItem,
326    _formatter: &crate::formatting::ColoredFormatter,
327) {
328    use super::complexity::classify_complexity_contribution;
329    use super::coverage::classify_coverage_contribution;
330
331    let coverage_contribution = classify_coverage_contribution(item);
332    let complexity_contribution =
333        classify_complexity_contribution(item.unified_score.complexity_factor);
334    let dependency_contribution =
335        classify_dependency_contribution(item.unified_score.dependency_factor);
336
337    writeln!(
338        output,
339        "- {} Coverage: {} | Complexity: {} | Dependencies: {}",
340        "SCORING:".bright_blue(),
341        coverage_contribution.bright_yellow(),
342        complexity_contribution.bright_yellow(),
343        dependency_contribution.bright_yellow()
344    )
345    .unwrap();
346
347    // Add file context information (spec 181: show context in verbose mode)
348    if let Some(ref context) = item.file_context {
349        use crate::priority::scoring::file_context_scoring::{
350            context_label, context_reduction_factor,
351        };
352
353        let factor = context_reduction_factor(context);
354        let label = context_label(context);
355
356        // Determine explanation based on context type
357        let explanation = if factor >= 1.0 {
358            "no score adjustment"
359        } else if factor >= 0.6 {
360            "40% score reduction"
361        } else if factor >= 0.2 {
362            "80% score reduction"
363        } else {
364            "90% score reduction"
365        };
366
367        writeln!(
368            output,
369            "  - {} {} ({})",
370            "File Context:".bright_blue(),
371            label.bright_magenta(),
372            explanation
373        )
374        .unwrap();
375
376        writeln!(
377            output,
378            "  - {} {:.2}",
379            "Context Factor:".bright_blue(),
380            factor
381        )
382        .unwrap();
383    }
384}
385
386/// Format related items section
387pub fn format_related_items(
388    output: &mut String,
389    related_items: &[String],
390    _formatter: &crate::formatting::ColoredFormatter,
391) {
392    if !related_items.is_empty() {
393        writeln!(
394            output,
395            "- {} {} related items to address:",
396            "RELATED:".bright_blue(),
397            related_items.len().to_string().cyan()
398        )
399        .unwrap();
400
401        for related in related_items.iter() {
402            let prefix = "   -";
403            writeln!(output, "{} {}", prefix, related.bright_magenta()).unwrap();
404        }
405    }
406}
407
408/// Format pattern analysis section if available (spec 151)
409pub fn format_pattern_analysis(output: &mut String, item: &UnifiedDebtItem, verbosity: u8) {
410    // Only show pattern analysis if verbosity >= 1 and patterns are available
411    if verbosity < 1 {
412        return;
413    }
414
415    if let Some(ref pattern_analysis) = item.pattern_analysis
416        && pattern_analysis.has_patterns()
417    {
418        writeln!(output, "├─ {}", "PATTERN ANALYSIS:".bright_blue()).unwrap();
419
420        // Use PatternFormatter to format the analysis
421        let formatted =
422            crate::output::pattern_formatter::PatternFormatter::format(pattern_analysis);
423
424        // Indent each line for proper tree formatting
425        for line in formatted.lines() {
426            if !line.is_empty() {
427                writeln!(output, "│  {}", line).unwrap();
428            }
429        }
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_classify_dependency_contribution() {
439        assert_eq!(classify_dependency_contribution(15.0), "CRITICAL PATH");
440        assert_eq!(classify_dependency_contribution(10.1), "CRITICAL PATH");
441        assert_eq!(classify_dependency_contribution(10.0), "HIGH");
442        assert_eq!(classify_dependency_contribution(7.0), "HIGH");
443        assert_eq!(classify_dependency_contribution(5.1), "HIGH");
444        assert_eq!(classify_dependency_contribution(5.0), "MEDIUM");
445        assert_eq!(classify_dependency_contribution(3.0), "MEDIUM");
446        assert_eq!(classify_dependency_contribution(2.1), "MEDIUM");
447        assert_eq!(classify_dependency_contribution(2.0), "LOW");
448        assert_eq!(classify_dependency_contribution(0.0), "LOW");
449    }
450}