Skip to main content

mur_common/skill/
env_class.rs

1//! Classify a failed run as a *workflow* failure (the skill is broken) vs an
2//! *environment* failure (network/credentials/missing binary) so a flaky
3//! network never marks a workflow Broken (workflow-engine v2, Layer 4).
4//!
5//! Classification is heuristic; the confidence field lets the Broken
6//! fast-path (P4) require high-confidence workflow failures before demoting,
7//! and lets the user override via `mur run --env-class workflow|env`.
8
9/// Result of classifying a failure's stderr/output.
10#[derive(Debug, Clone, PartialEq)]
11pub struct EnvClassification {
12    /// "workflow" | "env"
13    pub class: &'static str,
14    pub confidence: f64,
15}
16
17/// Substrings that indicate the *environment* failed, not the workflow.
18const ENV_MARKERS: &[&str] = &[
19    "connection refused",
20    "connection reset",
21    "timed out",
22    "timeout",
23    "could not resolve",
24    "temporary failure in name resolution",
25    "network is unreachable",
26    "tls",
27    "certificate",
28    "401",
29    "403",
30    "unauthorized",
31    "forbidden",
32    "credential",
33    "permission denied",
34    "no such file or directory",
35    "command not found",
36    "not found in path",
37    "rate limit",
38    "429",
39    "disk full",
40    "no space left",
41];
42
43/// Heuristic stderr classifier. Zero env markers → likely a workflow bug
44/// (moderate confidence); one marker → likely environmental; several → almost
45/// certainly environmental.
46pub fn classify_failure(stderr: &str) -> EnvClassification {
47    let lower = stderr.to_lowercase();
48    let hits = ENV_MARKERS.iter().filter(|m| lower.contains(*m)).count();
49    match hits {
50        0 => EnvClassification {
51            class: "workflow",
52            confidence: 0.6,
53        },
54        1 => EnvClassification {
55            class: "env",
56            confidence: 0.6,
57        },
58        _ => EnvClassification {
59            class: "env",
60            confidence: 0.9,
61        },
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn clean_assertion_failure_is_workflow() {
71        let c = classify_failure("assertion failed: expected 3 got 2");
72        assert_eq!(c.class, "workflow");
73    }
74
75    #[test]
76    fn network_error_is_env() {
77        let c = classify_failure("curl: (7) Connection refused");
78        assert_eq!(c.class, "env");
79    }
80
81    #[test]
82    fn multiple_markers_high_confidence_env() {
83        let c = classify_failure("error: timed out\ncurl: connection refused after retry");
84        assert_eq!(c.class, "env");
85        assert!(c.confidence > 0.8);
86    }
87}