use sdivi_core::{DivergenceSummary, Snapshot, ThresholdCheckResult, TrendResult};
use sdivi_patterns::{PatternCatalog, PatternStats};
pub fn print_catalog(catalog: &PatternCatalog) {
if catalog.entries.is_empty() {
println!("(no patterns found)");
return;
}
for (category, fingerprints) in &catalog.entries {
println!("=== {category} ===");
for (fp, stats) in fingerprints {
print_stats_line(&fp.to_hex(), stats);
}
}
}
pub fn print_snapshot(snapshot: &Snapshot) {
println!("snapshot_version: {}", snapshot.snapshot_version);
println!("timestamp: {}", snapshot.timestamp);
if let Some(commit) = &snapshot.commit {
println!("commit: {commit}");
}
println!("nodes: {}", snapshot.graph.node_count);
println!("edges: {}", snapshot.graph.edge_count);
println!("density: {:.6}", snapshot.graph.density);
println!(
"communities: {}",
snapshot.partition.community_count()
);
println!("modularity: {:.6}", snapshot.partition.modularity);
println!("pattern_categories:{}", snapshot.catalog.entries.len());
if let Some(id) = &snapshot.intent_divergence {
println!("boundaries: {}", id.boundary_count);
println!("violations: {}", id.violation_count);
}
if let Some(ref cc) = snapshot.change_coupling {
if cc.pairs.is_empty() {
println!("change coupling: 0 pairs");
} else {
let top: Vec<String> = cc
.pairs
.iter()
.take(5)
.map(|p| format!("({}, {}): {:.2}", p.source, p.target, p.frequency))
.collect();
println!(
"change coupling: {} pairs (top 5: {})",
cc.pairs.len(),
top.join("; ")
);
}
}
}
pub fn print_divergence(summary: &DivergenceSummary) {
fn fmt_opt_f64(v: Option<f64>) -> String {
match v {
Some(x) => format!("{x:.6}"),
None => "null".to_string(),
}
}
fn fmt_opt_i64(v: Option<i64>) -> String {
match v {
Some(x) => x.to_string(),
None => "null".to_string(),
}
}
println!(
"pattern_entropy_delta: {}",
fmt_opt_f64(summary.pattern_entropy_delta)
);
println!(
"coupling_delta: {}",
fmt_opt_f64(summary.coupling_delta)
);
println!(
"community_count_delta: {}",
fmt_opt_i64(summary.community_count_delta)
);
println!(
"boundary_violation_delta:{}",
fmt_opt_i64(summary.boundary_violation_delta)
);
}
pub fn print_check(result: &ThresholdCheckResult, summary: &DivergenceSummary) {
if result.breached {
println!(
"check: FAILED — {} threshold(s) exceeded",
result.breaches.len()
);
for b in &result.breaches {
if let Some(cat) = &b.category {
println!(
" {} [{}]: {:.6} > {:.6} (limit)",
b.dimension, cat, b.actual, b.limit
);
} else {
println!(
" {}: {:.6} > {:.6} (limit)",
b.dimension, b.actual, b.limit
);
}
}
} else {
println!("check: OK — all thresholds within limits");
}
if !result.applied_overrides.is_empty() {
println!();
println!("applied overrides:");
for (cat, info) in &result.applied_overrides {
if info.active {
println!(" {cat}: active (expires {})", info.expires);
} else {
let reason = info.expired_reason.as_deref().unwrap_or("expired");
println!(" {cat}: inactive — {reason}");
}
}
}
println!();
print_divergence(summary);
}
pub fn print_trend(result: &TrendResult) {
fn fmt_slope(v: Option<f64>) -> String {
match v {
Some(x) => format!("{x:+.6}"),
None => "null".to_string(),
}
}
println!("snapshots in window: {}", result.snapshot_count);
println!(
"pattern_entropy_slope: {}",
fmt_slope(result.pattern_entropy_slope)
);
println!(
"convention_drift_slope: {}",
fmt_slope(result.convention_drift_slope)
);
println!(
"coupling_slope: {}",
fmt_slope(result.coupling_slope)
);
println!(
"community_count_slope: {}",
fmt_slope(result.community_count_slope)
);
}
fn print_stats_line(hex: &str, stats: &PatternStats) {
let short = &hex[..12];
let locs: Vec<String> = stats
.locations
.iter()
.take(3)
.map(|l| format!("{}:{}:{}", l.file.display(), l.start_row, l.start_col))
.collect();
let loc_str = if locs.is_empty() {
String::new()
} else {
format!(" | {}", locs.join(", "))
};
println!(" {short}… count:{}{}", stats.count, loc_str);
}