#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone, Copy)]
pub struct Outcome {
pub ok: bool,
pub structured_error: bool,
}
impl Outcome {
pub fn ok() -> Self {
Self {
ok: true,
structured_error: false,
}
}
pub fn structured_failure() -> Self {
Self {
ok: false,
structured_error: true,
}
}
pub fn opaque_failure() -> Self {
Self {
ok: false,
structured_error: false,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct ReliabilityReport {
pub total: usize,
pub passed: usize,
pub failed: usize,
pub structured_failures: usize,
pub pass_rate: f64,
pub actionable_rate: f64,
}
pub fn assess_reliability<I>(cases: &[I], run: impl Fn(&I) -> Outcome) -> ReliabilityReport {
let total = cases.len();
let mut passed = 0usize;
let mut structured_failures = 0usize;
for case in cases {
let o = run(case);
if o.ok {
passed += 1;
} else if o.structured_error {
structured_failures += 1;
}
}
let failed = total - passed;
let (pass_rate, actionable_rate) = if total == 0 {
(1.0, 1.0)
} else {
(
passed as f64 / total as f64,
(passed + structured_failures) as f64 / total as f64,
)
};
ReliabilityReport {
total,
passed,
failed,
structured_failures,
pass_rate,
actionable_rate,
}
}
impl std::fmt::Display for ReliabilityReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"pass {:.0}% actionable {:.0}% ({}/{} ok, {} structured failures)",
self.pass_rate * 100.0,
self.actionable_rate * 100.0,
self.passed,
self.total,
self.structured_failures
)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone, Copy, Default)]
pub struct ErrorQuality {
pub has_code: bool,
pub has_message: bool,
pub has_location: bool,
pub has_fix: bool,
}
impl ErrorQuality {
pub fn score(&self) -> f64 {
let present = [
self.has_code,
self.has_message,
self.has_location,
self.has_fix,
]
.into_iter()
.filter(|b| *b)
.count();
present as f64 / 4.0
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct ErrorQualityReport {
pub failures: usize,
pub with_code: usize,
pub with_location: usize,
pub with_fix: usize,
pub mean_score: f64,
}
pub fn assess_error_quality<I>(
failures: &[I],
grade: impl Fn(&I) -> ErrorQuality,
) -> ErrorQualityReport {
let (mut with_code, mut with_location, mut with_fix, mut total) = (0, 0, 0, 0.0);
for case in failures {
let q = grade(case);
if q.has_code {
with_code += 1;
}
if q.has_location {
with_location += 1;
}
if q.has_fix {
with_fix += 1;
}
total += q.score();
}
let n = failures.len();
ErrorQualityReport {
failures: n,
with_code,
with_location,
with_fix,
mean_score: if n == 0 { 1.0 } else { total / n as f64 },
}
}
impl std::fmt::Display for ErrorQualityReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"error quality {:.0}% over {} failures (code={} location={} fix={})",
self.mean_score * 100.0,
self.failures,
self.with_code,
self.with_location,
self.with_fix
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_pass_is_perfect() {
let cases = ["a", "b", "c"];
let r = assess_reliability(&cases, |_| Outcome::ok());
assert_eq!(r.passed, 3);
assert_eq!(r.failed, 0);
assert_eq!(r.pass_rate, 1.0);
assert_eq!(r.actionable_rate, 1.0);
}
#[test]
fn structured_failures_are_actionable_even_when_not_passing() {
let cases = [0, 1, 2, 3];
let r = assess_reliability(&cases, |&i| match i {
0 | 1 => Outcome::ok(),
2 => Outcome::structured_failure(),
_ => Outcome::opaque_failure(),
});
assert_eq!(r.passed, 2);
assert_eq!(r.failed, 2);
assert_eq!(r.structured_failures, 1);
assert_eq!(r.pass_rate, 0.5);
assert_eq!(r.actionable_rate, 0.75);
}
#[test]
fn empty_set_is_vacuously_reliable() {
let cases: [&str; 0] = [];
let r = assess_reliability(&cases, |_| Outcome::ok());
assert_eq!(r.pass_rate, 1.0);
assert_eq!(r.actionable_rate, 1.0);
}
#[test]
fn error_quality_grades_actionability_components() {
let rich = ErrorQuality {
has_code: true,
has_message: true,
has_location: true,
has_fix: true,
};
assert_eq!(rich.score(), 1.0);
let prose = ErrorQuality {
has_message: true,
..Default::default()
};
assert_eq!(prose.score(), 0.25);
let failures = [rich, prose];
let r = assess_error_quality(&failures, |q| *q);
assert_eq!(r.failures, 2);
assert_eq!(r.with_code, 1);
assert!((r.mean_score - 0.625).abs() < 1e-9);
let empty: [ErrorQuality; 0] = [];
assert_eq!(assess_error_quality(&empty, |q| *q).mean_score, 1.0);
}
}