Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions detection/coauthor/coauthor.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var knownCoAuthorEmails = map[string]string{
"noreply@aider.chat": "Aider",
}

var coAuthorPattern = regexp.MustCompile(`(?im)^co-authored-by:\s*[^<]*<([^>]+)>`)
var coAuthorPattern = regexp.MustCompile(`(?im)^co-authored-by:\s*([^<]*)<([^>]+)>`)

type Detector struct{}

Expand All @@ -30,11 +30,27 @@ func (d *Detector) Detect(input detection.Input) []detection.Finding {
seen := map[string]bool{}

for _, match := range matches {
email := strings.ToLower(strings.TrimSpace(match[1]))
namePart := strings.TrimSpace(match[1])
email := strings.ToLower(strings.TrimSpace(match[2]))
if name, ok := knownCoAuthorEmails[email]; ok && !seen[name] {
model := ""
if name == "Aider" && strings.Contains(namePart, "(") {
start := strings.Index(namePart, "(")
end := strings.Index(namePart, ")")
if end > start {
model = namePart[start+1 : end]
}
} else if name == "Claude Code" {
model = namePart
if model == "Claude" {
model = ""
}
}

findings = append(findings, detection.Finding{
Detector: d.Name(),
Tool: name,
Model: model,
Confidence: detection.ConfidenceHigh,
Detail: fmt.Sprintf("Co-Authored-By trailer with email %s", email),
})
Expand Down
98 changes: 59 additions & 39 deletions detection/coauthor/coauthor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,78 +9,93 @@ import (
func TestDetect(t *testing.T) {
d := &Detector{}
tests := []struct {
name string
message string
wantTools []string
name string
message string
wantTools []string
wantModels []string
}{
{
name: "Claude trailer with Opus model",
message: "fix: update handler\n\nCo-Authored-By: Claude Opus 4 <noreply@anthropic.com>",
wantTools: []string{"Claude Code"},
name: "Claude trailer with Opus model",
message: "fix: update handler\n\nCo-Authored-By: Claude Opus 4 <noreply@anthropic.com>",
wantTools: []string{"Claude Code"},
wantModels: []string{"Claude Opus 4"},
},
{
name: "Claude trailer with Sonnet model",
message: "fix: update handler\n\nCo-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>",
wantTools: []string{"Claude Code"},
name: "Claude trailer with Sonnet model",
message: "fix: update handler\n\nCo-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>",
wantTools: []string{"Claude Code"},
wantModels: []string{"Claude Sonnet 4"},
},
{
name: "Cursor trailer",
message: "refactor: extract method\n\nCo-Authored-By: Cursor <cursoragent@cursor.com>",
wantTools: []string{"Cursor"},
name: "Cursor trailer",
message: "refactor: extract method\n\nCo-Authored-By: Cursor <cursoragent@cursor.com>",
wantTools: []string{"Cursor"},
wantModels: []string{""},
},
{
name: "Aider trailer with model name",
message: "feat: add endpoint\n\nCo-Authored-By: aider (gpt-4o) <noreply@aider.chat>",
wantTools: []string{"Aider"},
name: "Aider trailer with model name",
message: "feat: add endpoint\n\nCo-Authored-By: aider (gpt-4o) <noreply@aider.chat>",
wantTools: []string{"Aider"},
wantModels: []string{"gpt-4o"},
},
{
name: "Aider trailer with different model",
message: "feat: add endpoint\n\nCo-Authored-By: aider (claude-3.5-sonnet) <noreply@aider.chat>",
wantTools: []string{"Aider"},
name: "Aider trailer with different model",
message: "feat: add endpoint\n\nCo-Authored-By: aider (claude-3.5-sonnet) <noreply@aider.chat>",
wantTools: []string{"Aider"},
wantModels: []string{"claude-3.5-sonnet"},
Comment thread
abhinavgautam01 marked this conversation as resolved.
},
{
name: "multiple trailers with Claude and human",
message: "fix: bug\n\nCo-Authored-By: Claude Opus 4 <noreply@anthropic.com>\nCo-Authored-By: Alice <alice@example.com>",
wantTools: []string{"Claude Code"},
name: "multiple trailers with Claude and human",
message: "fix: bug\n\nCo-Authored-By: Claude Opus 4 <noreply@anthropic.com>\nCo-Authored-By: Alice <alice@example.com>",
wantTools: []string{"Claude Code"},
wantModels: []string{"Claude Opus 4"},
},
{
name: "multiple AI trailers",
message: "fix: bug\n\nCo-Authored-By: Claude Opus 4 <noreply@anthropic.com>\nCo-Authored-By: aider (gpt-4o) <noreply@aider.chat>",
wantTools: []string{"Claude Code", "Aider"},
name: "multiple AI trailers",
message: "fix: bug\n\nCo-Authored-By: Claude Opus 4 <noreply@anthropic.com>\nCo-Authored-By: aider (gpt-4o) <noreply@aider.chat>",
wantTools: []string{"Claude Code", "Aider"},
wantModels: []string{"Claude Opus 4", "gpt-4o"},
},
{
name: "case variation",
message: "fix: thing\n\nco-authored-by: Claude <noreply@anthropic.com>",
wantTools: []string{"Claude Code"},
name: "case variation",
message: "fix: thing\n\nco-authored-by: Claude <noreply@anthropic.com>",
wantTools: []string{"Claude Code"},
wantModels: []string{""},
},
{
name: "CO-AUTHORED-BY uppercase",
message: "fix: thing\n\nCO-AUTHORED-BY: Claude <noreply@anthropic.com>",
wantTools: []string{"Claude Code"},
name: "CO-AUTHORED-BY uppercase",
message: "fix: thing\n\nCO-AUTHORED-BY: Claude <noreply@anthropic.com>",
wantTools: []string{"Claude Code"},
wantModels: []string{""},
},
{
name: "no trailers",
message: "just a normal commit message",
wantTools: nil,
name: "no trailers",
message: "just a normal commit message",
wantTools: nil,
wantModels: nil,
},
{
name: "human co-author only",
message: "pair programming\n\nCo-Authored-By: Bob <bob@company.com>",
wantTools: nil,
name: "human co-author only",
message: "pair programming\n\nCo-Authored-By: Bob <bob@company.com>",
wantTools: nil,
wantModels: nil,
},
{
name: "empty message",
message: "",
wantTools: nil,
name: "empty message",
message: "",
wantTools: nil,
wantModels: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
findings := d.Detect(detection.Input{CommitMessage: tt.message})
gotTools := make([]string, len(findings))
gotModels := make([]string, len(findings))
for i, f := range findings {
gotTools[i] = f.Tool
gotModels[i] = f.Model
if f.Confidence != detection.ConfidenceHigh {
t.Errorf("confidence = %d, want %d", f.Confidence, detection.ConfidenceHigh)
}
Expand All @@ -91,6 +106,7 @@ func TestDetect(t *testing.T) {

if len(gotTools) == 0 {
gotTools = nil
gotModels = nil
}

if len(gotTools) != len(tt.wantTools) {
Expand All @@ -102,6 +118,10 @@ func TestDetect(t *testing.T) {
t.Errorf("tools = %v, want %v", gotTools, tt.wantTools)
return
}
if gotModels[i] != tt.wantModels[i] {
t.Errorf("models = %q, want %q", gotModels, tt.wantModels)
return
}
}
})
}
Expand Down
2 changes: 2 additions & 0 deletions detection/detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ func (c *Confidence) Increment() {
type Finding struct {
Detector string `json:"detector"`
Tool string `json:"tool"`
Model string `json:"model,omitempty"`
Version string `json:"version,omitempty"`
Confidence Confidence `json:"confidence"`
Detail string `json:"detail"`
}
Expand Down
1 change: 1 addition & 0 deletions detection/gitnotes/gitnotes.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func (d *Detector) Detect(input detection.Input) []detection.Finding {
findings = append(findings, detection.Finding{
Detector: d.Name(),
Tool: tool,
Model: prompt.AgentID.Model,
Confidence: detection.ConfidenceHigh,
Detail: detail,
})
Expand Down
18 changes: 16 additions & 2 deletions output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func FormatText(w io.Writer, report scan.Report) error {
}
fmt.Fprintf(w, "Commit %s\n", cr.Hash[:12])
for _, f := range cr.Findings {
fmt.Fprintf(w, " [%s] %s (%s): %s\n", f.Confidence, f.Tool, f.Detector, f.Detail)
fmt.Fprintf(w, " [%s] %s (%s): %s\n", f.Confidence, formatTool(f), f.Detector, f.Detail)
}
}

Expand All @@ -58,7 +58,7 @@ func FormatTextFindings(w io.Writer, findings []detection.Finding) error {

fmt.Fprintf(w, "Found %d AI signal(s):\n", len(findings))
for _, f := range findings {
fmt.Fprintf(w, " [%s] %s (%s): %s\n", f.Confidence, f.Tool, f.Detector, f.Detail)
fmt.Fprintf(w, " [%s] %s (%s): %s\n", f.Confidence, formatTool(f), f.Detector, f.Detail)
}
return nil
}
Expand All @@ -72,6 +72,20 @@ func FormatJSONFindings(w io.Writer, findings []detection.Finding) error {
}{Findings: findings})
}

func formatTool(f detection.Finding) string {
var extras []string
if f.Model != "" {
extras = append(extras, f.Model)
}
if f.Version != "" {
extras = append(extras, "v"+f.Version)
}
if len(extras) > 0 {
return fmt.Sprintf("%s [%s]", f.Tool, strings.Join(extras, " "))
}
return f.Tool
}

func sortedKeys(m map[string]int) []string {
keys := make([]string, 0, len(m))
for k := range m {
Expand Down
Loading