use crate::util::truncate_string;
use hotspots_core::delta::Delta;
use hotspots_core::policy::{PolicyResult, PolicyResults};
use std::fmt::Write;
pub(crate) fn render_policy_text_output(
delta: &Delta,
policy_results: &PolicyResults,
) -> anyhow::Result<String> {
let mut out = String::new();
writeln!(out, "Policy Evaluation Results")?;
writeln!(out, "{}", "=".repeat(80))?;
write_failing_functions_section(&mut out, delta, policy_results)?;
write_threshold_warning_section(
&mut out,
"Watch Level (approaching moderate threshold)",
"watch-threshold",
delta,
&policy_results.warnings,
)?;
write_threshold_warning_section(
&mut out,
"Attention Level (approaching high threshold)",
"attention-threshold",
delta,
&policy_results.warnings,
)?;
write_rapid_growth_section(&mut out, delta, &policy_results.warnings)?;
write_repo_warnings_section(&mut out, &policy_results.warnings)?;
write_co_change_delta_section(&mut out, delta)?;
write_policy_summary(&mut out, policy_results)?;
Ok(out)
}
pub(crate) fn print_policy_text_output(
delta: &Delta,
policy_results: &PolicyResults,
) -> anyhow::Result<()> {
print!("{}", render_policy_text_output(delta, policy_results)?);
Ok(())
}
fn write_co_change_delta_section(out: &mut String, delta: &Delta) -> anyhow::Result<()> {
let co_change_delta = match delta.aggregates.as_ref().map(|a| &a.co_change_delta) {
Some(d) if !d.is_empty() => d,
_ => return Ok(()),
};
let touched: std::collections::HashSet<String> = delta
.deltas
.iter()
.filter_map(|e| {
e.function_id
.rfind("::")
.map(|pos| e.function_id[..pos].to_string())
})
.collect();
let relevant: Vec<&hotspots_core::aggregates::CoChangeDeltaEntry> = co_change_delta
.iter()
.filter(|e| {
e.status != "dropped" && (touched.contains(&e.file_a) || touched.contains(&e.file_b))
})
.collect();
if relevant.is_empty() {
return Ok(());
}
writeln!(out, "\nCo-Change Coupling (files touched in this delta):")?;
writeln!(out, "{}", "-".repeat(80))?;
for entry in &relevant {
let risk = entry.curr_risk.as_deref().unwrap_or("unknown");
let dep_tag = if entry.has_static_dep {
" [expected]"
} else {
""
};
writeln!(
out,
" {} ↔ {} [{}{}] co-changed {:.0}% of the time",
entry.file_a,
entry.file_b,
risk,
dep_tag,
entry.coupling_ratio * 100.0,
)?;
}
Ok(())
}
fn write_failing_functions_section(
out: &mut String,
delta: &Delta,
policy_results: &PolicyResults,
) -> anyhow::Result<()> {
if policy_results.failed.is_empty() {
return Ok(());
}
writeln!(out, "\nPolicy failures:")?;
for result in &policy_results.failed {
if let Some(ref function_id) = result.function_id {
writeln!(out, "- {}: {}", result.id.as_str(), function_id)?;
} else {
writeln!(out, "- {}", result.id.as_str())?;
}
}
writeln!(out, "\nViolating functions:")?;
writeln!(
out,
"{:<40} {:<12} {:<12} {:<10} {:<20}",
"Function", "Before", "After", "ΔLRS", "Policy"
)?;
writeln!(out, "{}", "-".repeat(94))?;
let violating_ids: std::collections::HashSet<&str> = policy_results
.failed
.iter()
.filter_map(|r| r.function_id.as_deref())
.collect();
for entry in &delta.deltas {
if !violating_ids.contains(entry.function_id.as_str()) {
continue;
}
let before_band = entry
.before
.as_ref()
.map(|b| b.band.as_str())
.unwrap_or("N/A");
let after_band = entry
.after
.as_ref()
.map(|a| a.band.as_str())
.unwrap_or("N/A");
let delta_lrs = entry
.delta
.as_ref()
.map(|d| format!("{:.2}", d.lrs))
.unwrap_or_else(|| "N/A".to_string());
let policies: Vec<&str> = policy_results
.failed
.iter()
.filter(|r| r.function_id.as_deref() == Some(entry.function_id.as_str()))
.map(|r| r.id.as_str())
.collect();
writeln!(
out,
"{:<40} {:<12} {:<12} {:<10} {:<20}",
truncate_string(&entry.function_id, 40),
before_band,
after_band,
delta_lrs,
policies.join(", ")
)?;
}
Ok(())
}
fn write_threshold_warning_section(
out: &mut String,
header: &str,
policy_id: &str,
delta: &Delta,
warnings: &[PolicyResult],
) -> anyhow::Result<()> {
let group: Vec<_> = warnings
.iter()
.filter(|r| r.id.as_str() == policy_id)
.collect();
if group.is_empty() {
return Ok(());
}
writeln!(out, "\n{}:", header)?;
writeln!(
out,
"{:<40} {:<12} {:<12}",
"Function", "Current LRS", "Band"
)?;
writeln!(out, "{}", "-".repeat(64))?;
for warning in group {
if let Some(function_id) = &warning.function_id {
if let Some(entry) = delta.deltas.iter().find(|e| &e.function_id == function_id) {
let after_lrs = entry
.after
.as_ref()
.map(|a| format!("{:.2}", a.lrs))
.unwrap_or_else(|| "N/A".to_string());
let after_band = entry
.after
.as_ref()
.map(|a| a.band.as_str())
.unwrap_or("N/A");
writeln!(
out,
"{:<40} {:<12} {:<12}",
truncate_string(function_id, 40),
after_lrs,
after_band
)?;
}
}
}
Ok(())
}
fn write_rapid_growth_section(
out: &mut String,
delta: &Delta,
warnings: &[PolicyResult],
) -> anyhow::Result<()> {
let group: Vec<_> = warnings
.iter()
.filter(|r| r.id.as_str() == "rapid-growth")
.collect();
if group.is_empty() {
return Ok(());
}
writeln!(out, "\nRapid Growth (significant LRS increase):")?;
writeln!(
out,
"{:<40} {:<12} {:<12} {:<12}",
"Function", "Current LRS", "Delta", "Growth"
)?;
writeln!(out, "{}", "-".repeat(76))?;
for warning in group {
if let Some(function_id) = &warning.function_id {
if let Some(entry) = delta.deltas.iter().find(|e| &e.function_id == function_id) {
let after_lrs = entry
.after
.as_ref()
.map(|a| format!("{:.2}", a.lrs))
.unwrap_or_else(|| "N/A".to_string());
let delta_lrs = entry
.delta
.as_ref()
.map(|d| format!("{:+.2}", d.lrs))
.unwrap_or_else(|| "N/A".to_string());
let growth_pct = warning
.metadata
.as_ref()
.and_then(|m| m.growth_percent)
.map(|g| format!("{:+.0}%", g))
.unwrap_or_else(|| "N/A".to_string());
writeln!(
out,
"{:<40} {:<12} {:<12} {:<12}",
truncate_string(function_id, 40),
after_lrs,
delta_lrs,
growth_pct
)?;
}
}
}
Ok(())
}
fn write_repo_warnings_section(out: &mut String, warnings: &[PolicyResult]) -> anyhow::Result<()> {
let group: Vec<_> = warnings
.iter()
.filter(|r| r.id.as_str() == "net-repo-regression")
.collect();
if group.is_empty() {
return Ok(());
}
writeln!(out, "\nRepository-Level Warnings:")?;
for warning in group {
writeln!(out, "- {}", warning.message)?;
}
Ok(())
}
fn write_policy_summary(out: &mut String, policy_results: &PolicyResults) -> anyhow::Result<()> {
if policy_results.failed.is_empty() && policy_results.warnings.is_empty() {
writeln!(out, "\nNo policy violations detected.")?;
return Ok(());
}
writeln!(out, "\nSummary:")?;
writeln!(out, " Blocking failures: {}", policy_results.failed.len())?;
let watch_count = policy_results
.warnings
.iter()
.filter(|r| r.id.as_str() == "watch-threshold")
.count();
let attention_count = policy_results
.warnings
.iter()
.filter(|r| r.id.as_str() == "attention-threshold")
.count();
let rapid_growth_count = policy_results
.warnings
.iter()
.filter(|r| r.id.as_str() == "rapid-growth")
.count();
let other_count =
policy_results.warnings.len() - watch_count - attention_count - rapid_growth_count;
if watch_count > 0 {
writeln!(out, " Watch warnings: {watch_count}")?;
}
if attention_count > 0 {
writeln!(out, " Attention warnings: {attention_count}")?;
}
if rapid_growth_count > 0 {
writeln!(out, " Rapid growth warnings: {rapid_growth_count}")?;
}
if other_count > 0 {
writeln!(out, " Other warnings: {other_count}")?;
}
Ok(())
}