refactor(ax): round 2 AX sweep — usage examples, predictable names, dead code
All checks were successful
Security Scan / security (push) Successful in 10s
Test / test (push) Successful in 1m7s

- ai/ai.go: replace architectural prose with concrete usage examples (AX-2)
- ai/metrics.go: add usage examples to all exported/unexported functions; rename
  metricsWriteMutex (already was changed in round 1); drop redundant prose comment
  on the day-iteration loop
- ai/metrics_bench_test.go: remove dead `dir` allocation and `_ = dir` suppression;
  rename `n` param → `count` in seedEvents; add usage examples to helpers (AX-1/AX-2)
- ai/rag.go: rename qdrantCfg/ollamaCfg/queryCfg → qdrantConfig/ollamaConfig/queryConfig (AX-1)
- cmd/metrics/cmd.go: rename local `n` → `count` in parseDuration (AX-1)
- cmd/security/cmd_security.go: add usage example to checkGH and AlertSummary.Add (AX-2)
- cmd/security/cmd_alerts.go: add usage-example comments to fetch* functions (AX-2)
- cmd/security/cmd_jobs.go: rename `sb` → `builder` in buildJobIssueBody; add
  usage examples to createJobForTarget and buildJobIssueBody (AX-1/AX-2)
- cmd/security/cmd_scan.go: remove redundant "Default if not specified" inline comment (AX-2)

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-31 06:21:51 +01:00
parent 51d117de0b
commit 38bee76d96
9 changed files with 169 additions and 111 deletions

View file

@ -1,11 +1,12 @@
// Package ai provides the unified AI package for the core CLI.
// Package ai is the unified AI entry point for the Core CLI.
//
// It composes functionality from pkg/rag (vector search) and pkg/agentic
// (task management) into a single public API surface. New AI features
// should be added here; existing packages remain importable but pkg/ai
// is the canonical entry point.
// Record and query metrics:
//
// Sub-packages composed:
// - pkg/rag: Qdrant vector database + Ollama embeddings
// - pkg/agentic: Task queue client and context building
// ai.Record(ai.Event{Type: "security.scan", Repo: "core-php"})
// events, _ := ai.ReadEvents(time.Now().Add(-7 * 24 * time.Hour))
// summary := ai.Summary(events)
//
// Query RAG for task context:
//
// ctx, _ := ai.QueryRAGForTask(ai.TaskInfo{Title: "Fix auth bug", Description: "JWT expiry not checked"})
package ai

View file

