Skip to main content

llm_kernel/safety/
classify.rs

1//! Error classification via regex rules.
2//!
3//! Categorizes errors into broad buckets for pattern detection
4//! and telemetry without exposing raw error messages.
5
6use std::sync::LazyLock;
7
8/// Failure category for error classification.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FailureCategory {
11    /// Type mismatch, missing symbol, or borrow-checker error.
12    TypeError,
13    /// Unexpected token, parse error, or unclosed delimiter.
14    SyntaxError,
15    /// Test assertion failure or `test result: FAILED`.
16    TestFail,
17    /// Clippy warning, unused import, or denied lint.
18    LintFail,
19    /// Compilation or linking failure.
20    BuildFail,
21    /// OS-level permission or access denial.
22    PermissionDenied,
23    /// Request or operation timed out.
24    Timeout,
25    /// File, resource, or HTTP 404 not found.
26    NotFound,
27    /// Panic, segfault, OOM, or other runtime execution error.
28    RuntimeError,
29    /// Error does not match any known category.
30    Unknown,
31}
32
33impl FailureCategory {
34    /// All variants as a static slice.
35    pub fn all() -> &'static [FailureCategory] {
36        &[
37            Self::TypeError,
38            Self::SyntaxError,
39            Self::TestFail,
40            Self::LintFail,
41            Self::BuildFail,
42            Self::PermissionDenied,
43            Self::Timeout,
44            Self::NotFound,
45            Self::RuntimeError,
46            Self::Unknown,
47        ]
48    }
49
50    /// Human-readable label.
51    pub fn label(&self) -> &'static str {
52        match self {
53            Self::TypeError => "type_error",
54            Self::SyntaxError => "syntax_error",
55            Self::TestFail => "test_fail",
56            Self::LintFail => "lint_fail",
57            Self::BuildFail => "build_fail",
58            Self::PermissionDenied => "permission_denied",
59            Self::Timeout => "timeout",
60            Self::NotFound => "not_found",
61            Self::RuntimeError => "runtime_error",
62            Self::Unknown => "unknown",
63        }
64    }
65}
66
67impl std::str::FromStr for FailureCategory {
68    type Err = ();
69    fn from_str(s: &str) -> Result<Self, Self::Err> {
70        match s {
71            "type_error" => Ok(Self::TypeError),
72            "syntax_error" => Ok(Self::SyntaxError),
73            "test_fail" => Ok(Self::TestFail),
74            "lint_fail" => Ok(Self::LintFail),
75            "build_fail" => Ok(Self::BuildFail),
76            "permission_denied" => Ok(Self::PermissionDenied),
77            "timeout" => Ok(Self::Timeout),
78            "not_found" => Ok(Self::NotFound),
79            "runtime_error" => Ok(Self::RuntimeError),
80            "unknown" => Ok(Self::Unknown),
81            _ => Err(()),
82        }
83    }
84}
85
86struct ClassificationRule {
87    category: FailureCategory,
88    pattern: regex::Regex,
89}
90
91static RULES: LazyLock<Vec<ClassificationRule>> = LazyLock::new(|| {
92    let raw: &[(&str, FailureCategory)] = &[
93        // Compiler diagnostic codes (most specific, check first)
94        (r"(?i)error\[E\d{4}\]", FailureCategory::LintFail),
95        // Type errors
96        (
97            r"(?i)cannot find (value|function|type|module|struct|field|method)",
98            FailureCategory::TypeError,
99        ),
100        (r"(?i)mismatched types?", FailureCategory::TypeError),
101        (r"(?i)type mismatch", FailureCategory::TypeError),
102        (r"(?i)\bundefined\b", FailureCategory::TypeError),
103        (
104            r"(?i)is not (a |an )?(function|defined|iterable)",
105            FailureCategory::TypeError,
106        ),
107        (r"(?i)expected .+, found", FailureCategory::TypeError),
108        (r"(?i)no method named", FailureCategory::TypeError),
109        (
110            r"(?i)no (associated |)item named",
111            FailureCategory::TypeError,
112        ),
113        (r"(?i)borrow of moved value", FailureCategory::TypeError),
114        (r"(?i)use of moved value", FailureCategory::TypeError),
115        (r"(?i)cannot borrow", FailureCategory::TypeError),
116        // Syntax errors
117        (r"(?i)syntax error", FailureCategory::SyntaxError),
118        (r"(?i)unexpected token", FailureCategory::SyntaxError),
119        (
120            r"(?i)unexpected end of (file|input)",
121            FailureCategory::SyntaxError,
122        ),
123        (r"(?i)parse error", FailureCategory::SyntaxError),
124        (r"(?i)missing ';' or '}'", FailureCategory::SyntaxError),
125        (r"(?i)expected .+ but found", FailureCategory::SyntaxError),
126        (r"(?i)unclosed delimiter", FailureCategory::SyntaxError),
127        // Test failures
128        (r"(?i)test .* failed", FailureCategory::TestFail),
129        (r"(?i)assertion.*failed", FailureCategory::TestFail),
130        (r"(?i)panic!.*at", FailureCategory::TestFail),
131        (r"(?i)test result: FAILED", FailureCategory::TestFail),
132        (r"(?i)1 failed", FailureCategory::TestFail),
133        (r"(?i)\bFAIL\b", FailureCategory::TestFail),
134        // Lint failures
135        (r"(?i)clippy::", FailureCategory::LintFail),
136        (r"(?i)warning: .*denied", FailureCategory::LintFail),
137        (
138            r"(?i)unused (import|variable|function)",
139            FailureCategory::LintFail,
140        ),
141        // Build failures
142        (r"(?i)compilation failed", FailureCategory::BuildFail),
143        (r"(?i)build failed", FailureCategory::BuildFail),
144        (r"(?i)cargo build.*failed", FailureCategory::BuildFail),
145        (r"(?i)could not compile", FailureCategory::BuildFail),
146        (r"(?i)linking.*failed", FailureCategory::BuildFail),
147        (r"(?i)fatal error:", FailureCategory::BuildFail),
148        // Permission denied
149        (r"(?i)permission denied", FailureCategory::PermissionDenied),
150        (r"(?i)access denied", FailureCategory::PermissionDenied),
151        (r"(?i)EACCES", FailureCategory::PermissionDenied),
152        (r"(?i)EPERM", FailureCategory::PermissionDenied),
153        (
154            r"(?i)operation not permitted",
155            FailureCategory::PermissionDenied,
156        ),
157        // Timeout
158        (r"(?i)timed? ?out", FailureCategory::Timeout),
159        (r"(?i)deadline exceeded", FailureCategory::Timeout),
160        (r"(?i)ETIMEDOUT", FailureCategory::Timeout),
161        // Not found
162        (r"(?i)not found", FailureCategory::NotFound),
163        (r"(?i)ENOENT", FailureCategory::NotFound),
164        (r"(?i)no such file", FailureCategory::NotFound),
165        (r"(?i)404", FailureCategory::NotFound),
166        // Runtime errors (catch-all for execution errors)
167        (r"(?i)runtime error", FailureCategory::RuntimeError),
168        (r"(?i)segmentation fault", FailureCategory::RuntimeError),
169        (r"(?i)stack overflow", FailureCategory::RuntimeError),
170        (r"(?i)out of memory", FailureCategory::RuntimeError),
171        (r"(?i)OOM", FailureCategory::RuntimeError),
172        (r"(?i)panic!?", FailureCategory::RuntimeError),
173    ];
174
175    raw.iter()
176        .map(|(pattern, category)| ClassificationRule {
177            category: *category,
178            pattern: regex::Regex::new(pattern).expect("invalid classification regex"),
179        })
180        .collect()
181});
182
183/// Classify an error message into a failure category.
184///
185/// Uses the first matching rule from a priority-ordered rule set.
186/// Returns `Unknown` if no rule matches.
187///
188/// A fast byte-prefix path checks for compiler diagnostic codes
189/// (`error[EXXXX]`) before invoking the regex engine.
190pub fn classify_failure(error_message: &str) -> FailureCategory {
191    // Fast path: compiler diagnostic codes like "error[E0412]"
192    if let Some(rest) = error_message.strip_prefix("error[")
193        && let Some(bracket_end) = rest.find(']')
194    {
195        let code = &rest[..bracket_end];
196        if code.len() == 5
197            && code.as_bytes()[0] == b'E'
198            && code[1..].bytes().all(|b| b.is_ascii_digit())
199        {
200            return FailureCategory::LintFail;
201        }
202    }
203    for rule in RULES.iter() {
204        if rule.pattern.is_match(error_message) {
205            return rule.category;
206        }
207    }
208    FailureCategory::Unknown
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn type_error_undefined() {
217        assert_eq!(
218            classify_failure("Cannot find value `foo`"),
219            FailureCategory::TypeError
220        );
221    }
222
223    #[test]
224    fn type_error_mismatched() {
225        assert_eq!(
226            classify_failure("mismatched types: expected `i32`, found `String`"),
227            FailureCategory::TypeError
228        );
229    }
230
231    #[test]
232    fn syntax_error() {
233        assert_eq!(
234            classify_failure("syntax error: unexpected token `}`"),
235            FailureCategory::SyntaxError
236        );
237    }
238
239    #[test]
240    fn test_fail() {
241        assert_eq!(
242            classify_failure("test test_insert failed"),
243            FailureCategory::TestFail
244        );
245    }
246
247    #[test]
248    fn test_assertion() {
249        assert_eq!(
250            classify_failure("assertion `left == right` failed"),
251            FailureCategory::TestFail
252        );
253    }
254
255    #[test]
256    fn lint_fail() {
257        assert_eq!(
258            classify_failure("error[E0412]: cannot find type"),
259            FailureCategory::LintFail
260        );
261    }
262
263    #[test]
264    fn build_fail() {
265        assert_eq!(
266            classify_failure("could not compile `my-crate`"),
267            FailureCategory::BuildFail
268        );
269    }
270
271    #[test]
272    fn permission_denied() {
273        assert_eq!(
274            classify_failure("Permission denied (os error 13)"),
275            FailureCategory::PermissionDenied
276        );
277    }
278
279    #[test]
280    fn timeout() {
281        assert_eq!(
282            classify_failure("request timed out after 30s"),
283            FailureCategory::Timeout
284        );
285    }
286
287    #[test]
288    fn not_found() {
289        assert_eq!(
290            classify_failure("No such file or directory"),
291            FailureCategory::NotFound
292        );
293    }
294
295    #[test]
296    fn runtime_panic() {
297        assert_eq!(
298            classify_failure("thread 'main' panicked at 'overflow'"),
299            FailureCategory::RuntimeError
300        );
301    }
302
303    #[test]
304    fn unknown_for_gibberish() {
305        assert_eq!(
306            classify_failure("everything is fine"),
307            FailureCategory::Unknown
308        );
309    }
310
311    #[test]
312    fn category_labels() {
313        assert_eq!(FailureCategory::TypeError.label(), "type_error");
314        assert_eq!(FailureCategory::Unknown.label(), "unknown");
315    }
316
317    #[test]
318    fn all_categories_count() {
319        assert_eq!(FailureCategory::all().len(), 10);
320    }
321
322    #[test]
323    fn from_str_roundtrip() {
324        use std::str::FromStr;
325        for cat in FailureCategory::all() {
326            let label = cat.label();
327            let back = FailureCategory::from_str(label).unwrap();
328            assert_eq!(back, *cat, "roundtrip failed for {label}");
329        }
330        assert!(FailureCategory::from_str("not_a_real_category").is_err());
331    }
332}