use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConfidenceLevel {
Unknown,
Low,
Medium,
High,
}
impl ConfidenceLevel {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::High => "High",
Self::Medium => "Medium",
Self::Low => "Low",
Self::Unknown => "Unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Confidence {
pub level: ConfidenceLevel,
pub reasons: Vec<String>,
}
impl Confidence {
#[must_use]
pub fn high() -> Self {
Self {
level: ConfidenceLevel::High,
reasons: Vec::new(),
}
}
#[must_use]
pub fn unknown(reason: impl Into<String>) -> Self {
Self {
level: ConfidenceLevel::Unknown,
reasons: vec![reason.into()],
}
}
}
#[derive(Debug, Clone)]
pub struct ConfidenceFactors {
pub simulation_ok: bool,
pub logs_complete: bool,
pub parser_warnings: usize,
pub baseline_matched: Option<bool>,
pub unattributed_pct: f64,
pub scope_markers: usize,
pub metadata_available: bool,
}
impl Default for ConfidenceFactors {
fn default() -> Self {
Self {
simulation_ok: true,
logs_complete: true,
parser_warnings: 0,
baseline_matched: None,
unattributed_pct: 0.0,
scope_markers: 0,
metadata_available: false,
}
}
}
#[must_use]
pub fn score(factors: &ConfidenceFactors) -> Confidence {
let mut level = ConfidenceLevel::High;
let mut reasons = Vec::new();
if !factors.simulation_ok {
level = level.min(ConfidenceLevel::Low);
reasons.push("simulation did not complete as expected".to_string());
}
if !factors.logs_complete {
level = level.min(ConfidenceLevel::Low);
reasons.push("logs were incomplete or contained unrecognised lines".to_string());
}
if factors.parser_warnings > 0 {
level = level.min(ConfidenceLevel::Medium);
reasons.push(format!("{} parser warning(s)", factors.parser_warnings));
}
match factors.baseline_matched {
Some(true) => reasons.push("baseline matched".to_string()),
Some(false) => {
level = level.min(ConfidenceLevel::Low);
reasons.push("baseline fingerprint did not match".to_string());
}
None => {}
}
if factors.unattributed_pct >= 20.0 {
level = level.min(ConfidenceLevel::Medium);
reasons.push(format!("{:.0}% unattributed CU", factors.unattributed_pct));
}
if factors.scope_markers > 0 {
reasons.push(format!("{} scope markers detected", factors.scope_markers));
}
if !factors.metadata_available {
level = level.min(ConfidenceLevel::Medium);
reasons.push("runtime/version metadata unavailable".to_string());
}
Confidence { level, reasons }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clean_run_with_metadata_is_high() {
let f = ConfidenceFactors {
metadata_available: true,
..Default::default()
};
assert_eq!(score(&f).level, ConfidenceLevel::High);
}
#[test]
fn failed_simulation_is_low() {
let f = ConfidenceFactors {
simulation_ok: false,
metadata_available: true,
..Default::default()
};
assert_eq!(score(&f).level, ConfidenceLevel::Low);
}
#[test]
fn unattributed_cu_demotes_to_medium_with_reason() {
let f = ConfidenceFactors {
unattributed_pct: 22.0,
metadata_available: true,
..Default::default()
};
let c = score(&f);
assert_eq!(c.level, ConfidenceLevel::Medium);
assert!(c.reasons.iter().any(|r| r.contains("22% unattributed")));
}
#[test]
fn levels_order_high_above_low() {
assert!(ConfidenceLevel::High > ConfidenceLevel::Low);
assert!(ConfidenceLevel::Medium > ConfidenceLevel::Unknown);
}
}