cu_profiler_core/
confidence.rs1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum ConfidenceLevel {
13 Unknown,
15 Low,
17 Medium,
19 High,
21}
22
23impl ConfidenceLevel {
24 #[must_use]
26 pub fn label(self) -> &'static str {
27 match self {
28 Self::High => "High",
29 Self::Medium => "Medium",
30 Self::Low => "Low",
31 Self::Unknown => "Unknown",
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct Confidence {
39 pub level: ConfidenceLevel,
41 pub reasons: Vec<String>,
43}
44
45impl Confidence {
46 #[must_use]
48 pub fn high() -> Self {
49 Self {
50 level: ConfidenceLevel::High,
51 reasons: Vec::new(),
52 }
53 }
54
55 #[must_use]
57 pub fn unknown(reason: impl Into<String>) -> Self {
58 Self {
59 level: ConfidenceLevel::Unknown,
60 reasons: vec![reason.into()],
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
68pub struct ConfidenceFactors {
69 pub simulation_ok: bool,
71 pub logs_complete: bool,
73 pub parser_warnings: usize,
75 pub baseline_matched: Option<bool>,
77 pub unattributed_pct: f64,
79 pub scope_markers: usize,
81 pub metadata_available: bool,
83}
84
85impl Default for ConfidenceFactors {
86 fn default() -> Self {
87 Self {
88 simulation_ok: true,
89 logs_complete: true,
90 parser_warnings: 0,
91 baseline_matched: None,
92 unattributed_pct: 0.0,
93 scope_markers: 0,
94 metadata_available: false,
95 }
96 }
97}
98
99#[must_use]
104pub fn score(factors: &ConfidenceFactors) -> Confidence {
105 let mut level = ConfidenceLevel::High;
109 let mut reasons = Vec::new();
110
111 if !factors.simulation_ok {
112 level = level.min(ConfidenceLevel::Low);
113 reasons.push("simulation did not complete as expected".to_string());
114 }
115 if !factors.logs_complete {
116 level = level.min(ConfidenceLevel::Low);
117 reasons.push("logs were incomplete or contained unrecognised lines".to_string());
118 }
119 if factors.parser_warnings > 0 {
120 level = level.min(ConfidenceLevel::Medium);
121 reasons.push(format!("{} parser warning(s)", factors.parser_warnings));
122 }
123 match factors.baseline_matched {
124 Some(true) => reasons.push("baseline matched".to_string()),
125 Some(false) => {
126 level = level.min(ConfidenceLevel::Low);
127 reasons.push("baseline fingerprint did not match".to_string());
128 }
129 None => {}
130 }
131 if factors.unattributed_pct >= 20.0 {
132 level = level.min(ConfidenceLevel::Medium);
133 reasons.push(format!("{:.0}% unattributed CU", factors.unattributed_pct));
134 }
135 if factors.scope_markers > 0 {
136 reasons.push(format!("{} scope markers detected", factors.scope_markers));
137 }
138 if !factors.metadata_available {
139 level = level.min(ConfidenceLevel::Medium);
140 reasons.push("runtime/version metadata unavailable".to_string());
141 }
142
143 Confidence { level, reasons }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn clean_run_with_metadata_is_high() {
152 let f = ConfidenceFactors {
153 metadata_available: true,
154 ..Default::default()
155 };
156 assert_eq!(score(&f).level, ConfidenceLevel::High);
157 }
158
159 #[test]
160 fn failed_simulation_is_low() {
161 let f = ConfidenceFactors {
162 simulation_ok: false,
163 metadata_available: true,
164 ..Default::default()
165 };
166 assert_eq!(score(&f).level, ConfidenceLevel::Low);
167 }
168
169 #[test]
170 fn unattributed_cu_demotes_to_medium_with_reason() {
171 let f = ConfidenceFactors {
172 unattributed_pct: 22.0,
173 metadata_available: true,
174 ..Default::default()
175 };
176 let c = score(&f);
177 assert_eq!(c.level, ConfidenceLevel::Medium);
178 assert!(c.reasons.iter().any(|r| r.contains("22% unattributed")));
179 }
180
181 #[test]
182 fn levels_order_high_above_low() {
183 assert!(ConfidenceLevel::High > ConfidenceLevel::Low);
184 assert!(ConfidenceLevel::Medium > ConfidenceLevel::Unknown);
185 }
186}