remem-ai 0.5.145

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::collections::HashSet;

use crate::memory::Memory;

pub(super) fn claim_terms(
    query_text: &str,
    core_terms: &[String],
    project: Option<&str>,
) -> Vec<String> {
    let entity_terms: HashSet<String> = crate::retrieval::entity::extract_entities("", query_text)
        .into_iter()
        .filter_map(|term| normalize_claim_token(&term))
        .collect();
    let project_terms: HashSet<String> = project
        .into_iter()
        .flat_map(|project| project.split(|c: char| !c.is_alphanumeric() && !is_cjk(c)))
        .filter_map(normalize_claim_token)
        .collect();

    core_terms
        .iter()
        .filter_map(|term| normalize_claim_token(term))
        .filter(|term| !entity_terms.contains(term) && !project_terms.contains(term))
        .collect()
}

pub(super) fn claim_term_coverage(memory: &Memory, claim_terms: &[String]) -> f64 {
    if claim_terms.is_empty() {
        return 1.0;
    }
    let haystack = format!("{} {}", memory.title, memory.text).to_lowercase();
    let matched = claim_terms
        .iter()
        .filter(|term| claim_term_matches(&haystack, term))
        .count();
    matched as f64 / claim_terms.len() as f64
}

fn claim_term_matches(haystack: &str, term: &str) -> bool {
    if haystack.contains(term) {
        return true;
    }
    if claim_term_aliases(term)
        .iter()
        .any(|alias| haystack.contains(alias))
    {
        return true;
    }
    claim_term_stems(term)
        .iter()
        .any(|stem| stem.chars().count() >= 3 && haystack.contains(stem.as_str()))
}

fn claim_term_aliases(term: &str) -> &'static [&'static str] {
    match term {
        "child" | "children" | "kid" | "kids" => &[
            "child",
            "children",
            "kid",
            "kids",
            "son",
            "daughter",
            "sons",
            "daughters",
        ],
        _ => &[],
    }
}

fn claim_term_stems(term: &str) -> Vec<String> {
    let mut stems = Vec::new();
    if let Some(stem) = term.strip_suffix("ing") {
        stems.push(stem.to_string());
    }
    if let Some(stem) = term.strip_suffix("ed") {
        stems.push(stem.to_string());
        stems.push(format!("{stem}e"));
    }
    if let Some(stem) = term.strip_suffix('s') {
        stems.push(stem.to_string());
    }
    stems
}

fn normalize_claim_token(term: &str) -> Option<String> {
    let normalized = term
        .trim_matches(|c: char| !c.is_alphanumeric() && !is_cjk(c))
        .to_lowercase();
    let min_len = if normalized.chars().any(is_cjk) { 2 } else { 3 };
    if normalized.chars().count() < min_len || is_generic_query_term(&normalized) {
        None
    } else {
        Some(normalized)
    }
}

fn is_generic_query_term(term: &str) -> bool {
    matches!(
        term,
        "all"
            | "and"
            | "are"
            | "did"
            | "does"
            | "for"
            | "from"
            | "current"
            | "had"
            | "has"
            | "have"
            | "handles"
            | "how"
            | "into"
            | "is"
            | "its"
            | "latest"
            | "onto"
            | "project"
            | "show"
            | "that"
            | "the"
            | "this"
            | "through"
            | "today"
            | "tomorrow"
            | "yesterday"
            | "before"
            | "after"
            | "during"
            | "only"
            | "production"
            | "was"
            | "were"
            | "what"
            | "when"
            | "where"
            | "which"
            | "who"
            | "why"
            | "with"
    )
}

fn is_cjk(c: char) -> bool {
    matches!(
        c,
        '\u{4E00}'..='\u{9FFF}' | '\u{3400}'..='\u{4DBF}' | '\u{F900}'..='\u{FAFF}'
    )
}