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)
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaskClassDefinition {
pub id: String,
pub name: String,
pub description: String,
pub signal_keywords: Vec<String>,
}
impl TaskClassDefinition {
pub fn into_task_class(self) -> TaskClass {
TaskClass::new(self.id, self.name, self.signal_keywords)
}
}
pub fn builtin_task_class_definitions() -> Vec<TaskClassDefinition> {
vec![
TaskClassDefinition {
id: "missing-import".to_string(),
name: "Missing import / undefined symbol".to_string(),
description: "Compiler cannot find symbol unresolved import undefined reference \
missing use declaration cannot find value in scope"
.to_string(),
signal_keywords: vec![
"e0425",
"e0433",
"unresolved",
"undefined",
"import",
"missing",
"cannot",
"find",
"symbol",
]
.into_iter()
.map(String::from)
.collect(),
},
TaskClassDefinition {
id: "type-mismatch".to_string(),
name: "Type mismatch".to_string(),
description: "Type mismatch mismatched types expected one type found another \
type annotation required"
.to_string(),
signal_keywords: vec![
"e0308",
"mismatched",
"expected",
"found",
"type",
"mismatch",
]
.into_iter()
.map(String::from)
.collect(),
},
TaskClassDefinition {
id: "borrow-conflict".to_string(),
name: "Borrow checker conflict".to_string(),
description: "Borrow checker conflict cannot borrow as mutable lifetime error \
value moved cannot use after move"
.to_string(),
signal_keywords: vec![
"e0502", "e0505", "borrow", "lifetime", "moved", "cannot", "conflict",
]
.into_iter()
.map(String::from)
.collect(),
},
TaskClassDefinition {
id: "test-failure".to_string(),
name: "Test failure".to_string(),
description: "Test failure panicked assertion failed test did not pass".to_string(),
signal_keywords: vec!["test", "failed", "panic", "assert", "assertion", "failure"]
.into_iter()
.map(String::from)
.collect(),
},
TaskClassDefinition {
id: "performance".to_string(),
name: "Performance issue".to_string(),
description: "Performance issue slow response high latency operation timeout \
hot path resource contention"
.to_string(),
signal_keywords: vec!["slow", "latency", "timeout", "perf", "performance", "hot"]
.into_iter()
.map(String::from)
.collect(),
},
]
}
#[cfg(feature = "evolution-experimental")]
#[derive(Deserialize)]
struct TaskClassesToml {
task_classes: Vec<TaskClassDefinition>,
}
#[cfg(feature = "evolution-experimental")]
pub fn load_task_classes_from_toml(
path: &std::path::Path,
) -> Result<Vec<TaskClassDefinition>, String> {
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let parsed: TaskClassesToml = toml::from_str(&content).map_err(|e| e.to_string())?;
Ok(parsed.task_classes)
}
pub fn load_task_classes() -> Vec<TaskClassDefinition> {
#[cfg(feature = "evolution-experimental")]
{
if let Some(home) = std::env::var_os("HOME") {
let path = std::path::Path::new(&home)
.join(".oris")
.join("oris-task-classes.toml");
if path.exists() {
if let Ok(classes) = load_task_classes_from_toml(&path) {
return classes;
}
}
}
}
builtin_task_class_definitions()
}
pub struct TaskClassInferencer {
classes: Vec<TaskClassDefinition>,
threshold: f32,
}
impl TaskClassInferencer {
pub fn new(classes: Vec<TaskClassDefinition>) -> Self {
Self {
classes,
threshold: 0.75,
}
}
pub fn with_builtins() -> Self {
Self::new(builtin_task_class_definitions())
}
pub fn with_threshold(mut self, threshold: f32) -> Self {
self.threshold = threshold;
self
}
pub fn infer(&self, signal_description: &str) -> String {
let signal_tokens = tokenise(signal_description);
if signal_tokens.is_empty() {
return "generic_fix".to_string();
}
let mut best_id = "generic_fix";
let mut best_score = 0.0f32;
for class in &self.classes {
let score = recall_score(&signal_tokens, &class.signal_keywords);
if score > best_score {
best_score = score;
best_id = &class.id;
}
}
if best_score >= self.threshold {
best_id.to_string()
} else {
"generic_fix".to_string()
}
}
pub fn class_definitions(&self) -> &[TaskClassDefinition] {
&self.classes
}
}
fn recall_score(signal_tokens: &[String], keywords: &[String]) -> f32 {
if keywords.is_empty() {
return 0.0;
}
let intersection = keywords
.iter()
.filter(|kw| signal_tokens.contains(kw))
.count();
intersection as f32 / keywords.len() as f32
}
#[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");
}
#[test]
fn inferencer_canonical_compiler_error_missing_import() {
let inferencer = TaskClassInferencer::with_builtins();
let signal = "error[E0425]: cannot find value `foo`: \
unresolved import symbol is undefined missing";
let class_id = inferencer.infer(signal);
assert_eq!(
class_id, "missing-import",
"canonical missing-import signal should infer correct class"
);
}
#[test]
fn inferencer_canonical_compiler_error_type_mismatch() {
let inferencer = TaskClassInferencer::with_builtins();
let signal = "error[E0308]: mismatched type expected u32 found String type mismatch";
let class_id = inferencer.infer(signal);
assert_eq!(class_id, "type-mismatch");
}
#[test]
fn inferencer_score_below_threshold_falls_back_to_generic_fix() {
let inferencer = TaskClassInferencer::with_builtins();
let signal = "e0308";
let class_id = inferencer.infer(signal);
assert_eq!(
class_id, "generic_fix",
"low-match signal must fall back to generic_fix"
);
}
#[test]
fn inferencer_empty_signal_falls_back_to_generic_fix() {
let inferencer = TaskClassInferencer::with_builtins();
assert_eq!(inferencer.infer(""), "generic_fix");
}
#[test]
fn inferencer_custom_threshold_lower_accepts_partial_match() {
let inferencer = TaskClassInferencer::with_builtins().with_threshold(0.3);
let class_id = inferencer.infer("E0308 mismatched");
assert_eq!(class_id, "type-mismatch");
}
#[test]
fn inferencer_builtin_definitions_are_configurable_via_load() {
let defs = load_task_classes();
assert!(
!defs.is_empty(),
"load_task_classes must return at least builtins"
);
let has_missing_import = defs.iter().any(|d| d.id == "missing-import");
assert!(
has_missing_import,
"builtin missing-import class must be present"
);
}
}