use crate::classify::taxonomy::TopLevelCategory;
use crate::classify::tiers::ClassificationResult;
use crate::core::models::ClassificationMethod;
pub struct FuzzyClassifier;
impl FuzzyClassifier {
pub fn classify(&self, message: &str, is_merge: bool) -> Option<ClassificationResult> {
let trimmed = message.trim();
let lower = trimmed.to_lowercase();
if is_merge
|| lower.starts_with("merge pull request")
|| lower.starts_with("merge branch")
|| lower.starts_with("merge remote-tracking")
{
return Some(ClassificationResult {
category: "merge".to_string(),
subcategory: None,
top_level: Some(TopLevelCategory::Maintenance),
confidence: 0.95,
method: ClassificationMethod::FuzzyMatch,
ticket_id: None,
complexity: None,
});
}
if lower.starts_with("revert ") || lower.starts_with("revert:") {
return Some(ClassificationResult {
category: "revert".to_string(),
subcategory: None,
top_level: Some(TopLevelCategory::Maintenance),
confidence: 0.9,
method: ClassificationMethod::FuzzyMatch,
ticket_id: None,
complexity: None,
});
}
if let Some(ticket) = bare_ticket_prefix(trimmed) {
return Some(ClassificationResult {
category: "feature".to_string(),
subcategory: Some("ticketed".to_string()),
top_level: Some(TopLevelCategory::Feature),
confidence: 0.6,
method: ClassificationMethod::FuzzyMatch,
ticket_id: Some(ticket),
complexity: None,
});
}
if trimmed.len() < 12 && !trimmed.is_empty() {
return Some(ClassificationResult {
category: "chore".to_string(),
subcategory: None,
top_level: Some(TopLevelCategory::Maintenance),
confidence: 0.4,
method: ClassificationMethod::FuzzyMatch,
ticket_id: None,
complexity: None,
});
}
None
}
}
fn bare_ticket_prefix(message: &str) -> Option<String> {
let first = message.split_whitespace().next()?;
let candidate = first.trim_end_matches([':', '-', ',']);
let mut parts = candidate.split('-');
let project = parts.next()?;
let number = parts.next()?;
if parts.next().is_some() {
return None;
}
if project.is_empty() || number.is_empty() {
return None;
}
if !project
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
{
return None;
}
if !project.chars().next()?.is_ascii_uppercase() {
return None;
}
if !number.chars().all(|c| c.is_ascii_digit()) {
return None;
}
Some(format!("{project}-{number}"))
}