use super::candidate::SkillCandidate;
#[derive(Debug, Clone, PartialEq)]
pub enum Verdict {
Keep,
Skip(String),
}
const MIN_BODY_LEN: usize = 25;
const NOISE: [&str; 9] = [
"fixed bug",
"updated code",
"wip",
"work in progress",
"todo",
"refactored",
"cleanup",
"minor change",
"various changes",
];
fn is_generic(body: &str) -> bool {
let trimmed = body.trim().to_ascii_lowercase();
NOISE.iter().any(|n| {
trimmed == *n || (trimmed.starts_with(n) && trimmed.chars().count() < n.len() + 12)
})
}
pub fn judge(c: &SkillCandidate, min_confidence: f32, min_recurrence: u32) -> Verdict {
if c.body.chars().count() < MIN_BODY_LEN {
return Verdict::Skip("too short".to_string());
}
if is_generic(&c.body) {
return Verdict::Skip("generic / one-off phrasing".to_string());
}
if c.recurrence >= min_recurrence || c.confidence >= min_confidence {
Verdict::Keep
} else {
Verdict::Skip(format!(
"one-off (recurrence {} < {min_recurrence}, confidence {:.2} < {min_confidence:.2})",
c.recurrence, c.confidence
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cand(body: &str, recurrence: u32, confidence: f32) -> SkillCandidate {
SkillCandidate {
slug: "s".into(),
title: "t".into(),
body: body.into(),
category: "decision".into(),
recurrence,
confidence,
sources: vec![],
}
}
#[test]
fn rejects_too_short() {
assert!(matches!(
judge(&cand("short", 5, 0.9), 0.7, 2),
Verdict::Skip(_)
));
}
#[test]
fn rejects_generic() {
assert!(matches!(
judge(&cand("fixed bug", 5, 0.9), 0.7, 2),
Verdict::Skip(_)
));
}
#[test]
fn keeps_recurring_pattern() {
let c = cand(
"Always run lean-ctx stop before building the binary.",
2,
0.5,
);
assert_eq!(judge(&c, 0.7, 2), Verdict::Keep);
}
#[test]
fn keeps_high_confidence_single() {
let c = cand(
"Always run lean-ctx stop before building the binary.",
1,
0.85,
);
assert_eq!(judge(&c, 0.7, 2), Verdict::Keep);
}
#[test]
fn skips_low_signal_oneoff() {
let c = cand(
"Always run lean-ctx stop before building the binary.",
1,
0.5,
);
assert!(matches!(judge(&c, 0.7, 2), Verdict::Skip(_)));
}
}