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
12 changes: 8 additions & 4 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ const (
ExitError = 2
)

func allDetectors() []detection.Detector {
func allDetectors(includeModelCatalog bool) []detection.Detector {
return []detection.Detector{
&committer.Detector{},
&gitnotes.Detector{},
&trailer.Detector{},
&toolmention.Detector{},
&toolmention.Detector{IncludeModelCatalog: includeModelCatalog},
}
}

Expand Down Expand Up @@ -81,6 +81,7 @@ func scanCommand(stdout, stderr io.Writer, exitCode *int) *cobra.Command {
var rangeFlag string
var formatFlag string
var minConfFlag string
var includeModelCatalogFlag bool

cmd := &cobra.Command{
Use: "scan [repo-path]",
Expand Down Expand Up @@ -130,7 +131,7 @@ Examples:
return err
}

detectors := allDetectors()
detectors := allDetectors(includeModelCatalogFlag)
report, err := scan.ScanCommitRange(repoPath, rangeFlag, detectors)
if err != nil {
fmt.Fprintf(stderr, "error: %v\n", err)
Expand Down Expand Up @@ -170,13 +171,15 @@ Examples:
cmd.Flags().StringVar(&rangeFlag, "range", "", "commit range in BASE..HEAD format")
cmd.Flags().StringVar(&formatFlag, "format", "text", "output format: json or text")
cmd.Flags().StringVar(&minConfFlag, "min-confidence", "low", "minimum confidence level: low, medium, high (or 1, 2, 3)")
cmd.Flags().BoolVar(&includeModelCatalogFlag, "include-model-catalog", false, "include generated model catalogue in low-confidence text matching")

return cmd
}

func textCommand(stdout, stderr io.Writer, exitCode *int) *cobra.Command {
var formatFlag string
var inputFlag string
var includeModelCatalogFlag bool

cmd := &cobra.Command{
Use: "text",
Expand Down Expand Up @@ -220,7 +223,7 @@ Examples:
return err
}

detectors := allDetectors()
detectors := allDetectors(includeModelCatalogFlag)
findings := scan.ScanText(string(textBytes), detectors)

switch formatFlag {
Expand Down Expand Up @@ -252,6 +255,7 @@ Examples:

cmd.Flags().StringVar(&formatFlag, "format", "text", "output format: json or text")
cmd.Flags().StringVar(&inputFlag, "input", "-", "input file path, or - for stdin")
cmd.Flags().BoolVar(&includeModelCatalogFlag, "include-model-catalog", false, "include generated model catalogue in low-confidence text matching")

return cmd
}
Expand Down
57 changes: 57 additions & 0 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,63 @@ func TestRunScanInvalidRepo(t *testing.T) {
}
}

func TestRunTextAmbiguousModelProse(t *testing.T) {
inputPath := filepath.Join(t.TempDir(), "comment.txt")
text := strings.Join([]string{
"Press Command A to select all text.",
"The R1 resistor value changed during testing.",
"The sonar data was noisy in shallow water.",
"The UI spotlight highlighted the primary button.",
"A body builder updated the training plan.",
"The weaver repaired the fabric.",
"The bodybuilder updated the training plan.",
"The o1 visa, o3 zone, and Saba reference were unrelated.",
"The report included uncensored logs.",
}, "\n")
if err := os.WriteFile(inputPath, []byte(text), 0644); err != nil {
t.Fatal(err)
}

var stdout, stderr bytes.Buffer
code := Run([]string{"text", "--input=" + inputPath}, &stdout, &stderr)
if code != ExitNoAI {
t.Errorf("exit code = %d, want %d (stderr: %s, stdout: %s)", code, ExitNoAI, stderr.String(), stdout.String())
}
if !strings.Contains(stdout.String(), "No AI involvement detected") {
t.Errorf("expected no-detection output, got: %s", stdout.String())
}

stdout.Reset()
stderr.Reset()
code = Run([]string{"text", "--input=" + inputPath, "--include-model-catalog"}, &stdout, &stderr)
if code != ExitNoAI {
t.Errorf("exit code with model catalog = %d, want %d (stderr: %s, stdout: %s)", code, ExitNoAI, stderr.String(), stdout.String())
}
}

func TestRunTextGeneratedModelCatalogOptIn(t *testing.T) {
inputPath := filepath.Join(t.TempDir(), "comment.txt")
if err := os.WriteFile(inputPath, []byte("I used Aion-RP 1.0 for a comparison."), 0644); err != nil {
t.Fatal(err)
}

var stdout, stderr bytes.Buffer
code := Run([]string{"text", "--input=" + inputPath}, &stdout, &stderr)
if code != ExitNoAI {
t.Errorf("exit code = %d, want %d (stderr: %s, stdout: %s)", code, ExitNoAI, stderr.String(), stdout.String())
}

stdout.Reset()
stderr.Reset()
code = Run([]string{"text", "--input=" + inputPath, "--include-model-catalog"}, &stdout, &stderr)
if code != ExitAI {
t.Errorf("exit code with model catalog = %d, want %d (stderr: %s, stdout: %s)", code, ExitAI, stderr.String(), stdout.String())
}
if !strings.Contains(stdout.String(), "Aion-RP 1.0") {
t.Errorf("expected generated model in output, got: %s", stdout.String())
}
}

func TestFilterReport(t *testing.T) {
report := scan.Report{
Commits: []scan.CommitResult{
Expand Down
75 changes: 74 additions & 1 deletion detection/constants.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package detection

import "regexp"
import (
"regexp"
"strings"
)

// KnownAgentCommitters maps GitHub noreply emails to AI tool names.
var KnownAgentCommitters = map[string]string{
Expand Down Expand Up @@ -130,6 +133,76 @@ var SupportedToolsInMentions = []string{
"CodeWhisperer",
}

var genericModelMentionKeys = map[string]struct{}{
"auto": {},
"autorouter": {},
"bodybuilder": {},
"commanda": {},
"commandr": {},
"free": {},
"freemodelsrouter": {},
"o1": {},
"o3": {},
"r1": {},
"router": {},
"saba": {},
"sonar": {},
"spotlight": {},
"uncensored": {},
"weaver": {},
}

func SupportedToolMentions(includeGenerated bool) []string {
mentions := append([]string(nil), SupportedToolsInMentions...)
if includeGenerated {
mentions = appendCanonicalMentions(mentions, GeneratedModelMentions...)
}
return mentions
}

func appendCanonicalMentions(existing []string, candidates ...string) []string {
seen := make(map[string]struct{}, len(existing)+len(candidates))
for _, item := range existing {
key := strings.ToLower(strings.TrimSpace(item))
if key != "" {
seen[key] = struct{}{}
}
}

for _, candidate := range candidates {
canonical := strings.TrimSpace(candidate)
key := strings.ToLower(canonical)
if key == "" {
continue
}
if _, isGeneric := genericModelMentionKeys[modelMentionKey(canonical)]; isGeneric {
continue
}
if _, exists := seen[key]; exists {
continue
}
existing = append(existing, canonical)
seen[key] = struct{}{}
}

return existing
}

func modelMentionKey(model string) string {
return strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' {
return r
}
if r >= 'A' && r <= 'Z' {
return r + ('a' - 'A')
}
if r >= '0' && r <= '9' {
return r
}
return -1
}, model)
}

// KnownCoAuthorEmails Known emails present with Co-Authored-By trailers
var KnownCoAuthorEmails = map[string]string{
"noreply@anthropic.com": "Claude Code",
Expand Down
Loading