impl RichReporter {
pub fn render_text(&self) -> String {
let mut output = String::new();
let box_drawer = BoxDrawer::default();
let width = self.config.width;
writeln!(
output,
"{}",
box_drawer.draw_box(
&[
&self.report.title,
&format!(
"{} | {} | {}",
self.report.timestamp,
self.report.project,
self.report.andon_status.display()
),
],
width - 4
)
)
.ok();
writeln!(output).ok();
writeln!(output, "Summary").ok();
writeln!(output, "{}", "\u{2501}".repeat(width)).ok();
let progress = ProgressBar::new(20);
writeln!(
output,
"Quality Score: {} {:.1}%",
progress.render(self.report.quality_score / 100.0),
self.report.quality_score
)
.ok();
let counts = self.report.findings_by_severity();
writeln!(
output,
"Findings: {} total ({} critical, {} high, {} medium, {} low)",
self.report.findings.len(),
counts.get(&Severity::Critical).unwrap_or(&0),
counts.get(&Severity::High).unwrap_or(&0),
counts.get(&Severity::Medium).unwrap_or(&0),
counts.get(&Severity::Low).unwrap_or(&0),
)
.ok();
if !self.report.trends.is_empty() {
writeln!(output).ok();
let sparkline = Sparkline::default();
for trend in &self.report.trends {
writeln!(
output,
"{}: {} {} ({:+.1}%)",
trend.name,
sparkline.render(&trend.sparkline),
trend.direction.arrow(),
trend.change_percent
)
.ok();
}
}
writeln!(output).ok();
if !self.report.clusters.is_empty() {
writeln!(output, "Defect Clusters (K-Means)").ok();
writeln!(output, "{}", "\u{2501}".repeat(width)).ok();
for cluster in &self.report.clusters {
writeln!(
output,
"{}",
TreeRenderer::branch(&format!(
"Cluster {}: {} ({} items, cohesion: {:.0}%)",
cluster.id,
cluster.primary_category,
cluster.size,
cluster.cohesion * 100.0
))
)
.ok();
for (i, finding_id) in cluster.finding_ids.iter().take(3).enumerate() {
if let Some(finding) = self.report.findings.iter().find(|f| &f.id == finding_id)
{
let prefix = if i == cluster.finding_ids.len().min(3) - 1 {
TreeRenderer::last_branch
} else {
TreeRenderer::branch
};
writeln!(
output,
" {}",
prefix(&format!(
"{} {} - {}",
finding.severity.indicator(),
finding.location,
finding.message.chars().take(40).collect::<String>()
))
)
.ok();
}
}
if cluster.finding_ids.len() > 3 {
writeln!(
output,
" {}",
TreeRenderer::last_branch(&format!(
"... and {} more",
cluster.finding_ids.len() - 3
))
)
.ok();
}
}
writeln!(output).ok();
}
let high_pagerank: Vec<_> = self
.report
.findings
.iter()
.filter(|f| f.pagerank.is_some())
.collect();
if !high_pagerank.is_empty() {
writeln!(output, "Defect Centrality (PageRank)").ok();
writeln!(output, "{}", "\u{2501}".repeat(width)).ok();
let table = TableRenderer::new(vec![4, 8, 30, 20])
.with_alignments(vec![true, true, false, false]);
writeln!(
output,
"{}",
table.render_header(&["Rank", "Score", "Location", "Category"])
)
.ok();
let mut sorted = high_pagerank.clone();
sorted.sort_by(|a, b| {
b.pagerank
.unwrap_or(0.0)
.partial_cmp(&a.pagerank.unwrap_or(0.0))
.expect("internal error")
});
for (i, finding) in sorted.iter().take(5).enumerate() {
writeln!(
output,
"{}",
table.render_row(&[
&format!("{}", i + 1),
&format!("{:.3}", finding.pagerank.unwrap_or(0.0)),
&finding.location.to_string(),
&finding.category,
])
)
.ok();
}
writeln!(output, "{}", table.render_footer()).ok();
writeln!(output).ok();
}
if !self.report.communities.is_empty() {
writeln!(output, "Code Communities (Louvain)").ok();
writeln!(output, "{}", "\u{2501}".repeat(width)).ok();
for community in &self.report.communities {
writeln!(
output,
"{}",
box_drawer.draw_box(
&[
&format!(
"{} ({} files, {} defects)",
community.name,
community.files.len(),
community.defect_count
),
&format!(
"Primary issue: {}",
community.primary_issue.as_deref().unwrap_or("None")
),
],
width - 10
)
)
.ok();
}
writeln!(output).ok();
}
if !self.report.anomalies.is_empty() {
writeln!(output, "Anomalies Detected (Isolation Forest)").ok();
writeln!(output, "{}", "\u{2501}".repeat(width)).ok();
let anomaly_bar = ProgressBar::new(30);
for anomaly in &self.report.anomalies {
writeln!(
output,
"{} {} (score: {:.2})",
StatusIndicator::warning(),
anomaly.finding_id,
anomaly.score
)
.ok();
writeln!(output, " {}", anomaly_bar.render(anomaly.score)).ok();
writeln!(output, " Reason: {}", anomaly.reason).ok();
writeln!(output, " Action: {}", anomaly.action).ok();
}
writeln!(output).ok();
}
if !self.report.recommendations.is_empty() {
writeln!(output, "Recommendations").ok();
writeln!(output, "{}", "\u{2501}".repeat(width)).ok();
for (i, rec) in self.report.recommendations.iter().enumerate() {
writeln!(output, "{}. {}", i + 1, rec).ok();
}
writeln!(output).ok();
}
writeln!(output, "{}", "\u{2500}".repeat(width)).ok();
writeln!(output, "Generated by PMAT | {}", self.report.timestamp).ok();
output
}
pub fn render_json(&self) -> String {
serde_json::to_string_pretty(&self.report).unwrap_or_else(|_| "{}".to_string())
}
pub fn render_markdown(&self) -> String {
let mut output = String::new();
writeln!(output, "# {}", self.report.title).ok();
writeln!(output).ok();
writeln!(
output,
"**Project**: {} | **Date**: {} | **Status**: {}",
self.report.project,
self.report.timestamp,
self.report.andon_status.display()
)
.ok();
writeln!(output).ok();
writeln!(output, "## Summary").ok();
writeln!(output).ok();
writeln!(
output,
"- **Quality Score**: {:.1}%",
self.report.quality_score
)
.ok();
writeln!(
output,
"- **Total Findings**: {}",
self.report.findings.len()
)
.ok();
let counts = self.report.findings_by_severity();
writeln!(
output,
"- **By Severity**: {} Critical, {} High, {} Medium, {} Low",
counts.get(&Severity::Critical).unwrap_or(&0),
counts.get(&Severity::High).unwrap_or(&0),
counts.get(&Severity::Medium).unwrap_or(&0),
counts.get(&Severity::Low).unwrap_or(&0),
)
.ok();
writeln!(output).ok();
if !self.report.clusters.is_empty() {
writeln!(output, "## Defect Clusters").ok();
writeln!(output).ok();
for cluster in &self.report.clusters {
writeln!(
output,
"### Cluster {}: {} ({} items)",
cluster.id, cluster.primary_category, cluster.size
)
.ok();
writeln!(output, "- Cohesion: {:.0}%", cluster.cohesion * 100.0).ok();
writeln!(output).ok();
}
}
if !self.report.recommendations.is_empty() {
writeln!(output, "## Recommendations").ok();
writeln!(output).ok();
for rec in &self.report.recommendations {
writeln!(output, "- {}", rec).ok();
}
writeln!(output).ok();
}
output
}
pub fn render(&self) -> String {
match self.config.format {
OutputFormat::Text | OutputFormat::Plain => self.render_text(),
OutputFormat::Json => self.render_json(),
OutputFormat::Markdown => self.render_markdown(),
_ => self.render_text(),
}
}
pub fn report(&self) -> &RichReport {
&self.report
}
pub fn report_mut(&mut self) -> &mut RichReport {
&mut self.report
}
}