diff --git a/commit_check/ai_signatures_data.py b/commit_check/ai_signatures_data.py index 2924eea9..d247a405 100644 --- a/commit_check/ai_signatures_data.py +++ b/commit_check/ai_signatures_data.py @@ -89,6 +89,14 @@ def _body_marker(pattern: str, description: str = "") -> AiSignaturePattern: r"|\d+\+Claude@users\.noreply\.github\.com)>)?", "``Co-authored-by: Claude`` trailer", ), + # Any co-author name with the Anthropic noreply email — catches + # model-name variants such as "Claude Opus 4.5 (1M context)" that + # the pattern above misses. + _trailer( + "Co-authored-by", + r"[^<\n]*", + "``Co-authored-by`` with Anthropic noreply email", + ), # Assisted-by trailer (Linux kernel style, with optional tool list) _trailer( "Assisted-by", @@ -223,6 +231,16 @@ def _body_marker(pattern: str, description: str = "") -> AiSignaturePattern: r"(?:claude|gpt|gemini)[\w.]*-[\w.-]+(?:\s*<[^>]*>)?", "``Co-authored-by`` with AI model name", ), + # Catch space-separated AI model identifiers in Co-authored-by + # (e.g. "Claude Opus 4.5", "Gemini 2.5 Pro", "GPT 4 Turbo"). + # A purely numeric version token is required so human names with + # ordinals ("Claude Dubois 3rd") are NOT flagged. + _trailer( + "Co-authored-by", + r"(?:claude|gpt|gemini)(?:\s+[a-z]+)*\s+\d+(?:\.\d+)*(?!\w)" + r"(?:\s+[a-z]+)*(?:\s*\([^)]*\))?(?:\s*<[^>]*>)?", + "``Co-authored-by`` with space-separated AI model name", + ), # Catch Assisted-by trailer (Linux kernel style) regardless of agent, # with optional trailing tool list. _trailer( diff --git a/tests/ai_signatures_test.py b/tests/ai_signatures_test.py index 4a755e25..ba3b1931 100644 --- a/tests/ai_signatures_test.py +++ b/tests/ai_signatures_test.py @@ -47,6 +47,35 @@ def test_human_claude_with_personal_email_ignored(self): claude_hits = [s for s in result if s["tool"] == "Claude Code"] assert len(claude_hits) == 0 + @pytest.mark.benchmark + def test_claude_model_name_with_noreply_email(self): + """Model-name co-author with anthropic noreply is detected.""" + message = ( + "feat: add feature\n\n" + "Co-authored-by: Claude Opus 4.5 (1M context) " + ) + result = detect_ai_signatures(message) + assert any(s["tool"] == "Claude Code" for s in result) + + @pytest.mark.benchmark + def test_space_separated_model_name_detected(self): + """Space-separated model names with a version are detected.""" + for trailer in ( + "Co-authored-by: Claude Sonnet 4.5 ", + "Co-authored-by: Gemini 2.5 Pro ", + "Co-authored-by: GPT 4 Turbo", + ): + message = f"feat: add feature\n\n{trailer}" + assert has_ai_signature(message), trailer + + @pytest.mark.benchmark + def test_human_name_with_ordinal_ignored(self): + """A human name with an ordinal suffix is NOT detected.""" + message = ( + "feat: add feature\n\nCo-authored-by: Claude Dubois 3rd " + ) + assert not has_ai_signature(message) + @pytest.mark.benchmark def test_copilot_with_noreply_email(self): """Co-authored-by: Copilot with GitHub noreply is detected."""