use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BlockerPriority {
P0Critical,
P1High,
P2Medium,
P3Low,
}
impl BlockerPriority {
#[must_use]
pub fn from_frequency(count: usize, total: usize) -> Self {
if total == 0 {
return Self::P3Low;
}
let percentage = (count as f64 / total as f64) * 100.0;
if percentage > 20.0 || count >= 50 {
Self::P0Critical
} else if percentage > 10.0 || count >= 20 {
Self::P1High
} else if percentage > 5.0 || count >= 10 {
Self::P2Medium
} else {
Self::P3Low
}
}
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::P0Critical => "P0-CRITICAL",
Self::P1High => "P1-HIGH",
Self::P2Medium => "P2-MEDIUM",
Self::P3Low => "P3-LOW",
}
}
#[must_use]
pub fn short_label(&self) -> &'static str {
match self {
Self::P0Critical => "P0",
Self::P1High => "P1",
Self::P2Medium => "P2",
Self::P3Low => "P3",
}
}
}
impl std::fmt::Display for BlockerPriority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
#[derive(Debug, Clone)]
pub struct ErrorCluster {
pub code: String,
pub count: usize,
pub percentage: f64,
pub cumulative: f64,
pub priority: BlockerPriority,
pub root_cause: String,
pub fix_action: String,
}
impl ErrorCluster {
#[must_use]
pub fn new(code: impl Into<String>, count: usize, total: usize) -> Self {
let code = code.into();
let percentage = if total > 0 {
(count as f64 / total as f64) * 100.0
} else {
0.0
};
let priority = BlockerPriority::from_frequency(count, total);
let (root_cause, fix_action) = Self::suggest_fix(&code);
Self {
code,
count,
percentage,
cumulative: 0.0, priority,
root_cause,
fix_action,
}
}
fn suggest_fix(code: &str) -> (String, String) {
match code {
"E0308" => (
"Type inference failure".to_string(),
"Improve bidirectional type inference in transpiler".to_string(),
),
"E0382" => (
"Ownership violation".to_string(),
"Add Clone derive or use Rc/Arc for shared ownership".to_string(),
),
"E0412" => (
"Generic parameter unresolved".to_string(),
"Resolve generic type parameters from context".to_string(),
),
"E0425" => (
"Missing import/binding".to_string(),
"Add missing imports or variable bindings".to_string(),
),
"E0282" => (
"Insufficient type info".to_string(),
"Add explicit type annotations".to_string(),
),
"E0277" => (
"Missing trait implementation".to_string(),
"Implement required traits or add trait bounds".to_string(),
),
"E0502" | "E0503" | "E0505" => (
"Borrow checker conflict".to_string(),
"Restructure code to satisfy borrow checker".to_string(),
),
"E0106" | "E0621" => (
"Lifetime annotation needed".to_string(),
"Add explicit lifetime annotations".to_string(),
),
_ => (
"Transpilation error".to_string(),
"Investigate specific error pattern".to_string(),
),
}
}
}
#[derive(Debug, Clone)]
pub struct ParetoAnalysis {
pub clusters: Vec<ErrorCluster>,
pub total_errors: usize,
pub vital_few_count: usize,
pub vital_few_coverage: f64,
}
impl ParetoAnalysis {
#[must_use]
pub fn analyze(error_counts: &HashMap<String, usize>) -> Self {
let total_errors: usize = error_counts.values().sum();
if total_errors == 0 {
return Self {
clusters: Vec::new(),
total_errors: 0,
vital_few_count: 0,
vital_few_coverage: 0.0,
};
}
let mut clusters: Vec<ErrorCluster> = error_counts
.iter()
.map(|(code, &count)| ErrorCluster::new(code.clone(), count, total_errors))
.collect();
clusters.sort_by(|a, b| b.count.cmp(&a.count));
let mut cumulative = 0.0;
for cluster in &mut clusters {
cumulative += cluster.percentage;
cluster.cumulative = cumulative;
}
let vital_few_count = clusters.iter().take_while(|c| c.cumulative <= 80.0).count() + 1;
let clusters_len = clusters.len();
let vital_few_count = vital_few_count.min(clusters_len);
let vital_few_coverage = clusters
.iter()
.take(vital_few_count)
.map(|c| c.percentage)
.sum();
Self {
clusters,
total_errors,
vital_few_count,
vital_few_coverage,
}
}
#[must_use]
pub fn by_priority(&self, priority: BlockerPriority) -> Vec<&ErrorCluster> {
self.clusters
.iter()
.filter(|c| c.priority == priority)
.collect()
}
#[must_use]
pub fn vital_few(&self) -> &[ErrorCluster] {
&self.clusters[..self.vital_few_count.min(self.clusters.len())]
}
#[must_use]
pub fn has_critical(&self) -> bool {
self.clusters
.iter()
.any(|c| c.priority == BlockerPriority::P0Critical)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_priority_p0_critical_by_percentage() {
assert_eq!(
BlockerPriority::from_frequency(25, 100),
BlockerPriority::P0Critical
);
assert_eq!(
BlockerPriority::from_frequency(21, 100),
BlockerPriority::P0Critical
);
}
#[test]
fn test_priority_p0_critical_by_count() {
assert_eq!(
BlockerPriority::from_frequency(50, 1000),
BlockerPriority::P0Critical
);
assert_eq!(
BlockerPriority::from_frequency(100, 10000),
BlockerPriority::P0Critical
);
}
#[test]
fn test_priority_p1_high() {
assert_eq!(
BlockerPriority::from_frequency(15, 100),
BlockerPriority::P1High
);
assert_eq!(
BlockerPriority::from_frequency(20, 1000),
BlockerPriority::P1High
);
}
#[test]
fn test_priority_p2_medium() {
assert_eq!(
BlockerPriority::from_frequency(7, 100),
BlockerPriority::P2Medium
);
assert_eq!(
BlockerPriority::from_frequency(10, 1000),
BlockerPriority::P2Medium
);
}
#[test]
fn test_priority_p3_low() {
assert_eq!(
BlockerPriority::from_frequency(3, 100),
BlockerPriority::P3Low
);
assert_eq!(
BlockerPriority::from_frequency(5, 1000),
BlockerPriority::P3Low
);
}
#[test]
fn test_priority_zero_total() {
assert_eq!(
BlockerPriority::from_frequency(0, 0),
BlockerPriority::P3Low
);
}
#[test]
fn test_priority_labels() {
assert_eq!(BlockerPriority::P0Critical.label(), "P0-CRITICAL");
assert_eq!(BlockerPriority::P1High.label(), "P1-HIGH");
assert_eq!(BlockerPriority::P2Medium.label(), "P2-MEDIUM");
assert_eq!(BlockerPriority::P3Low.label(), "P3-LOW");
}
#[test]
fn test_priority_ordering() {
assert!(BlockerPriority::P0Critical < BlockerPriority::P1High);
assert!(BlockerPriority::P1High < BlockerPriority::P2Medium);
assert!(BlockerPriority::P2Medium < BlockerPriority::P3Low);
}
#[test]
fn test_error_cluster_new() {
let cluster = ErrorCluster::new("E0308", 25, 100);
assert_eq!(cluster.code, "E0308");
assert_eq!(cluster.count, 25);
assert!((cluster.percentage - 25.0).abs() < 0.01);
assert_eq!(cluster.priority, BlockerPriority::P0Critical);
}
#[test]
fn test_error_cluster_fix_suggestions() {
let cluster = ErrorCluster::new("E0308", 10, 100);
assert!(cluster.root_cause.contains("Type inference"));
let cluster = ErrorCluster::new("E0382", 10, 100);
assert!(cluster.root_cause.contains("Ownership"));
let cluster = ErrorCluster::new("E9999", 10, 100);
assert!(cluster.root_cause.contains("Transpilation"));
}
#[test]
fn test_pareto_empty() {
let counts: HashMap<String, usize> = HashMap::new();
let analysis = ParetoAnalysis::analyze(&counts);
assert_eq!(analysis.total_errors, 0);
assert!(analysis.clusters.is_empty());
}
#[test]
fn test_pareto_single_error() {
let mut counts = HashMap::new();
counts.insert("E0308".to_string(), 100);
let analysis = ParetoAnalysis::analyze(&counts);
assert_eq!(analysis.total_errors, 100);
assert_eq!(analysis.clusters.len(), 1);
assert!((analysis.clusters[0].percentage - 100.0).abs() < 0.01);
}
#[test]
fn test_pareto_sorting() {
let mut counts = HashMap::new();
counts.insert("E0308".to_string(), 50);
counts.insert("E0382".to_string(), 30);
counts.insert("E0425".to_string(), 20);
let analysis = ParetoAnalysis::analyze(&counts);
assert_eq!(analysis.clusters[0].code, "E0308");
assert_eq!(analysis.clusters[1].code, "E0382");
assert_eq!(analysis.clusters[2].code, "E0425");
}
#[test]
fn test_pareto_cumulative() {
let mut counts = HashMap::new();
counts.insert("E0308".to_string(), 50);
counts.insert("E0382".to_string(), 30);
counts.insert("E0425".to_string(), 20);
let analysis = ParetoAnalysis::analyze(&counts);
assert!((analysis.clusters[0].cumulative - 50.0).abs() < 0.01);
assert!((analysis.clusters[1].cumulative - 80.0).abs() < 0.01);
assert!((analysis.clusters[2].cumulative - 100.0).abs() < 0.01);
}
#[test]
fn test_pareto_vital_few() {
let mut counts = HashMap::new();
counts.insert("E0308".to_string(), 60); counts.insert("E0382".to_string(), 25); counts.insert("E0425".to_string(), 10); counts.insert("E0412".to_string(), 5);
let analysis = ParetoAnalysis::analyze(&counts);
assert_eq!(analysis.vital_few_count, 2);
assert!((analysis.vital_few_coverage - 85.0).abs() < 0.01);
}
#[test]
fn test_pareto_by_priority() {
let mut counts = HashMap::new();
counts.insert("E0308".to_string(), 50); counts.insert("E0382".to_string(), 30); counts.insert("E0425".to_string(), 6);
counts.insert("E0412".to_string(), 114);
let analysis = ParetoAnalysis::analyze(&counts);
let p0 = analysis.by_priority(BlockerPriority::P0Critical);
let p1 = analysis.by_priority(BlockerPriority::P1High);
let p3 = analysis.by_priority(BlockerPriority::P3Low);
assert_eq!(p0.len(), 2);
assert_eq!(p1.len(), 1);
assert_eq!(p3.len(), 1);
}
#[test]
fn test_pareto_has_critical() {
let mut counts = HashMap::new();
counts.insert("E0308".to_string(), 50);
let analysis = ParetoAnalysis::analyze(&counts);
assert!(analysis.has_critical());
let mut counts2 = HashMap::new();
counts2.insert("E0308".to_string(), 3);
counts2.insert("E0382".to_string(), 3);
counts2.insert("E0999".to_string(), 94);
let mut counts3 = HashMap::new();
counts3.insert("E0308".to_string(), 3); counts3.insert("E0382".to_string(), 3); counts3.insert("E0425".to_string(), 4);
let mut counts4 = HashMap::new();
for i in 0..20 {
counts4.insert(format!("E0{i:03}"), 5);
}
let analysis4 = ParetoAnalysis::analyze(&counts4);
assert!(!analysis4.has_critical());
}
}