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 PRE_SUBMIT_NUDGE: &str = "DiffLore pre-submit review: before committing, pushing, or opening a PR, run `difflore review --diff all`; fix real findings, then run it again. Use the full flow in `difflore://skills/pre-submit-review`. Do not commit, push, open a PR, or apply broad rewrites unless the user explicitly asks.";

const ENGLISH_POSITIVE_PHRASES: &[&str] = &[
    "pre-submit",
    "pre submit",
    "before commit",
    "before committing",
    "before push",
    "before pushing",
    "before pr",
    "before opening a pr",
    "before pull request",
    "before submitting",
    "ready to commit",
    "ready to push",
    "open a pr",
    "create a pr",
    "submit code",
    "final code review",
    "final review before",
    "ship this",
    "ship it",
];

const ENGLISH_NEGATIVE_PHRASES: &[&str] = &[
    "don't commit",
    "dont commit",
    "do not commit",
    "don't push",
    "dont push",
    "do not push",
    "don't open a pr",
    "do not open a pr",
];

const CHINESE_POSITIVE_PHRASES: &[&str] = &[
    "提交前",
    "提交代码前",
    "准备提交",
    "帮我提交",
    "推送前",
    "推送代码",
    "发pr",
    "发 pr",
    "提pr",
    "提 pr",
    "开pr",
    "开 pr",
    "pr前",
    "pr 前",
    "合并前",
    "最终检查",
    "最后检查一下",
    "发布前检查",
];

const CHINESE_NEGATIVE_PHRASES: &[&str] = &[
    "不要提交",
    "先别提交",
    "不用提交",
    "不要推送",
    "先别推送",
    "不用推送",
];

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

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

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

    contains_any(&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))
}

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

    #[test]
    fn detects_english_pre_submit_intent() {
        let result = nudge_for_prompt("Before committing, can you do the final code review?")
            .expect("pre-submit wording should nudge");

        let ctx = result.additional_context.expect("nudge context");
        assert!(ctx.contains("difflore review --diff all"));
        assert!(ctx.contains("difflore://skills/pre-submit-review"));
        assert!(ctx.contains("Do not commit"));
    }

    #[test]
    fn detects_chinese_pre_submit_intent() {
        assert!(nudge_for_prompt("提交前帮我最后检查一下").is_some());
        assert!(nudge_for_prompt("准备提交到远程了,先检查").is_some());
    }

    #[test]
    fn broad_git_or_negated_mentions_stay_noop() {
        assert!(nudge_for_prompt("Can you explain git commit message style?").is_none());
        assert!(nudge_for_prompt("Do not commit yet; just inspect the status.").is_none());
        assert!(nudge_for_prompt("先别提交,看看 diff。").is_none());
    }

    #[test]
    fn claude_output_surfaces_pre_submit_nudge_as_context() {
        let mut result =
            nudge_for_prompt("ready to push; please do a final review").expect("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("pre-submit-review")
        );
    }
}