From 38bee76d9648697983199c8343240dfd88a68cee Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 31 Mar 2026 06:21:51 +0100 Subject: [PATCH] =?UTF-8?q?refactor(ax):=20round=202=20AX=20sweep=20?= =?UTF-8?q?=E2=80=94=20usage=20examples,=20predictable=20names,=20dead=20c?= =?UTF-8?q?ode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- ai/ai.go | 17 +++++----- ai/metrics.go | 64 ++++++++++++++++++++++-------------- ai/metrics_bench_test.go | 17 +++++----- ai/rag.go | 24 +++++++------- cmd/metrics/cmd.go | 22 ++++++++----- cmd/security/cmd_alerts.go | 27 +++++++-------- cmd/security/cmd_jobs.go | 43 +++++++++++++----------- cmd/security/cmd_scan.go | 9 ++--- cmd/security/cmd_security.go | 57 ++++++++++++++++++++++++-------- 9 files changed, 169 insertions(+), 111 deletions(-) diff --git a/ai/ai.go b/ai/ai.go index 29cc20e..286b6e8 100644 --- a/ai/ai.go +++ b/ai/ai.go @@ -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 diff --git a/ai/metrics.go b/ai/metrics.go index 4bb256c..d3e2cc5 100644 --- a/ai/metrics.go +++ b/ai/metrics.go @@ -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 diff --git a/ai/metrics_bench_test.go b/ai/metrics_bench_test.go index ffe774d..6d90088 100644 --- a/ai/metrics_bench_test.go +++ b/ai/metrics_bench_test.go @@ -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), diff --git a/ai/rag.go b/ai/rag.go index f74a2b3..60fa7ff 100644 --- a/ai/rag.go +++ b/ai/rag.go @@ -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) } diff --git a/cmd/metrics/cmd.go b/cmd/metrics/cmd.go index e1b9b27..8cd757d 100644 --- a/cmd/metrics/cmd.go +++ b/cmd/metrics/cmd.go @@ -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) } diff --git a/cmd/security/cmd_alerts.go b/cmd/security/cmd_alerts.go index 1ad7025..b3aaff7 100644 --- a/cmd/security/cmd_alerts.go +++ b/cmd/security/cmd_alerts.go @@ -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) diff --git a/cmd/security/cmd_jobs.go b/cmd/security/cmd_jobs.go index 48a99a1..da3cc9c 100644 --- a/cmd/security/cmd_jobs.go +++ b/cmd/security/cmd_jobs.go @@ -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() } diff --git a/cmd/security/cmd_scan.go b/cmd/security/cmd_scan.go index a9ea6f0..70f65df 100644 --- a/cmd/security/cmd_scan.go +++ b/cmd/security/cmd_scan.go @@ -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 { diff --git a/cmd/security/cmd_security.go b/cmd/security/cmd_security.go index a8dcbf3..bf6b457 100644 --- a/cmd/security/cmd_security.go +++ b/cmd/security/cmd_security.go @@ -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 {