use std::sync::Arc;
use tokio::sync::Mutex;
use car_memgine::graph::{SkillOutcome, SkillTrigger, StructuredTrigger};
use car_memgine::MemgineEngine;
use super::contract::CheckResult;
const REPAIR_KIND: &str = "coder_repair";
const REPAIR_PERSONA: &str = "car-coder";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FailureSignature {
pub check: String,
pub error_class: String,
}
impl FailureSignature {
pub fn from_check(result: &CheckResult) -> Self {
Self {
check: normalize(&result.name),
error_class: classify(result),
}
}
pub fn key(&self) -> String {
format!("{}::{}", self.check, self.error_class)
}
}
fn normalize(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut prev_us = false;
for c in s.chars() {
if c.is_ascii_alphanumeric() {
out.push(c.to_ascii_lowercase());
prev_us = false;
} else if !prev_us {
out.push('_');
prev_us = true;
}
}
out.trim_matches('_').to_string()
}
fn classify(result: &CheckResult) -> String {
let tail = result.output_tail.to_ascii_lowercase();
if tail.contains("error[e")
|| tail.contains("cannot find")
|| tail.contains("mismatched types")
|| tail.contains("no method named")
|| tail.contains("unresolved import")
|| tail.contains("syntaxerror")
|| tail.contains("compilation failed")
{
return "compile_error".to_string();
}
if tail.contains("test result: failed")
|| tail.contains("assertion")
|| tail.contains("panicked")
|| tail.contains("failures:")
{
return "test_failure".to_string();
}
if tail.contains("command not found") || tail.contains("no such file") {
return "missing_command".to_string();
}
match result.exit_code {
Some(code) => format!("exit_{code}"),
None => "nonzero".to_string(),
}
}
#[derive(Clone)]
pub struct RepairMemory {
engine: Option<Arc<Mutex<MemgineEngine>>>,
}
impl RepairMemory {
pub fn new(engine: Option<Arc<Mutex<MemgineEngine>>>) -> Self {
Self { engine }
}
pub fn disabled() -> Self {
Self { engine: None }
}
pub fn enabled(&self) -> bool {
self.engine.is_some()
}
fn skill_name(sig: &FailureSignature) -> String {
format!("coder_repair::{}", sig.key())
}
pub async fn recall(&self, sig: &FailureSignature) -> Option<String> {
let engine = self.engine.as_ref()?;
let guard = engine.lock().await;
let name = Self::skill_name(sig);
let meta = guard
.find_skill(REPAIR_PERSONA, "", &sig.key(), 8)
.into_iter()
.map(|(m, _)| m)
.find(|m| m.name == name)
.or_else(|| guard.skill_meta(&name))?;
if meta.code.trim().is_empty() {
return None;
}
Some(meta.code)
}
pub async fn record_failure(&self, sig: &FailureSignature) {
let Some(engine) = self.engine.as_ref() else {
return;
};
let mut guard = engine.lock().await;
let name = Self::skill_name(sig);
if skill_exists(&guard, &name) {
let _ = guard.report_outcome(&name, SkillOutcome::Fail);
}
}
pub async fn record_success(&self, sig: &FailureSignature, approach: &str) {
let Some(engine) = self.engine.as_ref() else {
return;
};
let mut guard = engine.lock().await;
let name = Self::skill_name(sig);
if skill_exists(&guard, &name) {
let _ = guard.report_outcome(&name, SkillOutcome::Success);
return;
}
let trigger = SkillTrigger {
persona: REPAIR_PERSONA.to_string(),
url_pattern: String::new(),
task_keywords: vec![sig.key(), sig.check.clone(), sig.error_class.clone()],
structured: Some(StructuredTrigger {
kind: REPAIR_KIND.to_string(),
signature: serde_json::json!({
"check": sig.check,
"error_class": sig.error_class,
}),
}),
};
let description = format!(
"Repair approach that resolved a '{}' failure of check '{}'.",
sig.error_class, sig.check
);
guard.ingest_skill(
&name,
approach,
"coder",
trigger,
&description,
None,
Vec::new(),
Vec::new(),
);
let _ = guard.report_outcome(&name, SkillOutcome::Success);
}
}
fn skill_exists(engine: &MemgineEngine, name: &str) -> bool {
engine.skill_meta(name).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
fn failed(name: &str, exit: Option<i64>, tail: &str) -> CheckResult {
CheckResult {
name: name.into(),
passed: false,
exit_code: exit,
output_tail: tail.into(),
duration_ms: 1,
}
}
fn mem() -> RepairMemory {
RepairMemory::new(Some(Arc::new(Mutex::new(MemgineEngine::new(None)))))
}
#[test]
fn signature_normalizes_and_classifies() {
let sig = FailureSignature::from_check(&failed(
"Cargo Tests",
Some(101),
"error[E0433]: cannot find crate",
));
assert_eq!(sig.check, "cargo_tests");
assert_eq!(sig.error_class, "compile_error");
assert_eq!(sig.key(), "cargo_tests::compile_error");
}
#[test]
fn classify_buckets_are_coarse_and_stable() {
assert_eq!(
FailureSignature::from_check(&failed("t", Some(1), "test result: FAILED. 1 failed"))
.error_class,
"test_failure"
);
assert_eq!(
FailureSignature::from_check(&failed("t", Some(127), "bash: foo: command not found"))
.error_class,
"missing_command"
);
assert_eq!(
FailureSignature::from_check(&failed("t", Some(2), "something opaque")).error_class,
"exit_2"
);
}
#[tokio::test]
async fn disabled_memory_is_a_total_noop() {
let m = RepairMemory::disabled();
assert!(!m.enabled());
let sig = FailureSignature::from_check(&failed("t", Some(1), "boom"));
m.record_failure(&sig).await;
m.record_success(&sig, "fix it").await;
assert_eq!(m.recall(&sig).await, None);
}
#[tokio::test]
async fn success_ingests_then_recalls_the_approach() {
let m = mem();
let sig = FailureSignature::from_check(&failed("build", Some(101), "mismatched types"));
assert_eq!(m.recall(&sig).await, None, "nothing learned yet");
m.record_success(&sig, "cargo fix --allow-dirty then re-add the import")
.await;
let recalled = m.recall(&sig).await.expect("approach should be recalled");
assert!(recalled.contains("cargo fix"));
}
#[tokio::test]
async fn second_success_credits_the_same_skill_not_a_duplicate() {
let m = mem();
let sig = FailureSignature::from_check(&failed("tests", Some(101), "assertion failed"));
m.record_success(&sig, "first approach").await;
m.record_success(&sig, "different text").await;
let engine = m.engine.as_ref().unwrap().lock().await;
let skill = engine
.skill_meta(&RepairMemory::skill_name(&sig))
.expect("exactly one skill per signature");
assert_eq!(skill.code, "first approach", "approach preserved");
assert_eq!(skill.stats.success_count, 2);
}
#[tokio::test]
async fn failure_penalizes_an_existing_skill_only() {
let m = mem();
let sig = FailureSignature::from_check(&failed("tests", Some(101), "panicked"));
m.record_failure(&sig).await;
assert_eq!(m.recall(&sig).await, None);
m.record_success(&sig, "the fix").await;
m.record_failure(&sig).await;
let engine = m.engine.as_ref().unwrap().lock().await;
let skill = engine
.skill_meta(&RepairMemory::skill_name(&sig))
.unwrap();
assert_eq!(skill.stats.fail_count, 1);
assert_eq!(skill.stats.success_count, 1);
}
}