use crate::priority::formatter_verbosity::git_history::classify_stability;
use crate::priority::UnifiedDebtItem;
use crate::risk::context::ContextDetails;
use std::fmt::Write;
use super::utilities::{
extract_complexity_info, format_debt_type, format_dependency_list, format_impact,
get_severity_label,
};
pub(crate) fn format_priority_item_markdown(
output: &mut String,
rank: usize,
item: &UnifiedDebtItem,
verbosity: u8,
) {
format_header(output, rank, item, verbosity);
format_location_and_impact(output, item);
format_dependencies(output, item, verbosity);
if verbosity >= 1 {
format_git_context_section(output, item);
}
writeln!(output, "\n**Why:** {}", item.recommendation.rationale).unwrap();
if verbosity >= 2 {
if let Some(context) = &item.context_suggestion {
format_context_suggestion(output, context);
}
}
}
fn format_header(output: &mut String, rank: usize, item: &UnifiedDebtItem, verbosity: u8) {
let severity = get_severity_label(item.unified_score.final_score);
let tier_label = item
.tier
.as_ref()
.map(|t| format!("[{}] ", t.short_label()))
.unwrap_or_default();
writeln!(
output,
"### #{} {}Score: {:.1} [{}]",
rank, tier_label, item.unified_score.final_score, severity
)
.unwrap();
if verbosity >= 2 {
output.push_str(&format_score_breakdown_with_coverage(
&item.unified_score,
item.transitive_coverage.as_ref(),
));
} else if verbosity >= 1 {
output.push_str(&format_main_factors_with_coverage(
&item.unified_score,
&item.debt_type,
item.transitive_coverage.as_ref(),
));
}
}
fn format_location_and_impact(output: &mut String, item: &UnifiedDebtItem) {
writeln!(
output,
"**Type:** {} | **Location:** `{}:{} {}()`",
format_debt_type(&item.debt_type),
item.location.file.display(),
item.location.line,
item.location.function
)
.unwrap();
writeln!(output, "**Action:** {}", item.recommendation.primary_action).unwrap();
writeln!(
output,
"**Impact:** {}",
format_impact(&item.expected_impact)
)
.unwrap();
if let Some(complexity) = extract_complexity_info(&item.debt_type) {
writeln!(output, "**Complexity:** {}", complexity).unwrap();
}
}
fn format_dependencies(output: &mut String, item: &UnifiedDebtItem, verbosity: u8) {
if verbosity < 1 {
return;
}
writeln!(output, "\n#### Dependencies").unwrap();
writeln!(
output,
"- **Upstream:** {} | **Downstream:** {}",
item.upstream_dependencies, item.downstream_dependencies
)
.unwrap();
if verbosity >= 2 {
if !item.upstream_callers.is_empty() {
let caller_info = format_dependency_list(&item.upstream_callers, 3, "Called by");
if !caller_info.is_empty() {
writeln!(output, "{}", caller_info).unwrap();
}
}
if !item.downstream_callees.is_empty() {
let callee_info = format_dependency_list(&item.downstream_callees, 3, "Calls");
if !callee_info.is_empty() {
writeln!(output, "{}", callee_info).unwrap();
}
}
}
}
fn format_git_context_section(output: &mut String, item: &UnifiedDebtItem) {
let Some(ref contextual_risk) = item.contextual_risk else {
return;
};
let git_context = contextual_risk
.contexts
.iter()
.find(|ctx| ctx.provider == "git_history");
let Some(ctx) = git_context else {
return;
};
let ContextDetails::Historical {
change_frequency,
bug_density: _,
age_days,
author_count,
total_commits,
bug_fix_count,
} = &ctx.details
else {
return;
};
writeln!(output, "\n#### Git Context").unwrap();
let activity = if *total_commits == 0 {
"0 commits".to_string()
} else {
format!(
"{} commit{} ({:.2}/month)",
total_commits,
if *total_commits == 1 { "" } else { "s" },
change_frequency
)
};
writeln!(output, "- **Activity:** {}", activity).unwrap();
let stability = classify_stability(*change_frequency);
writeln!(output, "- **Stability:** {}", stability).unwrap();
let changes = total_commits.saturating_sub(1);
let fix_rate = if changes == 0 {
"no changes since intro".to_string()
} else {
format!(
"{} fix{} / {} change{}",
bug_fix_count,
if *bug_fix_count == 1 { "" } else { "es" },
changes,
if changes == 1 { "" } else { "s" }
)
};
writeln!(output, "- **Fix Rate:** {}", fix_rate).unwrap();
writeln!(output, "- **Age:** {} days", age_days).unwrap();
writeln!(output, "- **Contributors:** {}", author_count).unwrap();
let multiplier = if contextual_risk.base_risk > 0.0 {
contextual_risk.contextual_risk / contextual_risk.base_risk
} else {
1.0
};
writeln!(
output,
"- **Risk:** {:.1} → {:.1} ({:.2}x)",
contextual_risk.base_risk, contextual_risk.contextual_risk, multiplier
)
.unwrap();
}
fn format_context_suggestion(
output: &mut String,
context: &crate::priority::context::ContextSuggestion,
) {
writeln!(
output,
"\n#### Context to Read ({} lines, {:.0}% confidence)",
context.total_lines,
context.completeness_confidence * 100.0
)
.unwrap();
writeln!(
output,
"**Primary:** {}:{}-{} {}",
context.primary.file.display(),
context.primary.start_line,
context.primary.end_line,
context
.primary
.symbol
.as_ref()
.map(|s| format!("({})", s))
.unwrap_or_default()
)
.unwrap();
if !context.related.is_empty() {
writeln!(output, "\n**Related:**").unwrap();
for rel in &context.related {
writeln!(
output,
"- {}:{}-{} ({}) - {}",
rel.range.file.display(),
rel.range.start_line,
rel.range.end_line,
rel.relationship,
rel.reason
)
.unwrap();
}
}
}
pub(crate) fn format_score_breakdown_with_coverage(
unified_score: &crate::priority::UnifiedScore,
transitive_coverage: Option<&crate::priority::coverage_propagation::TransitiveCoverage>,
) -> String {
let weights = crate::config::get_scoring_weights();
let mut output = String::new();
writeln!(&mut output, "\n#### Score Calculation\n").unwrap();
writeln!(
&mut output,
"| Component | Value | Weight | Contribution | Details |"
)
.unwrap();
writeln!(
&mut output,
"|-----------|-------|--------|--------------|----------|"
)
.unwrap();
writeln!(
&mut output,
"| Complexity | {:.1} | {:.0}% | {:.2} | |",
unified_score.complexity_factor,
weights.complexity * 100.0,
unified_score.complexity_factor * weights.complexity
)
.unwrap();
let coverage_details = if let Some(trans_cov) = transitive_coverage {
format!("Line: {:.2}%", trans_cov.direct * 100.0)
} else {
"No data".to_string()
};
writeln!(
&mut output,
"| Coverage | {:.1} | {:.0}% | {:.2} | {} |",
unified_score.coverage_factor,
weights.coverage * 100.0,
unified_score.coverage_factor * weights.coverage,
coverage_details
)
.unwrap();
writeln!(
&mut output,
"| Dependency | {:.1} | {:.0}% | {:.2} | |",
unified_score.dependency_factor,
weights.dependency * 100.0,
unified_score.dependency_factor * weights.dependency
)
.unwrap();
let base_score = unified_score.complexity_factor * weights.complexity
+ unified_score.coverage_factor * weights.coverage
+ unified_score.dependency_factor * weights.dependency;
writeln!(&mut output).unwrap();
writeln!(&mut output, "- **Base Score:** {:.2}", base_score).unwrap();
writeln!(
&mut output,
"- **Role Adjustment:** ×{:.2}",
unified_score.role_multiplier
)
.unwrap();
writeln!(
&mut output,
"- **Final Score:** {:.2}",
unified_score.final_score
)
.unwrap();
writeln!(&mut output).unwrap();
output
}
fn coverage_factor(
transitive_coverage: Option<&crate::priority::coverage_propagation::TransitiveCoverage>,
coverage_factor_score: f64,
coverage_weight: f64,
) -> Option<String> {
match transitive_coverage {
Some(trans_cov) => {
let pct = trans_cov.direct * 100.0;
if pct >= 95.0 {
Some(format!("Excellent coverage {:.1}%", pct))
} else if pct >= 80.0 {
Some(format!("Good coverage {:.1}%", pct))
} else if coverage_factor_score > 3.0 {
Some(format!(
"Line coverage {:.1}% (weight: {:.0}%)",
pct,
coverage_weight * 100.0
))
} else {
None
}
}
None if coverage_factor_score > 3.0 => Some(format!(
"No coverage data (weight: {:.0}%)",
coverage_weight * 100.0
)),
None => None,
}
}
fn complexity_factor(complexity_score: f64, complexity_weight: f64) -> Option<String> {
if complexity_score > 5.0 {
Some(format!(
"Complexity (weight: {:.0}%)",
complexity_weight * 100.0
))
} else if complexity_score > 3.0 {
Some("Moderate complexity".to_string())
} else {
None
}
}
fn dependency_factor(dependency_score: f64, dependency_weight: f64) -> Option<String> {
if dependency_score > 5.0 {
Some(format!(
"Critical path (weight: {:.0}%)",
dependency_weight * 100.0
))
} else {
None
}
}
fn debt_type_factors(debt_type: &crate::priority::DebtType) -> Vec<String> {
match debt_type {
crate::priority::DebtType::NestedLoops { depth, .. } => {
vec![
"Complexity impact (High)".to_string(),
format!("{} level nested loops", depth),
]
}
crate::priority::DebtType::BlockingIO { operation, .. } => {
vec![
"Resource management issue".to_string(),
format!("Blocking {}", operation),
]
}
crate::priority::DebtType::AllocationInefficiency { pattern, .. } => {
vec![
"Resource management issue".to_string(),
format!("Allocation: {}", pattern),
]
}
_ => vec![],
}
}
pub(crate) fn format_main_factors_with_coverage(
unified_score: &crate::priority::UnifiedScore,
debt_type: &crate::priority::DebtType,
transitive_coverage: Option<&crate::priority::coverage_propagation::TransitiveCoverage>,
) -> String {
let weights = crate::config::get_scoring_weights();
let factors: Vec<String> = [
coverage_factor(
transitive_coverage,
unified_score.coverage_factor,
weights.coverage,
),
complexity_factor(unified_score.complexity_factor, weights.complexity),
dependency_factor(unified_score.dependency_factor, weights.dependency),
]
.into_iter()
.flatten()
.chain(debt_type_factors(debt_type))
.collect();
if factors.is_empty() {
String::new()
} else {
format!("*Main factors: {}*\n", factors.join(", "))
}
}