use crate::knowledge::thought::ThoughtCategory;
use crate::knowledge::types::{EvidenceCheckResult, MemorySearchResult};
use regex::Regex;
pub fn detect_category(text: &str) -> ThoughtCategory {
let lower = text.to_lowercase();
if contains_any(
&lower,
&[
"decided",
"chose",
"going with",
"picked",
"selected",
"settled on",
"committed to",
],
) {
return ThoughtCategory::Decision;
}
static PERSON_RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"(?i)\b(?:spoke to|met with|talked to|met|told)\s+[A-Z][a-z]+")
.expect("valid regex")
});
if PERSON_RE.is_match(text) {
return ThoughtCategory::Person;
}
if contains_any(
&lower,
&[
"noticed",
"realized",
"learned",
"discovered",
"turns out",
"interesting that",
"observation",
],
) {
return ThoughtCategory::Insight;
}
if contains_any(
&lower,
&[
"need to",
"todo:",
"todo ",
"must ",
"action item",
"follow up",
"by friday",
"by monday",
"by end of",
],
) {
return ThoughtCategory::ActionItem;
}
if contains_any(
&lower,
&[
"what if",
"idea:",
"could we",
"how about",
"maybe we",
"brainstorm",
"experiment with",
],
) {
return ThoughtCategory::Idea;
}
static MEETING_RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"(?i)\b(?:standup|meeting|discussed|retro|sprint|call with|1:1)\b|\bsync\b")
.expect("valid regex")
});
if MEETING_RE.is_match(text) {
return ThoughtCategory::MeetingNote;
}
static URL_RE: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"https?://").expect("valid regex"));
if URL_RE.is_match(text)
|| contains_any(&lower, &["docs at", "reference:", "link:", "see also"])
{
return ThoughtCategory::Reference;
}
ThoughtCategory::General
}
fn contains_any(haystack: &str, needles: &[&str]) -> bool {
needles.iter().any(|n| haystack.contains(n))
}
const NEGATION_WORDS: &[&str] = &[
"not ",
"never",
"no ",
"don't",
"doesn't",
"isn't",
"aren't",
"won't",
"can't",
"cannot",
"didn't",
"wasn't",
"weren't",
"shouldn't",
"wouldn't",
];
pub fn check_corroboration(results: &[MemorySearchResult], threshold: f32) -> EvidenceCheckResult {
let corroborations = results
.iter()
.filter(|r| r.score >= threshold)
.filter_map(|r| r.thought_id.clone())
.collect();
EvidenceCheckResult {
corroborations,
contradictions: Vec::new(),
}
}
pub fn check_contradiction(
new_content: &str,
results: &[MemorySearchResult],
threshold: f32,
) -> Vec<String> {
let new_lower = new_content.to_lowercase();
let new_has_negation = contains_any(&new_lower, NEGATION_WORDS);
results
.iter()
.filter(|r| r.score >= threshold)
.filter(|r| {
let existing_lower = r.content.to_lowercase();
let existing_has_negation = contains_any(&existing_lower, NEGATION_WORDS);
new_has_negation != existing_has_negation
})
.filter_map(|r| r.thought_id.clone())
.collect()
}
pub fn extract_tags(text: &str) -> Vec<String> {
let mut tags = Vec::new();
static HASHTAG_RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"#([A-Za-z][A-Za-z0-9_-]{1,30})").expect("valid regex")
});
for cap in HASHTAG_RE.captures_iter(text) {
let tag = cap[1].to_lowercase();
if !tags.contains(&tag) {
tags.push(tag);
}
}
tags
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decision_detection() {
assert_eq!(
detect_category("Decided to use PostgreSQL for the auth service"),
ThoughtCategory::Decision
);
assert_eq!(
detect_category("Going with React for the frontend"),
ThoughtCategory::Decision
);
}
#[test]
fn test_person_detection() {
assert_eq!(
detect_category("Spoke to Sarah about the deadline"),
ThoughtCategory::Person
);
assert_eq!(
detect_category("Met with John to discuss the architecture"),
ThoughtCategory::Person
);
}
#[test]
fn test_insight_detection() {
assert_eq!(
detect_category("Noticed that batch processing is 3x faster with async"),
ThoughtCategory::Insight
);
assert_eq!(
detect_category("Realized the bottleneck is in the serialization"),
ThoughtCategory::Insight
);
}
#[test]
fn test_meeting_note_detection() {
assert_eq!(
detect_category("Standup: team agreed to prioritize the auth refactor"),
ThoughtCategory::MeetingNote
);
}
#[test]
fn test_idea_detection() {
assert_eq!(
detect_category("What if we used WebSockets instead of polling?"),
ThoughtCategory::Idea
);
assert_eq!(
detect_category("Idea: cache the embeddings in Redis"),
ThoughtCategory::Idea
);
}
#[test]
fn test_action_item_detection() {
assert_eq!(
detect_category("Need to review PR #234 before Friday"),
ThoughtCategory::ActionItem
);
assert_eq!(
detect_category("TODO: update the API docs"),
ThoughtCategory::ActionItem
);
}
#[test]
fn test_reference_detection() {
assert_eq!(
detect_category("The API docs are at https://docs.example.com"),
ThoughtCategory::Reference
);
}
#[test]
fn test_general_fallback() {
assert_eq!(
detect_category("Just a random note"),
ThoughtCategory::General
);
}
#[test]
fn test_tag_extraction() {
let tags = extract_tags("Working on #rust and #mcp-server today");
assert!(tags.contains(&"rust".to_string()));
assert!(tags.contains(&"mcp-server".to_string()));
}
}