difflore-cli 0.2.0

Your AI coding agent learned public code, not your team's private decisions. difflore turns past PR reviews into source-backed local rules.
use crate::hook::adapters::types::HookResult;

const REMEMBER_NUDGE: &str = "DiffLore memory nudge: the user explicitly asked for this to be remembered. Consider turning it into a candidate preference/rule through the existing memory flow; do not silently persist sensitive or one-off data.";

const ENGLISH_POSITIVE_PHRASES: &[&str] = &[
    "remember this",
    "please remember",
    "from now on",
    "next time don't",
    "next time dont",
    "next time do not",
];

const ENGLISH_NEGATIVE_PHRASES: &[&str] = &[
    "don't remember this",
    "dont remember this",
    "do not remember this",
    "can't remember this",
    "cannot remember this",
    "not remember this",
    "no need to remember this",
];

const CHINESE_POSITIVE_PHRASES: &[&str] = &["记住这个", "帮我记住", "以后都这样", "下次不要"];
const CHINESE_NEGATIVE_PHRASES: &[&str] = &["别记住这个", "不要记住这个", "不用记住", "无需记住"];

pub(super) fn nudge_for_prompt(prompt: &str) -> Option<HookResult> {
    has_explicit_remember_intent(prompt).then(|| HookResult::with_context(REMEMBER_NUDGE))
}

fn has_explicit_remember_intent(prompt: &str) -> bool {
    let prompt = prompt.trim();
    if prompt.is_empty() {
        return false;
    }

    let lower = prompt.to_ascii_lowercase();
    if contains_any_english_phrase(&lower, ENGLISH_NEGATIVE_PHRASES)
        || contains_any(prompt, CHINESE_NEGATIVE_PHRASES)
    {
        return false;
    }

    contains_any_english_phrase(&lower, ENGLISH_POSITIVE_PHRASES)
        || contains_any(prompt, CHINESE_POSITIVE_PHRASES)
}

fn contains_any(haystack: &str, needles: &[&str]) -> bool {
    needles.iter().any(|needle| haystack.contains(needle))
}

fn contains_any_english_phrase(haystack: &str, needles: &[&str]) -> bool {
    needles
        .iter()
        .any(|needle| contains_english_phrase(haystack, needle))
}

fn contains_english_phrase(haystack: &str, needle: &str) -> bool {
    let mut search_from = 0;
    while let Some(relative_start) = haystack[search_from..].find(needle) {
        let start = search_from + relative_start;
        let end = start + needle.len();
        let before_ok = start == 0
            || !haystack.as_bytes()[start - 1].is_ascii_alphanumeric()
                && haystack.as_bytes()[start - 1] != b'_';
        let after_ok = end == haystack.len()
            || !haystack.as_bytes()[end].is_ascii_alphanumeric()
                && haystack.as_bytes()[end] != b'_';
        if before_ok && after_ok {
            return true;
        }
        search_from = end;
    }
    false
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::hook::adapters::PlatformAdapter;

    #[test]
    fn detects_chinese_explicit_memory_intent() {
        let result = nudge_for_prompt("帮我记住:这个项目里不要自动改 installer manifest。")
            .expect("explicit Chinese remember request should nudge");

        let ctx = result.additional_context.expect("nudge context");
        assert!(ctx.contains("explicitly asked"));
        assert!(ctx.contains("do not silently persist"));
    }

    #[test]
    fn detects_english_explicit_memory_intent() {
        assert!(
            nudge_for_prompt("Please remember: when I say quick pass, only run focused tests.")
                .is_some()
        );
        assert!(nudge_for_prompt("From now on, prefer compact status summaries.").is_some());
    }

    #[test]
    fn broad_memory_or_negated_mentions_stay_noop() {
        assert!(nudge_for_prompt("Can you explain how session memory works?").is_none());
        assert!(nudge_for_prompt("I don't remember this error from yesterday.").is_none());
        assert!(nudge_for_prompt("不用记住这个临时 token。").is_none());
    }

    #[test]
    fn english_phrase_matching_requires_word_boundaries() {
        assert!(nudge_for_prompt("Please remember this: use focused tests.").is_some());
        assert!(nudge_for_prompt("remember thiserror string.").is_none());
        assert!(nudge_for_prompt("prefixremember this: no boundary before.").is_none());
    }

    #[test]
    fn claude_output_surfaces_user_prompt_submit_nudge_as_context() {
        let mut result = nudge_for_prompt("remember this: use focused hook tests")
            .expect("explicit remember request should nudge");
        result.event_name = Some("UserPromptSubmit".to_owned());

        let adapter = crate::hook::adapters::claude_code::ClaudeCodeAdapter;
        let out = adapter.format_output(result);
        let value: serde_json::Value = serde_json::from_str(&out).expect("valid json");

        assert_eq!(value["continue"], true);
        assert_eq!(
            value["hookSpecificOutput"]["hookEventName"],
            "UserPromptSubmit"
        );
        assert!(
            value["hookSpecificOutput"]["additionalContext"]
                .as_str()
                .expect("additional context")
                .contains("candidate preference/rule")
        );
    }
}