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
177 changes: 177 additions & 0 deletions cmd/brief/ai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"sort"

"github.com/chaoss/ai-detection-action/detection"
"github.com/chaoss/ai-detection-action/detection/coauthor"
"github.com/chaoss/ai-detection-action/detection/committer"
"github.com/chaoss/ai-detection-action/detection/message"
"github.com/chaoss/ai-detection-action/detection/toolmention"
"github.com/chaoss/ai-detection-action/scan"
)

func cmdAI(args []string) {
fs := flag.NewFlagSet("brief ai", flag.ExitOnError)
jsonFlag := fs.Bool("json", false, "Force JSON output")
humanFlag := fs.Bool("human", false, "Force human-readable output")
markdownFlag := fs.Bool("markdown", false, "Force markdown output")
commitRange := fs.String("range", "", "Commit range to scan (e.g. base..head)")
minConfidence := fs.String("min-confidence", "low", "Minimum confidence level: low, medium, or high")
_ = fs.Parse(args)

path := "."
if fs.NArg() > 0 {
path = fs.Arg(0)
}

minConf, err := parseConfidence(*minConfidence)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}

detectors := []detection.Detector{
&committer.Detector{},
&coauthor.Detector{},
&message.Detector{},
&toolmention.Detector{},
}

report, err := scan.ScanCommitRange(path, *commitRange, detectors)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}

report = filterByConfidence(report, minConf)

switch {
case *markdownFlag:
aiMarkdown(os.Stdout, report)
case *jsonFlag || (!*humanFlag && !isTTY()):
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(report); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error writing JSON: %v\n", err)
os.Exit(1)
}
default:
aiHuman(os.Stdout, report)
}

if report.Summary.AICommits > 0 {
os.Exit(1)
}
}

func parseConfidence(s string) (detection.Confidence, error) {
switch s {
case "low":
return detection.ConfidenceLow, nil
case "medium":
return detection.ConfidenceMedium, nil
case "high":
return detection.ConfidenceHigh, nil
default:
return 0, fmt.Errorf("invalid confidence level %q, expected low, medium, or high", s)
}
}

func filterByConfidence(r scan.Report, minConf detection.Confidence) scan.Report {
var filtered []scan.CommitResult
for _, cr := range r.Commits {
var findings []detection.Finding
for _, f := range cr.Findings {
if f.Confidence >= minConf {
findings = append(findings, f)
}
}
if len(findings) > 0 {
filtered = append(filtered, scan.CommitResult{
Hash: cr.Hash,
Findings: findings,
})
}
}

// Rebuild summary from filtered results.
summary := scan.Summary{
TotalCommits: len(r.Commits),
AICommits: len(filtered),
ToolCounts: map[string]int{},
ByConfidence: map[string]int{},
}
for _, cr := range filtered {
for _, f := range cr.Findings {
summary.ToolCounts[f.Tool]++
summary.ByConfidence[f.Confidence.String()]++
}
}

return scan.Report{
Commits: filtered,
Summary: summary,
}
}

func sortedToolNames(counts map[string]int) []string {
names := make([]string, 0, len(counts))
for name := range counts {
names = append(names, name)
}
sort.Strings(names)
return names
}

const shortHashLen = 12

func aiHuman(w io.Writer, r scan.Report) {
_, _ = fmt.Fprintf(w, "%d commits scanned, %d with AI signals\n", r.Summary.TotalCommits, r.Summary.AICommits)

if r.Summary.AICommits == 0 {
return
}

_, _ = fmt.Fprintln(w)
for _, tool := range sortedToolNames(r.Summary.ToolCounts) {
_, _ = fmt.Fprintf(w, " %-25s %d commits\n", tool, r.Summary.ToolCounts[tool])
}

_, _ = fmt.Fprintln(w)
for _, cr := range r.Commits {
_, _ = fmt.Fprintf(w, "%s\n", cr.Hash[:min(len(cr.Hash), shortHashLen)])
for _, f := range cr.Findings {
_, _ = fmt.Fprintf(w, " [%s] %s: %s\n", f.Confidence, f.Tool, f.Detail)
}
}
}

func aiMarkdown(w io.Writer, r scan.Report) {
_, _ = fmt.Fprintf(w, "## AI Detection\n\n")
_, _ = fmt.Fprintf(w, "%d commits scanned, %d with AI signals\n\n", r.Summary.TotalCommits, r.Summary.AICommits)

if r.Summary.AICommits == 0 {
return
}

_, _ = fmt.Fprintln(w, "| Tool | Commits |")
_, _ = fmt.Fprintln(w, "|------|---------|")
for _, tool := range sortedToolNames(r.Summary.ToolCounts) {
_, _ = fmt.Fprintf(w, "| %s | %d |\n", tool, r.Summary.ToolCounts[tool])
}

_, _ = fmt.Fprintf(w, "\n### Findings\n\n")
for _, cr := range r.Commits {
_, _ = fmt.Fprintf(w, "**%s**\n\n", cr.Hash[:min(len(cr.Hash), shortHashLen)])
for _, f := range cr.Findings {
_, _ = fmt.Fprintf(w, "- [%s] %s: %s\n", f.Confidence, f.Tool, f.Detail)
}
_, _ = fmt.Fprintln(w)
}
}
Loading