use std::collections::HashMap;
pub const COMMAND_PATTERN_THRESHOLD: usize = 4;
#[derive(Debug, Clone, PartialEq)]
pub struct CommandCandidate {
pub signature: String,
pub count: usize,
pub samples: Vec<String>,
}
pub fn command_candidates(requests: &[String], threshold: usize) -> Vec<CommandCandidate> {
let mut groups: HashMap<String, Vec<String>> = HashMap::new();
for raw in requests {
let Some(sig) = signature(raw) else {
continue;
};
groups.entry(sig).or_default().push(raw.trim().to_string());
}
let mut out: Vec<CommandCandidate> = groups
.into_iter()
.filter(|(_, v)| v.len() >= threshold)
.map(|(signature, originals)| {
let mut samples: Vec<String> = Vec::new();
for o in &originals {
if !samples.contains(o) {
samples.push(o.clone());
}
if samples.len() == 3 {
break;
}
}
CommandCandidate {
signature,
count: originals.len(),
samples,
}
})
.collect();
out.sort_by(|a, b| {
b.count
.cmp(&a.count)
.then_with(|| a.signature.cmp(&b.signature))
});
out
}
fn signature(raw: &str) -> Option<String> {
let t = raw.trim();
if t.is_empty() || t.starts_with('/') {
return None;
}
let t: String = t
.split_whitespace()
.skip_while(|w| w.starts_with('@'))
.collect::<Vec<_>>()
.join(" ");
let lower = t.to_lowercase();
let words: Vec<&str> = lower
.split(|c: char| !c.is_alphanumeric())
.filter(|w| !w.is_empty())
.collect();
if words.len() < 2 || words.len() > 6 {
return None;
}
Some(words.join(" "))
}