use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskClass {
pub id: String,
pub name: String,
pub signal_keywords: Vec<String>,
}
impl TaskClass {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
signal_keywords: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
signal_keywords: signal_keywords
.into_iter()
.map(|k| k.into().to_lowercase())
.collect(),
}
}
pub(crate) fn overlap_score(&self, signal: &str) -> usize {
let tokens = tokenise(signal);
self.signal_keywords
.iter()
.filter(|kw| tokens.contains(*kw))
.count()
}
}
pub fn builtin_task_classes() -> Vec<TaskClass> {
vec![
TaskClass::new(
"missing-import",
"Missing import / undefined symbol",
[
"e0425",
"e0433",
"unresolved",
"undefined",
"import",
"missing",
"cannot",
"find",
"symbol",
],
),
TaskClass::new(
"type-mismatch",
"Type mismatch",
[
"e0308",
"mismatched",
"expected",
"found",
"type",
"mismatch",
],
),
TaskClass::new(
"borrow-conflict",
"Borrow checker conflict",
[
"e0502", "e0505", "borrow", "lifetime", "moved", "cannot", "conflict",
],
),
TaskClass::new(
"test-failure",
"Test failure",
["test", "failed", "panic", "assert", "assertion", "failure"],
),
TaskClass::new(
"performance",
"Performance issue",
["slow", "latency", "timeout", "perf", "performance", "hot"],
),
]
}
pub struct TaskClassMatcher {
classes: Vec<TaskClass>,
}
impl TaskClassMatcher {
pub fn new(classes: Vec<TaskClass>) -> Self {
Self { classes }
}
pub fn with_builtins() -> Self {
Self::new(builtin_task_classes())
}
pub fn classify<'a>(&'a self, signals: &[String]) -> Option<&'a TaskClass> {
let mut best: Option<(&TaskClass, usize)> = None;
for class in &self.classes {
let total_score: usize = signals.iter().map(|s| class.overlap_score(s)).sum();
if total_score > 0 {
match best {
None => best = Some((class, total_score)),
Some((_, prev_score)) if total_score > prev_score => {
best = Some((class, total_score));
}
_ => {}
}
}
}
best.map(|(c, _)| c)
}
pub fn classes(&self) -> &[TaskClass] {
&self.classes
}
}
fn tokenise(s: &str) -> Vec<String> {
s.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_lowercase())
.collect()
}
pub fn signals_match_class(signals: &[String], class_id: &str, registry: &[TaskClass]) -> bool {
let matcher = TaskClassMatcher::new(registry.to_vec());
matcher
.classify(signals)
.map_or(false, |c| c.id == class_id)
}
#[cfg(test)]
mod tests {
use super::*;
fn matcher() -> TaskClassMatcher {
TaskClassMatcher::with_builtins()
}
#[test]
fn test_missing_import_via_error_code() {
let m = matcher();
let signals = vec!["error[E0425]: cannot find value `foo` in scope".to_string()];
let cls = m.classify(&signals).expect("should classify");
assert_eq!(cls.id, "missing-import");
}
#[test]
fn test_missing_import_via_natural_language() {
let m = matcher();
let signals = vec!["undefined symbol: use_missing_fn".to_string()];
let cls = m.classify(&signals).expect("should classify");
assert_eq!(cls.id, "missing-import");
}
#[test]
fn test_missing_import_via_unresolved_import() {
let m = matcher();
let signals = vec!["unresolved import `std::collections::Missing`".to_string()];
let cls = m.classify(&signals).expect("should classify");
assert_eq!(cls.id, "missing-import");
}
#[test]
fn test_type_mismatch_classification() {
let m = matcher();
let signals =
vec!["error[E0308]: mismatched types: expected `u32` found `String`".to_string()];
let cls = m.classify(&signals).expect("should classify");
assert_eq!(cls.id, "type-mismatch");
}
#[test]
fn test_borrow_conflict_classification() {
let m = matcher();
let signals = vec![
"error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable"
.to_string(),
];
let cls = m.classify(&signals).expect("should classify");
assert_eq!(cls.id, "borrow-conflict");
}
#[test]
fn test_test_failure_classification() {
let m = matcher();
let signals = vec!["test panicked: assertion failed: x == y".to_string()];
let cls = m.classify(&signals).expect("should classify");
assert_eq!(cls.id, "test-failure");
}
#[test]
fn test_multiple_signals_accumulate_score() {
let m = matcher();
let signals = vec![
"expected type `u32`".to_string(),
"found type `String` — type mismatch".to_string(),
];
let cls = m.classify(&signals).expect("should classify");
assert_eq!(cls.id, "type-mismatch");
}
#[test]
fn test_no_false_positive_type_vs_borrow() {
let m = matcher();
let signals = vec!["error[E0308]: mismatched type".to_string()];
let cls = m.classify(&signals).unwrap();
assert_ne!(
cls.id, "borrow-conflict",
"must not cross-match borrow-conflict"
);
}
#[test]
fn test_no_false_positive_borrow_vs_import() {
let m = matcher();
let signals = vec!["error[E0502]: cannot borrow as mutable".to_string()];
let cls = m.classify(&signals).unwrap();
assert_ne!(cls.id, "missing-import");
}
#[test]
fn test_no_match_returns_none() {
let m = matcher();
let signals = vec!["network timeout connecting to database server".to_string()];
if let Some(cls) = m.classify(&signals) {
assert_ne!(cls.id, "missing-import");
assert_ne!(cls.id, "type-mismatch");
assert_ne!(cls.id, "borrow-conflict");
}
}
#[test]
fn test_empty_signals_returns_none() {
let m = matcher();
assert!(m.classify(&[]).is_none());
}
#[test]
fn test_custom_class_wins_over_builtin() {
let mut classes = builtin_task_classes();
classes.push(TaskClass::new(
"db-timeout",
"Database timeout",
["database", "timeout", "connection", "pool", "exhausted"],
));
let m = TaskClassMatcher::new(classes);
let signals = vec!["database connection pool exhausted — timeout".to_string()];
let cls = m.classify(&signals).expect("should classify");
assert_eq!(cls.id, "db-timeout");
}
#[test]
fn test_signals_match_class_helper() {
let registry = builtin_task_classes();
let signals = vec!["error[E0425]: cannot find value".to_string()];
assert!(signals_match_class(&signals, "missing-import", ®istry));
assert!(!signals_match_class(&signals, "type-mismatch", ®istry));
}
#[test]
fn test_overlap_score_case_insensitive() {
let class = TaskClass::new("tc", "Test", ["e0425", "unresolved"]);
let m = TaskClassMatcher::new(vec![class]);
let signals = vec!["E0425 unresolved import".to_string()];
let cls = m
.classify(&signals)
.expect("case-insensitive classify should work");
assert_eq!(cls.id, "tc");
}
}