perspt_coding/diag/
cluster.rs1use std::collections::BTreeMap;
11
12use perspt_sdk::ResidualClass;
13
14use super::types::StructuredDiagnostic;
15
16pub const CLUSTER_PROFILE_V1: &str = "perspt-cluster-v1:log-damped";
19
20#[derive(Debug, Clone, PartialEq)]
22pub struct DiagnosticCluster {
23 pub class: ResidualClass,
24 pub code: String,
27 pub root: StructuredDiagnostic,
30 pub members: Vec<StructuredDiagnostic>,
31}
32
33impl DiagnosticCluster {
34 pub fn magnitude(&self) -> f64 {
37 1.0 + (self.members.len() as f64).ln()
38 }
39}
40
41pub fn cluster(diagnostics: Vec<StructuredDiagnostic>) -> Vec<DiagnosticCluster> {
45 let mut order: Vec<(ResidualClass, String)> = Vec::new();
46 let mut buckets: BTreeMap<(String, String), Vec<StructuredDiagnostic>> = BTreeMap::new();
47 let mut last_primary_key: Option<(ResidualClass, String)> = None;
48 for diagnostic in diagnostics {
49 let key = if !diagnostic.primary && diagnostic.code.is_none() {
50 last_primary_key
51 .clone()
52 .unwrap_or((diagnostic.class, format!("{:?}", diagnostic.class)))
53 } else {
54 let code = diagnostic
55 .code
56 .clone()
57 .unwrap_or_else(|| format!("{:?}", diagnostic.class));
58 let key = (diagnostic.class, code);
59 if diagnostic.primary {
60 last_primary_key = Some(key.clone());
61 }
62 key
63 };
64 let map_key = (format!("{:?}", key.0), key.1.clone());
65 if !buckets.contains_key(&map_key) {
66 order.push(key.clone());
67 }
68 buckets.entry(map_key).or_default().push(diagnostic);
69 }
70 order
71 .into_iter()
72 .map(|(class, code)| {
73 let members = buckets
74 .remove(&(format!("{class:?}"), code.clone()))
75 .unwrap_or_default();
76 DiagnosticCluster {
77 class,
78 code,
79 root: members.first().cloned().expect("nonempty cluster"),
80 members,
81 }
82 })
83 .collect()
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 fn diag(class: ResidualClass, code: Option<&str>, primary: bool) -> StructuredDiagnostic {
91 StructuredDiagnostic {
92 class,
93 code: code.map(str::to_string),
94 message: "m".into(),
95 path: Some("src/lib.rs".into()),
96 line: Some(1),
97 column: Some(1),
98 primary,
99 suggestion: None,
100 }
101 }
102
103 #[test]
104 fn cascades_fold_into_their_root_cluster() {
105 let mut diagnostics = vec![diag(ResidualClass::ImportGraph, Some("E0432"), true)];
106 for _ in 0..199 {
107 diagnostics.push(diag(ResidualClass::ImportGraph, None, false));
108 }
109 let clusters = cluster(diagnostics);
110 assert_eq!(clusters.len(), 1);
111 assert_eq!(clusters[0].members.len(), 200);
112 let magnitude = clusters[0].magnitude();
113 assert!(magnitude < 8.0, "log damping: {magnitude}");
114 assert!(magnitude > 1.0);
115 }
116
117 #[test]
118 fn distinct_codes_stay_distinct_and_singletons_score_one() {
119 let clusters = cluster(vec![
120 diag(ResidualClass::Type, Some("E0308"), true),
121 diag(ResidualClass::ImportGraph, Some("E0432"), true),
122 ]);
123 assert_eq!(clusters.len(), 2);
124 assert!((clusters[0].magnitude() - 1.0).abs() < 1e-9);
125 }
126}