#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::colors as c;
use crate::cli::enums::OutputFormat;
use crate::services::metric_trends::{MetricTrendStore, PredictionResult};
use anyhow::Result;
use std::collections::HashMap;
use std::path::PathBuf;
pub async fn handle_predict_quality(
metric: Option<String>,
threshold: Option<f64>,
days: usize,
format: OutputFormat,
all: bool,
failures_only: bool,
) -> Result<()> {
let mut store = if let Ok(metrics_dir) = std::env::var("PMAT_METRICS_DIR") {
let trends_path = PathBuf::from(metrics_dir).join("trends");
MetricTrendStore::from_path(trends_path)?
} else {
MetricTrendStore::new()?
};
let default_thresholds = HashMap::from([
("lint".to_string(), 30_000.0),
("test-fast".to_string(), 300_000.0),
("coverage".to_string(), 600_000.0),
("build-release".to_string(), 50_000_000.0),
]);
let metrics_to_check = if all {
store.metrics()?
} else if let Some(m) = metric {
vec![m]
} else {
anyhow::bail!("Must specify --metric or --all");
};
let mut predictions = Vec::new();
for metric_name in metrics_to_check {
let threshold_value = threshold
.or_else(|| default_thresholds.get(&metric_name).copied())
.unwrap_or(0.0);
if threshold_value == 0.0 {
eprintln!(
"{} No threshold configured for metric: {}",
c::warn(""),
c::label(&metric_name)
);
continue;
}
match store.predict_threshold_breach(&metric_name, threshold_value, days) {
Ok(prediction) => {
if failures_only && prediction.breach_in_days.is_none() {
continue;
}
predictions.push(prediction);
}
Err(e) => {
eprintln!(
"{} Failed to predict {}: {} {}",
c::warn(""),
c::label(&metric_name),
e,
c::dim("(need at least 7 observations)")
);
}
}
}
if predictions.is_empty() {
println!(
"\n{}",
c::pass("No metrics to predict (all metrics safe or insufficient data)")
);
return Ok(());
}
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&predictions)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml_ng::to_string(&predictions)?);
}
_ => {
print_predictions_table(&predictions);
}
}
Ok(())
}
fn print_predictions_table(predictions: &[PredictionResult]) {
println!("\n{}\n", c::header("Quality Metrics Predictions"));
for pred in predictions {
println!("{}", c::subheader(&pred.metric));
println!(" {}: {:.1}ms", c::dim("Current"), pred.current_value);
println!(" {}: {:.1}ms", c::dim("Threshold"), pred.threshold);
if let Some(days) = pred.breach_in_days {
if let Some(value) = pred.predicted_value {
let urgency = if days <= 7 {
format!("{}URGENT{}", c::BOLD_RED, c::RESET)
} else if days <= 14 {
format!("{}WARNING{}", c::BOLD_YELLOW, c::RESET)
} else {
format!("{}INFO{}", c::BOLD_BLUE, c::RESET)
};
println!(
" {}: {} in {} days (predicted: {:.1}ms)",
c::dim("Breach"),
urgency,
c::number(&days.to_string()),
value
);
println!(
" {}: {} (R²={:.3})",
c::dim("Confidence"),
c::pct(pred.confidence * 100.0, 80.0, 50.0),
pred.confidence
);
}
} else {
println!(" {}: {}", c::dim("Breach"), c::pass("No breach predicted"));
println!(
" {}: {} (R²={:.3})",
c::dim("Confidence"),
c::pct(pred.confidence * 100.0, 80.0, 50.0),
pred.confidence
);
}
if !pred.recommendations.is_empty() {
println!(" {}:", c::dim("Recommendations"));
for rec in &pred.recommendations {
println!(" {} {}", c::dim("•"), rec);
}
}
println!();
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore] async fn test_predict_quality_no_metric() {
let result =
handle_predict_quality(None, None, 30, OutputFormat::Table, false, false).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Must specify"));
}
}