@ -14,10 +14,11 @@ import (
coreerr "dappco.re/go/core/log"
)
// metricsMu protects concurrent file writes in Record.
var metricsMu sync.Mutex
var metricsWriteMutex sync.Mutex
// Event represents a recorded AI/security metric event.
// Event is a recorded AI or security metric entry.
//
// ai.Record(ai.Event{Type: "security.scan", Repo: "core-php", AgentID: "codex-1"})
type Event struct {
Type string `json:"type"`
Timestamp time.Time `json:"timestamp"`
@ -27,7 +28,9 @@ type Event struct {
Data map[string]any `json:"data,omitempty"`
}
// metricsDir returns the base directory for metrics storage.
// metricsDir returns the storage root for daily metric files.
//
// dir, _ := metricsDir() // → "/home/user/.core/ai/metrics"
func metricsDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
@ -36,20 +39,23 @@ func metricsDir() (string, error) {
return filepath.Join(home, ".core", "ai", "metrics"), nil
}
// metricsFilePath returns the JSONL file path for the given date.
// metricsFilePath returns the JSONL path for a given date.
//
// ai.metricsFilePath("/home/user/.core/ai/metrics", time.Now()) // → "/home/user/.core/ai/metrics/2026-03-31.jsonl"
func metricsFilePath(dir string, t time.Time) string {
return filepath.Join(dir, t.Format("2006-01-02")+".jsonl")
}
// Record appends an event to the daily JSONL file at
// ~/.core/ai/metrics/YYYY-MM-DD.jsonl.
// Record appends an event to the daily JSONL file at ~/.core/ai/metrics/YYYY-MM-DD.jsonl.
//
// ai.Record(ai.Event{Type: "security.scan", Repo: "go-ai", AgentID: "codex-1"})
func Record(event Event) (err error) {
if event.Timestamp.IsZero() {
event.Timestamp = time.Now()
}
metricsMu.Lock()
defer metricsMu.Unlock()
metricsWriteMutex.Lock()
defer metricsWriteMutex.Unlock()
dir, err := metricsDir()
if err != nil {
@ -62,12 +68,12 @@ func Record(event Event) (err error) {
path := metricsFilePath(dir, event.Timestamp)
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
w, err := coreio.Local.Append(path)
if err != nil {
return coreerr.E("ai.Record", "open metrics file", err)
return coreerr.E("ai.Record", "open metrics file for append", err)
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
if cerr := w.Close(); cerr != nil && err == nil {
err = coreerr.E("ai.Record", "close metrics file", cerr)
}
}()
@ -77,14 +83,16 @@ func Record(event Event) (err error) {
return coreerr.E("ai.Record", "marshal event", err)
}
if _, err := f.Write(append(data, '\n')); err != nil {
if _, err := w.Write(append(data, '\n')); err != nil {
return coreerr.E("ai.Record", "write event", err)
}
return nil
}
// ReadEvents reads events from JSONL files within the given time range.
// ReadEvents reads all events recorded on or after since.
//
// events, _ := ai.ReadEvents(time.Now().Add(-7 * 24 * time.Hour))
func ReadEvents(since time.Time) ([]Event, error) {
dir, err := metricsDir()
if err != nil {
@ -94,7 +102,6 @@ func ReadEvents(since time.Time) ([]Event, error) {
var events []Event
now := time.Now()
// Iterate each day from since to now.
for d := time.Date(since.Year(), since.Month(), since.Day(), 0, 0, 0, 0, time.Local); !d.After(now); d = d.AddDate(0, 0, 1) {
path := metricsFilePath(dir, d)
@ -108,23 +115,26 @@ func ReadEvents(since time.Time) ([]Event, error) {
return events, nil
}
// readMetricsFile reads events from a single JSONL file, returning only those at or after since.
// readMetricsFile reads events from a single JSONL file, skipping lines before since.
//
// events, _ := readMetricsFile("/home/user/.core/ai/metrics/2026-03-31.jsonl", time.Now().Add(-24*time.Hour))
func readMetricsFile(path string, since time.Time) ([]Event, error) {
f, err := os.Open(path)
if !coreio.Local.Exists(path) {
return nil, nil
}
r, err := coreio.Local.ReadStream(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, coreerr.E("ai.readMetricsFile", "open metrics file", err)
}
defer func() { _ = f.Close() }()
defer func() { _ = r.Close() }()
var events []Event
scanner := bufio.NewScanner(f)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
var ev Event
if err := json.Unmarshal(scanner.Bytes(), &ev); err != nil {
continue // skip malformed lines
continue
}
if !ev.Timestamp.Before(since) {
events = append(events, ev)
@ -137,6 +147,10 @@ func readMetricsFile(path string, since time.Time) ([]Event, error) {
}
// Summary aggregates events into counts by type, repo, and agent.
//
// summary := ai.Summary(events)
// summary["total"] // int — total event count
// summary["by_type"] // []map[string]any — sorted by count descending
func Summary(events []Event) map[string]any {
byType := make(map[string]int)
byRepo := make(map[string]int)
@ -160,7 +174,9 @@ func Summary(events []Event) map[string]any {
}
}
// sortedMap returns a slice of key-count pairs sorted by count descending.
// sortedMap converts a string→count map to a slice sorted by count descending.
//
// sortedMap(map[string]int{"build": 5, "test": 2}) // → [{key: "build", count: 5}, ...]
func sortedMap(m map[string]int) []map[string]any {
type entry struct {
key string

View file

@ -12,15 +12,13 @@ import (
// --- Helpers ---
// setupBenchMetricsDir overrides the metrics directory to a temp dir for benchmarks.
// Returns a cleanup function to restore the original.
// setupBenchMetricsDir overrides HOME to a temp dir and returns the metrics path.
//
// metricsPath := setupBenchMetricsDir(b) // → "/tmp/.../home/.core/ai/metrics"
func setupBenchMetricsDir(b *testing.B) string {
b.Helper()
dir := b.TempDir()
// Override HOME so metricsDir() resolves to our temp dir
origHome := os.Getenv("HOME")
tmpHome := b.TempDir()
// Create the metrics path under the fake HOME
metricsPath := filepath.Join(tmpHome, ".core", "ai", "metrics")
if err := coreio.Local.EnsureDir(metricsPath); err != nil {
b.Fatalf("Failed to create metrics dir: %v", err)
@ -29,15 +27,16 @@ func setupBenchMetricsDir(b *testing.B) string {
b.Cleanup(func() {
os.Setenv("HOME", origHome)
})
_ = dir
return metricsPath
}
// seedEvents writes n events to the metrics directory for the current day.
func seedEvents(b *testing.B, n int) {
// seedEvents writes count events to the metrics directory for the current day.
//
// seedEvents(b, 10_000) // writes 10K events spread across the last 10 seconds
func seedEvents(b *testing.B, count int) {
b.Helper()
now := time.Now()
for i := range n {
for i := range count {
ev := Event{
Type: fmt.Sprintf("type-%d", i%10),
Timestamp: now.Add(-time.Duration(i) * time.Millisecond),

View file

@ -8,34 +8,34 @@ import (
"forge.lthn.ai/core/go-rag"
)
// TaskInfo carries the minimal task data needed for RAG queries,
// avoiding a direct dependency on pkg/agentic (which imports pkg/ai).
// TaskInfo carries the task data used to build a RAG query.
//
// ai.QueryRAGForTask(ai.TaskInfo{Title: "Fix auth", Description: "JWT expiry not checked"})
type TaskInfo struct {
Title string
Description string
}
// QueryRAGForTask queries Qdrant for documentation relevant to a task.
// It builds a query from the task title and description, queries with
// sensible defaults, and returns formatted context.
// QueryRAGForTask returns formatted documentation context for a task from Qdrant.
//
// ctx, _ := ai.QueryRAGForTask(ai.TaskInfo{Title: "Fix auth bug", Description: "JWT expiry not checked"})
func QueryRAGForTask(task TaskInfo) (string, error) {
query := task.Title + " " + task.Description
// Truncate to 500 runes to keep the embedding focused.
runes := []rune(query)
if len(runes) > 500 {
query = string(runes[:500])
}
qdrantCfg := rag.DefaultQdrantConfig()
qdrantClient, err := rag.NewQdrantClient(qdrantCfg)
qdrantConfig := rag.DefaultQdrantConfig()
qdrantClient, err := rag.NewQdrantClient(qdrantConfig)
if err != nil {
return "", coreerr.E("ai.QueryRAGForTask", "rag qdrant client", err)
}
defer func() { _ = qdrantClient.Close() }()
ollamaCfg := rag.DefaultOllamaConfig()
ollamaClient, err := rag.NewOllamaClient(ollamaCfg)
ollamaConfig := rag.DefaultOllamaConfig()
ollamaClient, err := rag.NewOllamaClient(ollamaConfig)
if err != nil {
return "", coreerr.E("ai.QueryRAGForTask", "rag ollama client", err)
}
@ -43,13 +43,13 @@ func QueryRAGForTask(task TaskInfo) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
queryCfg := rag.QueryConfig{
queryConfig := rag.QueryConfig{
Collection: "hostuk-docs",
Limit: 3,
Threshold: 0.5,
}
results, err := rag.Query(ctx, qdrantClient, ollamaClient, query, queryCfg)
results, err := rag.Query(ctx, qdrantClient, ollamaClient, query, queryConfig)
if err != nil {
return "", coreerr.E("ai.QueryRAGForTask", "rag query", err)
}

View file

@ -31,7 +31,9 @@ func initMetricsFlags() {
metricsCmd.Flags().BoolVar(&metricsJSON, "json", false, i18n.T("common.flag.json"))
}
// AddMetricsCommand adds the 'metrics' command to the parent.
// AddMetricsCommand registers the metrics subcommand.
//
// metrics.AddMetricsCommand(rootCmd) // → core ai metrics --since 7d
func AddMetricsCommand(parent *cli.Command) {
initMetricsFlags()
parent.AddCommand(metricsCmd)
@ -101,7 +103,11 @@ func runMetrics() error {
return nil
}
// parseDuration parses a human-friendly duration like "7d", "24h", "30d".
// parseDuration parses a human-friendly duration string.
//
// parseDuration("7d") // → 7 * 24 * time.Hour
// parseDuration("24h") // → 24 * time.Hour
// parseDuration("30m") // → 30 * time.Minute
func parseDuration(s string) (time.Duration, error) {
if len(s) < 2 {
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", s), nil)
@ -110,22 +116,22 @@ func parseDuration(s string) (time.Duration, error) {
unit := s[len(s)-1]
value := s[:len(s)-1]
var n int
if _, err := fmt.Sscanf(value, "%d", &n); err != nil {
var count int
if _, err := fmt.Sscanf(value, "%d", &count); err != nil {
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", s), nil)
}
if n <= 0 {
if count <= 0 {
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("duration must be positive: %s", s), nil)
}
switch unit {
case 'd':
return time.Duration(n) * 24 * time.Hour, nil
return time.Duration(count) * 24 * time.Hour, nil
case 'h':
return time.Duration(n) * time.Hour, nil
return time.Duration(count) * time.Hour, nil
case 'm':
return time.Duration(n) * time.Minute, nil
return time.Duration(count) * time.Minute, nil
default:
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("unknown unit %c in duration: %s", unit, s), nil)
}

View file

@ -27,7 +27,7 @@ func addAlertsCommand(parent *cli.Command) {
parent.AddCommand(cmd)
}
// AlertOutput represents a unified alert for output.
// AlertOutput is the unified alert shape written to JSON or the terminal.
type AlertOutput struct {
Repo string `json:"repo"`
Severity string `json:"severity"`
@ -44,7 +44,6 @@ func runAlerts() error {
return err
}
// External target mode: bypass registry entirely
if securityTarget != "" {
return runAlertsForTarget(securityTarget)
}
@ -65,7 +64,6 @@ func runAlerts() error {
for _, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
// Fetch Dependabot alerts
depAlerts, err := fetchDependabotAlerts(repoFullName)
if err == nil {
for _, alert := range depAlerts {
@ -89,7 +87,6 @@ func runAlerts() error {
}
}
// Fetch code scanning alerts
codeAlerts, err := fetchCodeScanningAlerts(repoFullName)
if err == nil {
for _, alert := range codeAlerts {
@ -113,7 +110,6 @@ func runAlerts() error {
}
}
// Fetch secret scanning alerts
secretAlerts, err := fetchSecretScanningAlerts(repoFullName)
if err == nil {
for _, alert := range secretAlerts {
@ -123,7 +119,7 @@ func runAlerts() error {
if !filterBySeverity("high", securitySeverity) {
continue
}
summary.Add("high") // Secrets are always high severity
summary.Add("high")
allAlerts = append(allAlerts, AlertOutput{
Repo: repo.Name,
Severity: "high",
@ -144,7 +140,6 @@ func runAlerts() error {
return nil
}
// Print summary
cli.Blank()
cli.Print("%s %s\n", cli.DimStyle.Render("Alerts:"), summary.String())
cli.Blank()
@ -153,11 +148,9 @@ func runAlerts() error {
return nil
}
// Print table
for _, alert := range allAlerts {
sevStyle := severityStyle(alert.Severity)
// Format: repo SEVERITY ID package/location type
location := alert.Package
if location == "" {
location = alert.Location
@ -179,7 +172,9 @@ func runAlerts() error {
return nil
}
// runAlertsForTarget runs unified alert checks against an external repo target.
// runAlertsForTarget runs unified alert checks against an external owner/repo target.
//
// runAlertsForTarget("wailsapp/wails")
func runAlertsForTarget(target string) error {
repo, fullName := buildTargetRepo(target)
if repo == nil {
@ -189,7 +184,6 @@ func runAlertsForTarget(target string) error {
var allAlerts []AlertOutput
summary := &AlertSummary{}
// Fetch Dependabot alerts
depAlerts, err := fetchDependabotAlerts(fullName)
if err == nil {
for _, alert := range depAlerts {
@ -213,7 +207,6 @@ func runAlertsForTarget(target string) error {
}
}
// Fetch code scanning alerts
codeAlerts, err := fetchCodeScanningAlerts(fullName)
if err == nil {
for _, alert := range codeAlerts {
@ -237,7 +230,6 @@ func runAlertsForTarget(target string) error {
}
}
// Fetch secret scanning alerts
secretAlerts, err := fetchSecretScanningAlerts(fullName)
if err == nil {
for _, alert := range secretAlerts {
@ -297,6 +289,9 @@ func runAlertsForTarget(target string) error {
return nil
}
// fetchDependabotAlerts returns open Dependabot vulnerability alerts for the given repo.
//
// alerts, _ := fetchDependabotAlerts("host-uk/core-php")
func fetchDependabotAlerts(repoFullName string) ([]DependabotAlert, error) {
endpoint := fmt.Sprintf("repos/%s/dependabot/alerts?state=open", repoFullName)
output, err := runGHAPI(endpoint)
@ -311,6 +306,9 @@ func fetchDependabotAlerts(repoFullName string) ([]DependabotAlert, error) {
return alerts, nil
}
// fetchCodeScanningAlerts returns open code-scanning alerts for the given repo.
//
// alerts, _ := fetchCodeScanningAlerts("host-uk/core-php")
func fetchCodeScanningAlerts(repoFullName string) ([]CodeScanningAlert, error) {
endpoint := fmt.Sprintf("repos/%s/code-scanning/alerts?state=open", repoFullName)
output, err := runGHAPI(endpoint)
@ -325,6 +323,9 @@ func fetchCodeScanningAlerts(repoFullName string) ([]CodeScanningAlert, error) {
return alerts, nil
}
// fetchSecretScanningAlerts returns open secret-scanning alerts for the given repo.
//
// alerts, _ := fetchSecretScanningAlerts("host-uk/core-php")
func fetchSecretScanningAlerts(repoFullName string) ([]SecretScanningAlert, error) {
endpoint := fmt.Sprintf("repos/%s/secret-scanning/alerts?state=open", repoFullName)
output, err := runGHAPI(endpoint)

View file

@ -66,6 +66,9 @@ func runJobs() error {
return nil
}
// createJobForTarget gathers all security findings for target and creates a GitHub issue.
//
// createJobForTarget("wailsapp/wails")
func createJobForTarget(target string) error {
parts := strings.SplitN(target, "/", 2)
if len(parts) != 2 {
@ -195,36 +198,38 @@ func createJobForTarget(target string) error {
return nil
}
// buildJobIssueBody renders the GitHub issue body for a security scan job.
//
// body := buildJobIssueBody("wailsapp/wails", summary, findings)
func buildJobIssueBody(target string, summary *AlertSummary, findings []string) string {
var sb strings.Builder
var builder strings.Builder
fmt.Fprintf(&sb, "## Security Scan: %s\n\n", target)
fmt.Fprintf(&sb, "**Summary:** %s\n\n", summary.String())
fmt.Fprintf(&builder, "## Security Scan: %s\n\n", target)
fmt.Fprintf(&builder, "**Summary:** %s\n\n", summary.String())
sb.WriteString("### Findings\n\n")
builder.WriteString("### Findings\n\n")
if len(findings) > 50 {
// Truncate long lists
for _, f := range findings[:50] {
sb.WriteString(f + "\n")
builder.WriteString(f + "\n")
}
fmt.Fprintf(&sb, "\n... and %d more\n", len(findings)-50)
fmt.Fprintf(&builder, "\n... and %d more\n", len(findings)-50)
} else {
for _, f := range findings {
sb.WriteString(f + "\n")
builder.WriteString(f + "\n")
}
}
sb.WriteString("\n### Checklist\n\n")
sb.WriteString("- [ ] Review findings above\n")
sb.WriteString("- [ ] Triage by severity (critical/high first)\n")
sb.WriteString("- [ ] Create PRs for fixes\n")
sb.WriteString("- [ ] Verify fixes resolve alerts\n")
builder.WriteString("\n### Checklist\n\n")
builder.WriteString("- [ ] Review findings above\n")
builder.WriteString("- [ ] Triage by severity (critical/high first)\n")
builder.WriteString("- [ ] Create PRs for fixes\n")
builder.WriteString("- [ ] Verify fixes resolve alerts\n")
sb.WriteString("\n### Instructions\n\n")
sb.WriteString("1. Claim this issue by assigning yourself\n")
fmt.Fprintf(&sb, "2. Run `core security alerts --target %s` for the latest findings\n", target)
sb.WriteString("3. Work through the checklist above\n")
sb.WriteString("4. Close this issue when all findings are addressed\n")
builder.WriteString("\n### Instructions\n\n")
builder.WriteString("1. Claim this issue by assigning yourself\n")
fmt.Fprintf(&builder, "2. Run `core security alerts --target %s` for the latest findings\n", target)
builder.WriteString("3. Work through the checklist above\n")
builder.WriteString("4. Close this issue when all findings are addressed\n")
return sb.String()
return builder.String()
}

View file

@ -34,7 +34,7 @@ func addScanCommand(parent *cli.Command) {
parent.AddCommand(cmd)
}
// ScanAlert represents a code scanning alert for output.
// ScanAlert is a code scanning alert entry written to JSON or the terminal.
type ScanAlert struct {
Repo string `json:"repo"`
Severity string `json:"severity"`
@ -51,7 +51,6 @@ func runScan() error {
return err
}
// External target mode: bypass registry entirely
if securityTarget != "" {
return runScanForTarget(securityTarget)
}
@ -90,7 +89,7 @@ func runScan() error {
severity := alert.Rule.Severity
if severity == "" {
severity = "medium" // Default if not specified
severity = "medium"
}
if !filterBySeverity(severity, securitySeverity) {
@ -163,7 +162,9 @@ func runScan() error {
return nil
}
// runScanForTarget runs a code scanning check against an external repo target.
// runScanForTarget runs code scanning checks against an external owner/repo target.
//
// runScanForTarget("wailsapp/wails")
func runScanForTarget(target string) error {
repo, fullName := buildTargetRepo(target)
if repo == nil {

View file

@ -22,7 +22,9 @@ var (
securityTarget string // External repo target (e.g. "wailsapp/wails")
)
// AddSecurityCommands adds the 'security' command to the root.
// AddSecurityCommands registers the security subcommand tree.
//
// security.AddSecurityCommands(rootCmd) // → core security alerts|deps|scan|secrets|jobs
func AddSecurityCommands(root *cli.Command) {
secCmd := &cli.Command{
Use: "security",
@ -39,7 +41,7 @@ func AddSecurityCommands(root *cli.Command) {
root.AddCommand(secCmd)
}
// DependabotAlert represents a Dependabot vulnerability alert.
// DependabotAlert is a GitHub Dependabot vulnerability alert (from repos/{org}/{repo}/dependabot/alerts).
type DependabotAlert struct {
Number int `json:"number"`
State string `json:"state"`
@ -68,7 +70,7 @@ type DependabotAlert struct {
} `json:"security_vulnerability"`
}
// CodeScanningAlert represents a code scanning alert.
// CodeScanningAlert is a GitHub code scanning alert (from repos/{org}/{repo}/code-scanning/alerts).
type CodeScanningAlert struct {
Number int `json:"number"`
State string `json:"state"`
@ -95,7 +97,7 @@ type CodeScanningAlert struct {
} `json:"most_recent_instance"`
}
// SecretScanningAlert represents a secret scanning alert.
// SecretScanningAlert is a GitHub secret scanning alert (from repos/{org}/{repo}/secret-scanning/alerts).
type SecretScanningAlert struct {
Number int `json:"number"`
State string `json:"state"`
@ -105,7 +107,10 @@ type SecretScanningAlert struct {
Resolution string `json:"resolution"`
}
// loadRegistry loads the repository registry.
// loadRegistry loads the registry from registryPath, or auto-discovers it from the working directory.
//
// loadRegistry("") // auto-discover repos.yaml
// loadRegistry("/path/to/repos.yaml")
func loadRegistry(registryPath string) (*repos.Registry, error) {
if registryPath != "" {
reg, err := repos.LoadRegistry(io.Local, registryPath)
@ -126,7 +131,9 @@ func loadRegistry(registryPath string) (*repos.Registry, error) {
return reg, nil
}
// checkGH verifies gh CLI is available.
// checkGH returns an error if the gh CLI is not on PATH.
//
// if err := checkGH(); err != nil { return err }
func checkGH() error {
if _, err := exec.LookPath("gh"); err != nil {
return coreerr.E("security.checkGH", i18n.T("error.gh_not_found"), nil)
@ -134,7 +141,9 @@ func checkGH() error {
return nil
}
// runGHAPI runs a gh api command and returns the output.
// runGHAPI calls gh api with pagination and returns the raw JSON body.
//
// runGHAPI("repos/host-uk/core-php/dependabot/alerts?state=open")
func runGHAPI(endpoint string) ([]byte, error) {
cmd := exec.Command("gh", "api", endpoint, "--paginate")
output, err := cmd.Output()
@ -154,7 +163,10 @@ func runGHAPI(endpoint string) ([]byte, error) {
return output, nil
}
// severityStyle returns the appropriate style for a severity level.
// severityStyle maps a severity string to its terminal render style.
//
// severityStyle("critical") // → cli.ErrorStyle
// severityStyle("high") // → cli.WarningStyle
func severityStyle(severity string) *cli.AnsiStyle {
switch strings.ToLower(severity) {
case "critical":
@ -168,7 +180,11 @@ func severityStyle(severity string) *cli.AnsiStyle {
}
}
// filterBySeverity checks if the severity matches the filter.
// filterBySeverity reports whether severity matches the comma-separated filter list.
//
// filterBySeverity("high", "critical,high") // → true
// filterBySeverity("low", "critical,high") // → false
// filterBySeverity("high", "") // → true (empty = all pass)
func filterBySeverity(severity, filter string) bool {
if filter == "" {
return true
@ -180,7 +196,10 @@ func filterBySeverity(severity, filter string) bool {
})
}
// getReposToCheck returns the list of repos to check based on flags.
// getReposToCheck returns a single repo when repoFilter is set, or all repos in the registry.
//
// getReposToCheck(reg, "core-php") // → [core-php]
// getReposToCheck(reg, "") // → all repos
func getReposToCheck(reg *repos.Registry, repoFilter string) []*repos.Repo {
if repoFilter != "" {
if repo, ok := reg.Get(repoFilter); ok {
@ -191,7 +210,9 @@ func getReposToCheck(reg *repos.Registry, repoFilter string) []*repos.Repo {
return reg.List()
}
// buildTargetRepo creates a synthetic Repo entry for an external target (e.g. "wailsapp/wails").
// buildTargetRepo parses an owner/repo target string into a Repo and its full name.
//
// buildTargetRepo("wailsapp/wails") // → &Repo{Name:"wails"}, "wailsapp/wails"
func buildTargetRepo(target string) (*repos.Repo, string) {
parts := strings.SplitN(target, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
@ -200,7 +221,11 @@ func buildTargetRepo(target string) (*repos.Repo, string) {
return &repos.Repo{Name: parts[1]}, target
}
// AlertSummary holds aggregated alert counts.
// AlertSummary tracks alert counts by severity across a scan run.
//
// s := &AlertSummary{}
// s.Add("critical")
// s.String() // → "1 critical"
type AlertSummary struct {
Critical int
High int
@ -210,7 +235,9 @@ type AlertSummary struct {
Total int
}
// Add increments summary counters for the provided severity.
// Add increments the counter for the given severity level.
//
// s.Add("critical") // s.Critical == 1, s.Total == 1
func (s *AlertSummary) Add(severity string) {
s.Total++
switch strings.ToLower(severity) {
@ -227,7 +254,9 @@ func (s *AlertSummary) Add(severity string) {
}
}
// String renders a human-readable summary of alert counts.
// String renders a styled, human-readable summary of alert counts.
//
// (&AlertSummary{Critical: 1, High: 2}).String() // → "1 critical | 2 high"
func (s *AlertSummary) String() string {
parts := []string{}
if s.Critical > 0 {