use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq)]
pub enum SaveDecision {
Save,
Skip(String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum SearchDecision {
Search,
Skip(String),
}
#[derive(Debug, Clone)]
pub struct GateConfig {
pub min_word_count: usize,
pub max_trivial_ratio: f32,
pub custom_trivial: Vec<String>,
}
impl Default for GateConfig {
fn default() -> Self {
Self {
min_word_count: 3,
max_trivial_ratio: 0.8,
custom_trivial: Vec::new(),
}
}
}
const TRIVIAL_PHRASES: &[&str] = &[
"ok", "yes", "no", "sure", "yep", "nope", "k", "yeah", "nah", "alright",
"right", "cool", "nice", "great", "perfect", "fine", "agreed", "understood",
"noted", "thanks", "ty", "thx", "lol", "lmao", "haha", "hm", "hmm", "ah",
"oh", "hey", "hi", "hello", "bye", "goodbye", "yo", "sup", "wow", "omg",
"brb", "gtg", "idk", "imo", "tbh", "smh", "ikr", "np", "gg", "ez", "rip",
"oof", "yikes", "meh", "duh", "oops", "ugh", "yay", "woo", "okay",
"got it", "sounds good", "makes sense", "that works", "no problem",
"no worries", "of course", "my bad", "my mistake", "will do",
"good point", "fair enough", "for sure", "all good", "thank you",
"good luck", "take care", "see ya", "you too", "same here",
"oh well", "oh no", "ha ha", "he he", "me too",
"ok sounds good", "yes thats right", "no thats wrong", "that makes sense",
"thats fine", "sure thing", "youre right", "i agree", "i see",
"i understand", "ok cool", "yep got it", "sounds great", "no doubt",
"for real", "oh i see", "ok thanks", "thanks a lot", "much appreciated",
];
const TRIVIAL_WORDS: &[&str] = &[
"ok", "yes", "no", "sure", "yeah", "nah", "right", "cool", "nice", "great",
"perfect", "fine", "thanks", "lol", "haha", "wow", "oh", "ah", "hmm", "hey",
"hi", "hello", "bye", "yo", "the", "a", "an", "i", "it", "is", "was", "and",
"or", "but", "so", "just", "very", "really", "too", "also", "well", "like",
"um", "uh",
];
fn normalize(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut prev_space = true; for c in text.chars() {
if c.is_alphanumeric() {
for lc in c.to_lowercase() {
result.push(lc);
}
prev_space = false;
} else if c.is_whitespace() {
if !prev_space && !result.is_empty() {
result.push(' ');
prev_space = true;
}
}
}
if result.ends_with(' ') {
result.pop();
}
result
}
pub struct DecisionGate {
config: GateConfig,
trivial_phrases: HashSet<String>,
trivial_words: HashSet<String>,
}
impl DecisionGate {
pub fn new(config: GateConfig) -> Self {
let mut trivial_phrases: HashSet<String> =
TRIVIAL_PHRASES.iter().map(|s| s.to_string()).collect();
for phrase in &config.custom_trivial {
trivial_phrases.insert(normalize(phrase));
}
let trivial_words: HashSet<String> =
TRIVIAL_WORDS.iter().map(|s| s.to_string()).collect();
Self {
config,
trivial_phrases,
trivial_words,
}
}
pub fn should_save(&self, text: &str) -> SaveDecision {
match self.classify(text) {
Some(reason) => SaveDecision::Skip(reason),
None => SaveDecision::Save,
}
}
pub fn should_search(&self, text: &str) -> SearchDecision {
match self.classify(text) {
Some(reason) => SearchDecision::Skip(reason),
None => SearchDecision::Search,
}
}
fn classify(&self, text: &str) -> Option<String> {
let normalized = normalize(text);
if normalized.is_empty() {
return Some("empty input".to_string());
}
if self.trivial_phrases.contains(&normalized) {
return Some(format!("trivial phrase: {normalized}"));
}
let words: Vec<&str> = normalized.split_whitespace().collect();
if words.len() < self.config.min_word_count {
return Some(format!(
"too few words: {} < {}",
words.len(),
self.config.min_word_count
));
}
let trivial_count = words
.iter()
.filter(|w| self.trivial_words.contains(**w))
.count();
let ratio = trivial_count as f32 / words.len() as f32;
if ratio > self.config.max_trivial_ratio {
return Some(format!(
"high trivial ratio: {ratio:.2} > {:.2}",
self.config.max_trivial_ratio
));
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Instant;
fn default_gate() -> DecisionGate {
DecisionGate::new(GateConfig::default())
}
#[test]
fn test_trivial_single_word_skip() {
let gate = default_gate();
assert!(matches!(gate.should_save("ok"), SaveDecision::Skip(_)));
assert!(matches!(gate.should_save("yes"), SaveDecision::Skip(_)));
assert!(matches!(gate.should_save("lol"), SaveDecision::Skip(_)));
}
#[test]
fn test_nontrivial_save() {
let gate = default_gate();
assert_eq!(
gate.should_save("Tell me about the deployment architecture"),
SaveDecision::Save
);
}
#[test]
fn test_search_trivial_skip() {
let gate = default_gate();
assert!(matches!(gate.should_search("ok"), SearchDecision::Skip(_)));
}
#[test]
fn test_search_nontrivial() {
let gate = default_gate();
assert_eq!(
gate.should_search("What were the Q4 revenue numbers?"),
SearchDecision::Search
);
}
#[test]
fn test_word_count_filter() {
let gate = default_gate();
assert!(matches!(gate.should_save("got"), SaveDecision::Skip(_)));
assert!(matches!(gate.should_save("I see"), SaveDecision::Skip(_)));
}
#[test]
fn test_trivial_ratio_filter() {
let gate = default_gate();
assert!(matches!(
gate.should_save("yes sure yeah cool"),
SaveDecision::Skip(_)
));
}
#[test]
fn test_nontrivial_ratio_passes() {
let gate = default_gate();
assert_eq!(
gate.should_save("The deployment needs a new configuration"),
SaveDecision::Save
);
}
#[test]
fn test_custom_trivial_phrases() {
let config = GateConfig {
custom_trivial: vec!["roger that".to_string()],
..GateConfig::default()
};
let gate = DecisionGate::new(config);
assert!(matches!(
gate.should_save("roger that"),
SaveDecision::Skip(_)
));
}
#[test]
fn test_case_insensitive() {
let gate = default_gate();
assert!(matches!(gate.should_save("OK"), SaveDecision::Skip(_)));
assert!(matches!(gate.should_save("Thanks"), SaveDecision::Skip(_)));
assert!(matches!(gate.should_save("LOL"), SaveDecision::Skip(_)));
}
#[test]
fn test_whitespace_handling() {
let gate = default_gate();
assert!(matches!(
gate.should_save(" ok "),
SaveDecision::Skip(_)
));
assert!(matches!(
gate.should_save(" hello world "),
SaveDecision::Skip(_)
));
}
#[test]
fn test_empty_string() {
let gate = default_gate();
assert!(matches!(gate.should_save(""), SaveDecision::Skip(_)));
}
#[test]
fn test_gate_under_1_microsecond() {
let gate = default_gate();
let limit_us: u128 = if cfg!(debug_assertions) { 50_000 } else { 1_000 };
let start = Instant::now();
for _ in 0..1000 {
std::hint::black_box(gate.should_save("ok"));
}
let trivial_elapsed = start.elapsed();
let start = Instant::now();
for _ in 0..1000 {
std::hint::black_box(gate.should_save("Tell me about deployment architecture"));
}
let nontrivial_elapsed = start.elapsed();
assert!(
trivial_elapsed.as_micros() < limit_us,
"1000 trivial calls took {}µs, expected <{limit_us}µs",
trivial_elapsed.as_micros()
);
assert!(
nontrivial_elapsed.as_micros() < limit_us,
"1000 nontrivial calls took {}µs, expected <{limit_us}µs",
nontrivial_elapsed.as_micros()
);
}
}