[agent/codex] A specs/ folder has been injected into this workspace with R... #12

Open
Virgil wants to merge 6 commits from agent/upgrade-to-core-v0-8-0-alpha-1 into dev
31 changed files with 1500 additions and 605 deletions

View file

@ -3,21 +3,22 @@ package ai
import (
"bufio"
"cmp"
"encoding/json"
"os"
"path/filepath"
"slices"
"sync"
"time"
"dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
// metricsMu protects concurrent file writes in Record.
var metricsMu sync.Mutex
// Event represents a recorded AI/security metric event.
//
// Example:
//
// _ = Record(Event{Type: "security.scan", Repo: "host-uk/core"})
type Event struct {
Type string `json:"type"`
Timestamp time.Time `json:"timestamp"`
@ -29,20 +30,30 @@ type Event struct {
// metricsDir returns the base directory for metrics storage.
func metricsDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", coreerr.E("ai.metricsDir", "get home directory", err)
home := core.Env("CORE_HOME")
if home == "" {
home = core.Env("HOME")
}
return filepath.Join(home, ".core", "ai", "metrics"), nil
if home == "" {
home = core.Env("DIR_HOME")
}
if home == "" {
return "", core.E("ai.metricsDir", "home directory unavailable", nil)
}
return core.Path(home, ".core", "ai", "metrics"), nil
}
// metricsFilePath returns the JSONL file path for the given date.
func metricsFilePath(dir string, t time.Time) string {
return filepath.Join(dir, t.Format("2006-01-02")+".jsonl")
return core.Path(dir, core.Concat(t.Format("2006-01-02"), ".jsonl"))
}
// Record appends an event to the daily JSONL file at
// ~/.core/ai/metrics/YYYY-MM-DD.jsonl.
//
// Example:
//
// err := Record(Event{Type: "metrics.read", AgentID: "agent-1"})
func Record(event Event) (err error) {
if event.Timestamp.IsZero() {
event.Timestamp = time.Now()
@ -56,35 +67,39 @@ func Record(event Event) (err error) {
return err
}
if err := coreio.Local.EnsureDir(dir); err != nil {
return coreerr.E("ai.Record", "create metrics directory", err)
if err := coreio.EnsureDir(coreio.Local, dir); err != nil {
return core.E("ai.Record", "create metrics directory", err)
}
path := metricsFilePath(dir, event.Timestamp)
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
f, err := coreio.Local.Append(path)
if err != nil {
return coreerr.E("ai.Record", "open metrics file", err)
return core.E("ai.Record", "open metrics file", err)
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = coreerr.E("ai.Record", "close metrics file", cerr)
err = core.E("ai.Record", "close metrics file", cerr)
}
}()
data, err := json.Marshal(event)
if err != nil {
return coreerr.E("ai.Record", "marshal event", err)
data := core.JSONMarshal(event)
if !data.OK {
return core.E("ai.Record", "marshal event", data.Value.(error))
}
if _, err := f.Write(append(data, '\n')); err != nil {
return coreerr.E("ai.Record", "write event", err)
if _, err := f.Write(append(data.Value.([]byte), '\n')); err != nil {
return core.E("ai.Record", "write event", err)
}
return nil
}
// ReadEvents reads events from JSONL files within the given time range.
//
// Example:
//
// events, err := ReadEvents(time.Now().Add(-24 * time.Hour))
func ReadEvents(since time.Time) ([]Event, error) {
dir, err := metricsDir()
if err != nil {
@ -110,20 +125,21 @@ func ReadEvents(since time.Time) ([]Event, error) {
// readMetricsFile reads events from a single JSONL file, returning only those at or after since.
func readMetricsFile(path string, since time.Time) ([]Event, error) {
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, coreerr.E("ai.readMetricsFile", "open metrics file", err)
if !coreio.Local.Exists(path) {
return nil, nil
}
defer func() { _ = f.Close() }()
f, err := coreio.Local.Open(path)
if err != nil {
return nil, core.E("ai.readMetricsFile", "open metrics file", err)
}
defer core.CloseStream(f)
var events []Event
scanner := bufio.NewScanner(f)
for scanner.Scan() {
var ev Event
if err := json.Unmarshal(scanner.Bytes(), &ev); err != nil {
if result := core.JSONUnmarshal(scanner.Bytes(), &ev); !result.OK {
continue // skip malformed lines
}
if !ev.Timestamp.Before(since) {
@ -131,12 +147,16 @@ func readMetricsFile(path string, since time.Time) ([]Event, error) {
}
}
if err := scanner.Err(); err != nil {
return nil, coreerr.E("ai.readMetricsFile", "read metrics file", err)
return nil, core.E("ai.readMetricsFile", "read metrics file", err)
}
return events, nil
}
// Summary aggregates events into counts by type, repo, and agent.
//
// Example:
//
// summary := Summary(events)
func Summary(events []Event) map[string]any {
byType := make(map[string]int)
byRepo := make(map[string]int)
@ -172,7 +192,10 @@ func sortedMap(m map[string]int) []map[string]any {
}
slices.SortFunc(entries, func(a, b entry) int {
return cmp.Compare(b.count, a.count)
if diff := cmp.Compare(b.count, a.count); diff != 0 {
return diff
}
return cmp.Compare(a.key, b.key)
})
result := make([]map[string]any, len(entries))

View file

@ -1,48 +1,22 @@
package ai
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
coreio "dappco.re/go/core/io"
"dappco.re/go/core"
)
// --- Helpers ---
// setupBenchMetricsDir overrides the metrics directory to a temp dir for benchmarks.
// Returns a cleanup function to restore the original.
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)
}
os.Setenv("HOME", tmpHome)
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) {
b.Helper()
now := time.Now()
for i := range n {
ev := Event{
Type: fmt.Sprintf("type-%d", i%10),
Type: core.Sprintf("type-%d", i%10),
Timestamp: now.Add(-time.Duration(i) * time.Millisecond),
AgentID: fmt.Sprintf("agent-%d", i%5),
Repo: fmt.Sprintf("repo-%d", i%3),
AgentID: core.Sprintf("agent-%d", i%5),
Repo: core.Sprintf("repo-%d", i%3),
Data: map[string]any{"i": i, "tool": "bench_tool"},
}
if err := Record(ev); err != nil {
@ -55,7 +29,7 @@ func seedEvents(b *testing.B, n int) {
// BenchmarkMetricsRecord benchmarks writing individual metric events.
func BenchmarkMetricsRecord(b *testing.B) {
setupBenchMetricsDir(b)
withTempHome(b)
now := time.Now()
b.ResetTimer()
@ -76,7 +50,7 @@ func BenchmarkMetricsRecord(b *testing.B) {
// BenchmarkMetricsRecord_Parallel benchmarks concurrent metric recording.
func BenchmarkMetricsRecord_Parallel(b *testing.B) {
setupBenchMetricsDir(b)
withTempHome(b)
now := time.Now()
b.ResetTimer()
@ -101,7 +75,7 @@ func BenchmarkMetricsRecord_Parallel(b *testing.B) {
// BenchmarkMetricsQuery_10K benchmarks querying 10K events.
func BenchmarkMetricsQuery_10K(b *testing.B) {
setupBenchMetricsDir(b)
withTempHome(b)
seedEvents(b, 10_000)
since := time.Now().Add(-24 * time.Hour)
@ -120,7 +94,7 @@ func BenchmarkMetricsQuery_10K(b *testing.B) {
// BenchmarkMetricsQuery_50K benchmarks querying 50K events.
func BenchmarkMetricsQuery_50K(b *testing.B) {
setupBenchMetricsDir(b)
withTempHome(b)
seedEvents(b, 50_000)
since := time.Now().Add(-24 * time.Hour)
@ -139,7 +113,7 @@ func BenchmarkMetricsQuery_50K(b *testing.B) {
// BenchmarkMetricsSummary_10K benchmarks summarising 10K events.
func BenchmarkMetricsSummary_10K(b *testing.B) {
setupBenchMetricsDir(b)
withTempHome(b)
seedEvents(b, 10_000)
since := time.Now().Add(-24 * time.Hour)
@ -159,14 +133,14 @@ func BenchmarkMetricsSummary_10K(b *testing.B) {
// BenchmarkMetricsRecordAndQuery benchmarks the full write-then-read cycle at 10K scale.
func BenchmarkMetricsRecordAndQuery(b *testing.B) {
setupBenchMetricsDir(b)
withTempHome(b)
now := time.Now()
// Write 10K events
for i := range 10_000 {
ev := Event{
Type: fmt.Sprintf("type-%d", i%10),
Type: core.Sprintf("type-%d", i%10),
Timestamp: now,
AgentID: "bench",
Repo: "bench-repo",
@ -190,19 +164,9 @@ func BenchmarkMetricsRecordAndQuery(b *testing.B) {
// --- Unit tests for metrics at scale ---
// TestMetricsRecordAndRead_10K_Good writes 10K events and reads them back.
func TestMetricsRecordAndRead_10K_Good(t *testing.T) {
// Override HOME to temp dir
origHome := os.Getenv("HOME")
tmpHome := t.TempDir()
metricsPath := filepath.Join(tmpHome, ".core", "ai", "metrics")
if err := coreio.Local.EnsureDir(metricsPath); err != nil {
t.Fatalf("Failed to create metrics dir: %v", err)
}
os.Setenv("HOME", tmpHome)
t.Cleanup(func() {
os.Setenv("HOME", origHome)
})
// TestMetricsBench_MetricsRecordAndRead_Good writes 10K events and reads them back.
func TestMetricsBench_MetricsRecordAndRead_Good(t *testing.T) {
withTempHome(t)
now := time.Now()
const n = 10_000
@ -210,10 +174,10 @@ func TestMetricsRecordAndRead_10K_Good(t *testing.T) {
// Write events
for i := range n {
ev := Event{
Type: fmt.Sprintf("type-%d", i%10),
Type: core.Sprintf("type-%d", i%10),
Timestamp: now.Add(-time.Duration(i) * time.Millisecond),
AgentID: fmt.Sprintf("agent-%d", i%5),
Repo: fmt.Sprintf("repo-%d", i%3),
AgentID: core.Sprintf("agent-%d", i%5),
Repo: core.Sprintf("repo-%d", i%3),
Data: map[string]any{"index": i},
}
if err := Record(ev); err != nil {

View file

@ -1,213 +1,198 @@
package ai
import (
"os"
"path/filepath"
"testing"
"time"
"dappco.re/go/core"
coreio "dappco.re/go/core/io"
)
// withTempHome overrides HOME to a temp dir for the duration of the test.
func withTempHome(t *testing.T) {
t.Helper()
origHome := os.Getenv("HOME")
tmpHome := t.TempDir()
metricsPath := filepath.Join(tmpHome, ".core", "ai", "metrics")
if err := coreio.Local.EnsureDir(metricsPath); err != nil {
t.Fatalf("create metrics dir: %v", err)
func withTempHome(tb testing.TB) {
tb.Helper()
tmpHome := tb.TempDir()
metricsPath := core.Path(tmpHome, ".core", "ai", "metrics")
if err := coreio.EnsureDir(coreio.Local, metricsPath); err != nil {
tb.Fatalf("create metrics dir: %v", err)
}
os.Setenv("HOME", tmpHome)
t.Cleanup(func() { os.Setenv("HOME", origHome) })
tb.Setenv("HOME", tmpHome)
tb.Setenv("CORE_HOME", tmpHome)
}
// --- Record ---
func TestMetrics_Record_Good(t *testing.T) {
t.Run("PersistsEvent", func(t *testing.T) {
withTempHome(t)
func TestRecord_Good(t *testing.T) {
withTempHome(t)
ev := Event{
Type: "test_event",
AgentID: "agent-1",
Repo: "repo-1",
Data: map[string]any{"key": "value"},
}
if err := Record(ev); err != nil {
t.Fatalf("Record: %v", err)
}
ev := Event{
Type: "test_event",
AgentID: "agent-1",
Repo: "repo-1",
Data: map[string]any{"key": "value"},
}
if err := Record(ev); err != nil {
t.Fatalf("Record: %v", err)
}
events, err := ReadEvents(time.Now().Add(-time.Hour))
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("expected 1 event, got %d", len(events))
}
if events[0].Type != "test_event" {
t.Errorf("expected type test_event, got %s", events[0].Type)
}
if events[0].Timestamp.IsZero() {
t.Error("expected auto-set timestamp, got zero")
}
})
events, err := ReadEvents(time.Now().Add(-time.Hour))
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("expected 1 event, got %d", len(events))
}
if events[0].Type != "test_event" {
t.Errorf("expected type test_event, got %s", events[0].Type)
}
if events[0].Timestamp.IsZero() {
t.Error("expected auto-set timestamp, got zero")
}
t.Run("AutoTimestamp", func(t *testing.T) {
withTempHome(t)
before := time.Now()
ev := Event{Type: "auto_ts"}
if err := Record(ev); err != nil {
t.Fatalf("Record: %v", err)
}
after := time.Now()
events, err := ReadEvents(before)
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("expected 1 event, got %d", len(events))
}
ts := events[0].Timestamp
if ts.Before(before) || ts.After(after) {
t.Errorf("timestamp %v not in range [%v, %v]", ts, before, after)
}
})
t.Run("PresetTimestamp", func(t *testing.T) {
withTempHome(t)
fixed := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)
ev := Event{Type: "preset_ts", Timestamp: fixed}
if err := Record(ev); err != nil {
t.Fatalf("Record: %v", err)
}
events, err := ReadEvents(fixed.Add(-time.Hour))
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("expected 1 event, got %d", len(events))
}
if !events[0].Timestamp.Equal(fixed) {
t.Errorf("expected timestamp %v, got %v", fixed, events[0].Timestamp)
}
})
}
func TestRecord_Good_AutoTimestamp(t *testing.T) {
withTempHome(t)
func TestMetrics_ReadEvents_Good(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
withTempHome(t)
before := time.Now()
ev := Event{Type: "auto_ts"}
if err := Record(ev); err != nil {
t.Fatalf("Record: %v", err)
}
after := time.Now()
events, err := ReadEvents(time.Now().Add(-24 * time.Hour))
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 0 {
t.Errorf("expected 0 events, got %d", len(events))
}
})
events, err := ReadEvents(before)
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("expected 1 event, got %d", len(events))
}
ts := events[0].Timestamp
if ts.Before(before) || ts.After(after) {
t.Errorf("timestamp %v not in range [%v, %v]", ts, before, after)
}
t.Run("MultiDay", func(t *testing.T) {
withTempHome(t)
now := time.Now()
yesterday := now.AddDate(0, 0, -1)
if err := Record(Event{Type: "yesterday", Timestamp: yesterday}); err != nil {
t.Fatalf("Record yesterday: %v", err)
}
if err := Record(Event{Type: "today", Timestamp: now}); err != nil {
t.Fatalf("Record today: %v", err)
}
events, err := ReadEvents(now.AddDate(0, 0, -2))
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 2 {
t.Fatalf("expected 2 events, got %d", len(events))
}
})
t.Run("FiltersBySince", func(t *testing.T) {
withTempHome(t)
now := time.Now()
if err := Record(Event{Type: "old", Timestamp: now.Add(-2 * time.Hour)}); err != nil {
t.Fatalf("Record old: %v", err)
}
if err := Record(Event{Type: "recent", Timestamp: now}); err != nil {
t.Fatalf("Record recent: %v", err)
}
events, err := ReadEvents(now.Add(-time.Hour))
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("expected 1 event, got %d", len(events))
}
if events[0].Type != "recent" {
t.Errorf("expected type recent, got %s", events[0].Type)
}
})
}
func TestRecord_Good_PresetTimestamp(t *testing.T) {
withTempHome(t)
func TestMetrics_ReadMetricsFile_Good(t *testing.T) {
t.Run("MalformedLines", func(t *testing.T) {
withTempHome(t)
fixed := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)
ev := Event{Type: "preset_ts", Timestamp: fixed}
if err := Record(ev); err != nil {
t.Fatalf("Record: %v", err)
}
dir, err := metricsDir()
if err != nil {
t.Fatalf("metricsDir: %v", err)
}
events, err := ReadEvents(fixed.Add(-time.Hour))
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("expected 1 event, got %d", len(events))
}
if !events[0].Timestamp.Equal(fixed) {
t.Errorf("expected timestamp %v, got %v", fixed, events[0].Timestamp)
}
}
// --- ReadEvents ---
func TestReadEvents_Good_Empty(t *testing.T) {
withTempHome(t)
events, err := ReadEvents(time.Now().Add(-24 * time.Hour))
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 0 {
t.Errorf("expected 0 events, got %d", len(events))
}
}
func TestReadEvents_Good_MultiDay(t *testing.T) {
withTempHome(t)
now := time.Now()
yesterday := now.AddDate(0, 0, -1)
// Write event for yesterday
ev1 := Event{Type: "yesterday", Timestamp: yesterday}
if err := Record(ev1); err != nil {
t.Fatalf("Record yesterday: %v", err)
}
// Write event for today
ev2 := Event{Type: "today", Timestamp: now}
if err := Record(ev2); err != nil {
t.Fatalf("Record today: %v", err)
}
// Read from 2 days ago — should get both
events, err := ReadEvents(now.AddDate(0, 0, -2))
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 2 {
t.Fatalf("expected 2 events, got %d", len(events))
}
}
func TestReadEvents_Good_FiltersBySince(t *testing.T) {
withTempHome(t)
now := time.Now()
// Write an old event and a recent one
old := Event{Type: "old", Timestamp: now.Add(-2 * time.Hour)}
if err := Record(old); err != nil {
t.Fatalf("Record old: %v", err)
}
recent := Event{Type: "recent", Timestamp: now}
if err := Record(recent); err != nil {
t.Fatalf("Record recent: %v", err)
}
// Read only events from the last hour
events, err := ReadEvents(now.Add(-time.Hour))
if err != nil {
t.Fatalf("ReadEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("expected 1 event, got %d", len(events))
}
if events[0].Type != "recent" {
t.Errorf("expected type recent, got %s", events[0].Type)
}
}
// --- readMetricsFile ---
func TestReadMetricsFile_Good_MalformedLines(t *testing.T) {
withTempHome(t)
dir, err := metricsDir()
if err != nil {
t.Fatalf("metricsDir: %v", err)
}
// Write a file with a mix of valid and invalid lines
path := metricsFilePath(dir, time.Now())
content := `{"type":"valid","timestamp":"2026-03-15T10:00:00Z"}
path := metricsFilePath(dir, time.Now())
content := `{"type":"valid","timestamp":"2026-03-15T10:00:00Z"}
not-json
{"type":"also_valid","timestamp":"2026-03-15T11:00:00Z"}
{broken
`
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("write test file: %v", err)
}
if err := coreio.Write(coreio.Local, path, content); err != nil {
t.Fatalf("write test file: %v", err)
}
events, err := readMetricsFile(path, time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
if err != nil {
t.Fatalf("readMetricsFile: %v", err)
}
if len(events) != 2 {
t.Errorf("expected 2 valid events (skipping malformed), got %d", len(events))
}
events, err := readMetricsFile(path, time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
if err != nil {
t.Fatalf("readMetricsFile: %v", err)
}
if len(events) != 2 {
t.Errorf("expected 2 valid events (skipping malformed), got %d", len(events))
}
})
t.Run("NonExistent", func(t *testing.T) {
events, err := readMetricsFile(core.Path(t.TempDir(), "nonexistent-metrics-file.jsonl"), time.Time{})
if err != nil {
t.Fatalf("expected nil error for missing file, got: %v", err)
}
if len(events) != 0 {
t.Errorf("expected 0 events, got %d", len(events))
}
})
}
func TestReadMetricsFile_Good_NonExistent(t *testing.T) {
events, err := readMetricsFile("/tmp/nonexistent-metrics-file.jsonl", time.Time{})
if err != nil {
t.Fatalf("expected nil error for missing file, got: %v", err)
}
if len(events) != 0 {
t.Errorf("expected 0 events, got %d", len(events))
}
}
// --- metricsFilePath ---
func TestMetricsFilePath_Good(t *testing.T) {
func TestMetrics_MetricsFilePath_Good(t *testing.T) {
ts := time.Date(2026, 3, 17, 14, 30, 0, 0, time.UTC)
got := metricsFilePath("/base", ts)
want := "/base/2026-03-17.jsonl"
@ -216,63 +201,78 @@ func TestMetricsFilePath_Good(t *testing.T) {
}
}
// --- Summary ---
func TestMetrics_Summary_Good(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
s := Summary(nil)
total, ok := s["total"].(int)
if !ok || total != 0 {
t.Errorf("expected total 0, got %v", s["total"])
}
})
func TestSummary_Good_Empty(t *testing.T) {
s := Summary(nil)
total, ok := s["total"].(int)
if !ok || total != 0 {
t.Errorf("expected total 0, got %v", s["total"])
}
t.Run("Aggregates", func(t *testing.T) {
events := []Event{
{Type: "build", Repo: "core-php", AgentID: "agent-1"},
{Type: "build", Repo: "core-php", AgentID: "agent-2"},
{Type: "test", Repo: "core-api", AgentID: "agent-1"},
}
s := Summary(events)
total, _ := s["total"].(int)
if total != 3 {
t.Errorf("expected total 3, got %d", total)
}
byType, _ := s["by_type"].([]map[string]any)
if len(byType) != 2 {
t.Fatalf("expected 2 types, got %d", len(byType))
}
if byType[0]["key"] != "build" || byType[0]["count"] != 2 {
t.Errorf("expected build:2 first, got %v:%v", byType[0]["key"], byType[0]["count"])
}
})
}
func TestSummary_Good(t *testing.T) {
events := []Event{
{Type: "build", Repo: "core-php", AgentID: "agent-1"},
{Type: "build", Repo: "core-php", AgentID: "agent-2"},
{Type: "test", Repo: "core-api", AgentID: "agent-1"},
}
func TestMetrics_SortedMap_Good(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
result := sortedMap(map[string]int{})
if len(result) != 0 {
t.Errorf("expected empty slice, got %d entries", len(result))
}
})
s := Summary(events)
t.Run("Ordering", func(t *testing.T) {
m := map[string]int{"a": 1, "b": 3, "c": 2}
result := sortedMap(m)
if len(result) != 3 {
t.Fatalf("expected 3 entries, got %d", len(result))
}
if result[0]["key"] != "b" {
t.Errorf("expected first key 'b', got %v", result[0]["key"])
}
if result[1]["key"] != "c" {
t.Errorf("expected second key 'c', got %v", result[1]["key"])
}
if result[2]["key"] != "a" {
t.Errorf("expected third key 'a', got %v", result[2]["key"])
}
})
total, _ := s["total"].(int)
if total != 3 {
t.Errorf("expected total 3, got %d", total)
}
byType, _ := s["by_type"].([]map[string]any)
if len(byType) != 2 {
t.Fatalf("expected 2 types, got %d", len(byType))
}
// Sorted by count descending — "build" (2) first
if byType[0]["key"] != "build" || byType[0]["count"] != 2 {
t.Errorf("expected build:2 first, got %v:%v", byType[0]["key"], byType[0]["count"])
}
}
// --- sortedMap ---
func TestSortedMap_Good_Empty(t *testing.T) {
result := sortedMap(map[string]int{})
if len(result) != 0 {
t.Errorf("expected empty slice, got %d entries", len(result))
}
}
func TestSortedMap_Good_Ordering(t *testing.T) {
m := map[string]int{"a": 1, "b": 3, "c": 2}
result := sortedMap(m)
if len(result) != 3 {
t.Fatalf("expected 3 entries, got %d", len(result))
}
// Descending by count
if result[0]["key"] != "b" {
t.Errorf("expected first key 'b', got %v", result[0]["key"])
}
if result[1]["key"] != "c" {
t.Errorf("expected second key 'c', got %v", result[1]["key"])
}
if result[2]["key"] != "a" {
t.Errorf("expected third key 'a', got %v", result[2]["key"])
}
t.Run("TieBreaksByKey", func(t *testing.T) {
m := map[string]int{"b": 1, "c": 1, "a": 1}
result := sortedMap(m)
if len(result) != 3 {
t.Fatalf("expected 3 entries, got %d", len(result))
}
if result[0]["key"] != "a" {
t.Errorf("expected first key 'a', got %v", result[0]["key"])
}
if result[1]["key"] != "b" {
t.Errorf("expected second key 'b', got %v", result[1]["key"])
}
if result[2]["key"] != "c" {
t.Errorf("expected third key 'c', got %v", result[2]["key"])
}
})
}

View file

@ -4,12 +4,16 @@ import (
"context"
"time"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core"
"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).
//
// Example:
//
// task := TaskInfo{Title: "Fix metrics", Description: "Need storage docs"}
type TaskInfo struct {
Title string
Description string
@ -18,8 +22,12 @@ type TaskInfo struct {
// 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.
//
// Example:
//
// context, err := QueryRAGForTask(TaskInfo{Title: "Deploy", Description: "Need release steps"})
func QueryRAGForTask(task TaskInfo) (string, error) {
query := task.Title + " " + task.Description
query := core.Concat(task.Title, " ", task.Description)
// Truncate to 500 runes to keep the embedding focused.
runes := []rune(query)
@ -30,14 +38,14 @@ func QueryRAGForTask(task TaskInfo) (string, error) {
qdrantCfg := rag.DefaultQdrantConfig()
qdrantClient, err := rag.NewQdrantClient(qdrantCfg)
if err != nil {
return "", coreerr.E("ai.QueryRAGForTask", "rag qdrant client", err)
return "", core.E("ai.QueryRAGForTask", "rag qdrant client", err)
}
defer func() { _ = qdrantClient.Close() }()
ollamaCfg := rag.DefaultOllamaConfig()
ollamaClient, err := rag.NewOllamaClient(ollamaCfg)
if err != nil {
return "", coreerr.E("ai.QueryRAGForTask", "rag ollama client", err)
return "", core.E("ai.QueryRAGForTask", "rag ollama client", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@ -51,7 +59,7 @@ func QueryRAGForTask(task TaskInfo) (string, error) {
results, err := rag.Query(ctx, qdrantClient, ollamaClient, query, queryCfg)
if err != nil {
return "", coreerr.E("ai.QueryRAGForTask", "rag query", err)
return "", core.E("ai.QueryRAGForTask", "rag query", err)
}
return rag.FormatResultsContext(results), nil

View file

@ -10,19 +10,14 @@
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"math"
"net/http"
"os"
"sort"
"strings"
"time"
coreerr "dappco.re/go/core/log"
"dappco.re/go/core"
)
var ollamaURL = flag.String("ollama", "http://localhost:11434", "Ollama base URL")
@ -91,16 +86,16 @@ var queries = []struct {
func main() {
flag.Parse()
fmt.Println("OpenBrain Embedding Model Benchmark")
fmt.Println(strings.Repeat("=", 60))
core.Println("OpenBrain Embedding Model Benchmark")
core.Println(repeat("=", 60))
for _, model := range models {
fmt.Printf("\n## Model: %s\n", model)
fmt.Println(strings.Repeat("-", 40))
core.Println(core.Sprintf("\n## Model: %s", model))
core.Println(repeat("-", 40))
// Check model is available
if !modelAvailable(model) {
fmt.Printf(" SKIPPED — model not pulled (run: ollama pull %s)\n", model)
core.Println(core.Sprintf(" SKIPPED - model not pulled (run: ollama pull %s)", model))
continue
}
@ -114,20 +109,20 @@ func main() {
}
}
fmt.Printf(" Embedding %d memories...\n", len(allMemories))
core.Println(core.Sprintf(" Embedding %d memories...", len(allMemories)))
start := time.Now()
memVectors := make([][]float64, len(allMemories))
for i, mem := range allMemories {
vec, err := embed(model, mem)
if err != nil {
fmt.Printf(" ERROR embedding memory %d: %v\n", i, err)
core.Println(core.Sprintf(" ERROR embedding memory %d: %v", i, err))
break
}
memVectors[i] = vec
}
embedTime := time.Since(start)
fmt.Printf(" Embedded in %v (%.0fms/memory)\n", embedTime, float64(embedTime.Milliseconds())/float64(len(allMemories)))
fmt.Printf(" Vector dimension: %d\n", len(memVectors[0]))
core.Println(core.Sprintf(" Embedded in %v (%.0fms/memory)", embedTime, float64(embedTime.Milliseconds())/float64(len(allMemories))))
core.Println(core.Sprintf(" Vector dimension: %d", len(memVectors[0])))
// 2. Intra-group vs inter-group similarity
var intraSims, interSims []float64
@ -146,18 +141,18 @@ func main() {
interAvg := avg(interSims)
separation := intraAvg - interAvg
fmt.Printf("\n Cluster separation:\n")
fmt.Printf(" Intra-group similarity (same topic): %.4f\n", intraAvg)
fmt.Printf(" Inter-group similarity (diff topic): %.4f\n", interAvg)
fmt.Printf(" Separation gap: %.4f %s\n", separation, qualityLabel(separation))
core.Println("\n Cluster separation:")
core.Println(core.Sprintf(" Intra-group similarity (same topic): %.4f", intraAvg))
core.Println(core.Sprintf(" Inter-group similarity (diff topic): %.4f", interAvg))
core.Println(core.Sprintf(" Separation gap: %.4f %s", separation, qualityLabel(separation)))
// 3. Query recall accuracy
fmt.Printf("\n Query recall (top-1 accuracy):\n")
core.Println("\n Query recall (top-1 accuracy):")
correct := 0
for _, q := range queries {
qVec, err := embed(model, q.query)
if err != nil {
fmt.Printf(" ERROR: %v\n", err)
core.Println(core.Sprintf(" ERROR: %v", err))
continue
}
@ -181,11 +176,11 @@ func main() {
if !hit {
marker = "✗"
}
fmt.Printf(" %s %.4f %q → %s (want: %s)\n", marker, bestSim, truncate(q.query, 40), matchTopic, q.targetTopic)
core.Println(core.Sprintf(" %s %.4f %q -> %s (want: %s)", marker, bestSim, truncate(q.query, 40), matchTopic, q.targetTopic))
}
accuracy := float64(correct) / float64(len(queries)) * 100
fmt.Printf("\n Top-1 accuracy: %.0f%% (%d/%d)\n", accuracy, correct, len(queries))
core.Println(core.Sprintf("\n Top-1 accuracy: %.0f%% (%d/%d)", accuracy, correct, len(queries)))
// 4. Top-3 recall
correct3 := 0
@ -210,11 +205,11 @@ func main() {
}
}
accuracy3 := float64(correct3) / float64(len(queries)) * 100
fmt.Printf(" Top-3 accuracy: %.0f%% (%d/%d)\n", accuracy3, correct3, len(queries))
core.Println(core.Sprintf(" Top-3 accuracy: %.0f%% (%d/%d)", accuracy3, correct3, len(queries)))
}
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Println("Done.")
core.Println(core.Concat("\n", repeat("=", 60)))
core.Println("Done.")
}
// -- Ollama helpers --
@ -236,40 +231,57 @@ type embedResponse struct {
}
func embed(model, text string) ([]float64, error) {
body, _ := json.Marshal(embedRequest{Model: model, Prompt: text})
resp, err := httpClient.Post(*ollamaURL+"/api/embeddings", "application/json", bytes.NewReader(body))
body := core.JSONMarshal(embedRequest{Model: model, Prompt: text})
if !body.OK {
return nil, core.E("embed", "marshal request", body.Value.(error))
}
resp, err := httpClient.Post(core.Concat(*ollamaURL, "/api/embeddings"), "application/json", core.NewReader(string(body.Value.([]byte))))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, coreerr.E("embed", fmt.Sprintf("HTTP %d", resp.StatusCode), nil)
if resp.StatusCode != http.StatusOK {
core.CloseStream(resp.Body)
return nil, core.E("embed", core.Sprintf("HTTP %d", resp.StatusCode), nil)
}
payload := core.ReadAll(resp.Body)
if !payload.OK {
return nil, core.E("embed", "read response body", payload.Value.(error))
}
var result embedResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
if decoded := core.JSONUnmarshalString(payload.Value.(string), &result); !decoded.OK {
return nil, core.E("embed", "decode response", decoded.Value.(error))
}
if len(result.Embedding) == 0 {
return nil, coreerr.E("embed", "empty embedding", nil)
return nil, core.E("embed", "empty embedding", nil)
}
return result.Embedding, nil
}
func modelAvailable(model string) bool {
resp, err := httpClient.Get(*ollamaURL + "/api/tags")
resp, err := httpClient.Get(core.Concat(*ollamaURL, "/api/tags"))
if err != nil {
return false
}
defer resp.Body.Close()
payload := core.ReadAll(resp.Body)
if !payload.OK {
return false
}
var result struct {
Models []struct {
Name string `json:"name"`
} `json:"models"`
}
json.NewDecoder(resp.Body).Decode(&result)
if decoded := core.JSONUnmarshalString(payload.Value.(string), &result); !decoded.OK {
return false
}
for _, m := range result.Models {
// Match "nomic-embed-text:latest" against "nomic-embed-text"
if m.Name == model || strings.HasPrefix(m.Name, model+":") {
if m.Name == model || core.HasPrefix(m.Name, core.Concat(model, ":")) {
return true
}
}
@ -316,14 +328,17 @@ func qualityLabel(gap float64) string {
}
}
func repeat(s string, n int) string {
b := core.NewBuilder()
for range n {
b.WriteString(s)
}
return b.String()
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n-3] + "..."
}
func init() {
// Ensure stderr doesn't buffer
os.Stderr.Sync()
}

View file

@ -5,10 +5,11 @@ package lab
import (
"context"
"io"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"forge.lthn.ai/core/cli/pkg/cli"
@ -43,6 +44,10 @@ func init() {
}
// AddLabCommands registers the 'lab' command and subcommands.
//
// Example:
//
// AddLabCommands(root)
func AddLabCommands(root *cli.Command) {
labCmd.AddCommand(serveCmd)
root.AddCommand(labCmd)
@ -53,7 +58,7 @@ func runServe(cmd *cli.Command, args []string) error {
cfg.Addr = labBind
store := lab.NewStore()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger := slog.New(slog.NewJSONHandler(io.Discard, nil))
// Setup collectors.
reg := collector.NewRegistry(logger)
@ -79,7 +84,7 @@ func runServe(cmd *cli.Command, args []string) error {
time.Duration(cfg.InfluxInterval)*time.Second)
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT)
defer cancel()
reg.Start(ctx)
defer reg.Stop()

View file

@ -2,14 +2,13 @@
package metrics
import (
"encoding/json"
"fmt"
"strconv"
"time"
"forge.lthn.ai/core/cli/pkg/cli"
"dappco.re/go/core"
"dappco.re/go/core/ai/ai"
"dappco.re/go/core/i18n"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/cli/pkg/cli"
)
var (
@ -32,6 +31,10 @@ func initMetricsFlags() {
}
// AddMetricsCommand adds the 'metrics' command to the parent.
//
// Example:
//
// AddMetricsCommand(root)
func AddMetricsCommand(parent *cli.Command) {
initMetricsFlags()
parent.AddCommand(metricsCmd)
@ -51,11 +54,11 @@ func runMetrics() error {
if metricsJSON {
summary := ai.Summary(events)
output, err := json.MarshalIndent(summary, "", " ")
if err != nil {
return cli.Wrap(err, "marshal JSON output")
output := core.JSONMarshal(summary)
if !output.OK {
return cli.Wrap(output.Value.(error), "marshal JSON output")
}
cli.Text(string(output))
cli.Text(string(output.Value.([]byte)))
return nil
}
@ -104,19 +107,19 @@ func runMetrics() error {
// parseDuration parses a human-friendly duration like "7d", "24h", "30d".
func parseDuration(s string) (time.Duration, error) {
if len(s) < 2 {
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", s), nil)
return 0, core.E("metrics.parseDuration", core.Concat("invalid duration: ", s), nil)
}
unit := s[len(s)-1]
value := s[:len(s)-1]
var n int
if _, err := fmt.Sscanf(value, "%d", &n); err != nil {
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", s), nil)
n, err := strconv.Atoi(value)
if err != nil {
return 0, core.E("metrics.parseDuration", core.Concat("invalid duration: ", s), err)
}
if n <= 0 {
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("duration must be positive: %s", s), nil)
return 0, core.E("metrics.parseDuration", core.Concat("duration must be positive: ", s), nil)
}
switch unit {
@ -127,6 +130,6 @@ func parseDuration(s string) (time.Duration, error) {
case 'm':
return time.Duration(n) * time.Minute, nil
default:
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("unknown unit %c in duration: %s", unit, s), nil)
return 0, core.E("metrics.parseDuration", core.Concat("unknown unit ", string(unit), " in duration: ", s), nil)
}
}

View file

@ -5,7 +5,7 @@ import (
"time"
)
func TestParseDuration_Good(t *testing.T) {
func TestCmd_ParseDuration_Good(t *testing.T) {
tests := []struct {
input string
want time.Duration
@ -29,7 +29,7 @@ func TestParseDuration_Good(t *testing.T) {
}
}
func TestParseDuration_Bad(t *testing.T) {
func TestCmd_ParseDuration_Bad(t *testing.T) {
bad := []string{
"", // too short
"d", // too short

View file

@ -4,4 +4,8 @@ package rag
import ragcmd "forge.lthn.ai/core/go-rag/cmd/rag"
// AddRAGSubcommands registers RAG commands as subcommands of parent.
//
// Example:
//
// AddRAGSubcommands(root)
var AddRAGSubcommands = ragcmd.AddRAGSubcommands

View file

@ -1,11 +1,9 @@
package security
import (
"encoding/json"
"fmt"
"forge.lthn.ai/core/cli/pkg/cli"
"dappco.re/go/core"
"dappco.re/go/core/i18n"
"forge.lthn.ai/core/cli/pkg/cli"
)
func addAlertsCommand(parent *cli.Command) {
@ -28,6 +26,10 @@ func addAlertsCommand(parent *cli.Command) {
}
// AlertOutput represents a unified alert for output.
//
// Example:
//
// var alert AlertOutput
type AlertOutput struct {
Repo string `json:"repo"`
Severity string `json:"severity"`
@ -63,11 +65,15 @@ func runAlerts() error {
summary := &AlertSummary{}
for _, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
repoFullName := core.Concat(reg.Org, "/", repo.Name)
fetchErrors := 0
// Fetch Dependabot alerts
depAlerts, err := fetchDependabotAlerts(repoFullName)
if err == nil {
if err != nil {
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
fetchErrors++
} else {
for _, alert := range depAlerts {
if alert.State != "open" {
continue
@ -91,7 +97,10 @@ func runAlerts() error {
// Fetch code scanning alerts
codeAlerts, err := fetchCodeScanningAlerts(repoFullName)
if err == nil {
if err != nil {
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
fetchErrors++
} else {
for _, alert := range codeAlerts {
if alert.State != "open" {
continue
@ -101,7 +110,7 @@ func runAlerts() error {
continue
}
summary.Add(severity)
location := fmt.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
location := core.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
allAlerts = append(allAlerts, AlertOutput{
Repo: repo.Name,
Severity: severity,
@ -115,7 +124,10 @@ func runAlerts() error {
// Fetch secret scanning alerts
secretAlerts, err := fetchSecretScanningAlerts(repoFullName)
if err == nil {
if err != nil {
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
fetchErrors++
} else {
for _, alert := range secretAlerts {
if alert.State != "open" {
continue
@ -127,21 +139,20 @@ func runAlerts() error {
allAlerts = append(allAlerts, AlertOutput{
Repo: repo.Name,
Severity: "high",
ID: fmt.Sprintf("secret-%d", alert.Number),
ID: core.Sprintf("secret-%d", alert.Number),
Type: "secret-scanning",
Message: alert.SecretType,
})
}
}
if fetchErrors == 3 {
cli.Print("%s %s: all alert sources failed\n", cli.WarningStyle.Render(">>"), repoFullName)
}
}
if securityJSON {
output, err := json.MarshalIndent(allAlerts, "", " ")
if err != nil {
return cli.Wrap(err, "marshal JSON output")
}
cli.Text(string(output))
return nil
return printJSON(allAlerts)
}
// Print summary
@ -163,12 +174,12 @@ func runAlerts() error {
location = alert.Location
}
if alert.Version != "" {
location = fmt.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
location = core.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
}
cli.Print("%-20s %s %-16s %-40s %s\n",
cli.ValueStyle.Render(alert.Repo),
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
alert.ID,
location,
cli.DimStyle.Render(alert.Type),
@ -188,10 +199,14 @@ func runAlertsForTarget(target string) error {
var allAlerts []AlertOutput
summary := &AlertSummary{}
fetchErrors := 0
// Fetch Dependabot alerts
depAlerts, err := fetchDependabotAlerts(fullName)
if err == nil {
if err != nil {
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), fullName, err)
fetchErrors++
} else {
for _, alert := range depAlerts {
if alert.State != "open" {
continue
@ -215,7 +230,10 @@ func runAlertsForTarget(target string) error {
// Fetch code scanning alerts
codeAlerts, err := fetchCodeScanningAlerts(fullName)
if err == nil {
if err != nil {
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), fullName, err)
fetchErrors++
} else {
for _, alert := range codeAlerts {
if alert.State != "open" {
continue
@ -225,7 +243,7 @@ func runAlertsForTarget(target string) error {
continue
}
summary.Add(severity)
location := fmt.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
location := core.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
allAlerts = append(allAlerts, AlertOutput{
Repo: repo.Name,
Severity: severity,
@ -239,7 +257,10 @@ func runAlertsForTarget(target string) error {
// Fetch secret scanning alerts
secretAlerts, err := fetchSecretScanningAlerts(fullName)
if err == nil {
if err != nil {
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), fullName, err)
fetchErrors++
} else {
for _, alert := range secretAlerts {
if alert.State != "open" {
continue
@ -251,20 +272,19 @@ func runAlertsForTarget(target string) error {
allAlerts = append(allAlerts, AlertOutput{
Repo: repo.Name,
Severity: "high",
ID: fmt.Sprintf("secret-%d", alert.Number),
ID: core.Sprintf("secret-%d", alert.Number),
Type: "secret-scanning",
Message: alert.SecretType,
})
}
}
if fetchErrors == 3 {
return cli.Err("failed to fetch alerts for %s", fullName)
}
if securityJSON {
output, err := json.MarshalIndent(allAlerts, "", " ")
if err != nil {
return cli.Wrap(err, "marshal JSON output")
}
cli.Text(string(output))
return nil
return printJSON(allAlerts)
}
cli.Blank()
@ -282,11 +302,11 @@ func runAlertsForTarget(target string) error {
location = alert.Location
}
if alert.Version != "" {
location = fmt.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
location = core.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
}
cli.Print("%-20s %s %-16s %-40s %s\n",
cli.ValueStyle.Render(alert.Repo),
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
alert.ID,
location,
cli.DimStyle.Render(alert.Type),
@ -298,43 +318,43 @@ func runAlertsForTarget(target string) error {
}
func fetchDependabotAlerts(repoFullName string) ([]DependabotAlert, error) {
endpoint := fmt.Sprintf("repos/%s/dependabot/alerts?state=open", repoFullName)
endpoint := core.Concat("repos/", repoFullName, "/dependabot/alerts?state=open")
output, err := runGHAPI(endpoint)
if err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("fetch dependabot alerts for %s", repoFullName))
return nil, cli.Wrap(err, core.Sprintf("fetch dependabot alerts for %s", repoFullName))
}
var alerts []DependabotAlert
if err := json.Unmarshal(output, &alerts); err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("parse dependabot alerts for %s", repoFullName))
if decoded := core.JSONUnmarshal(output, &alerts); !decoded.OK {
return nil, cli.Wrap(decoded.Value.(error), core.Sprintf("parse dependabot alerts for %s", repoFullName))
}
return alerts, nil
}
func fetchCodeScanningAlerts(repoFullName string) ([]CodeScanningAlert, error) {
endpoint := fmt.Sprintf("repos/%s/code-scanning/alerts?state=open", repoFullName)
endpoint := core.Concat("repos/", repoFullName, "/code-scanning/alerts?state=open")
output, err := runGHAPI(endpoint)
if err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("fetch code-scanning alerts for %s", repoFullName))
return nil, cli.Wrap(err, core.Sprintf("fetch code-scanning alerts for %s", repoFullName))
}
var alerts []CodeScanningAlert
if err := json.Unmarshal(output, &alerts); err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("parse code-scanning alerts for %s", repoFullName))
if decoded := core.JSONUnmarshal(output, &alerts); !decoded.OK {
return nil, cli.Wrap(decoded.Value.(error), core.Sprintf("parse code-scanning alerts for %s", repoFullName))
}
return alerts, nil
}
func fetchSecretScanningAlerts(repoFullName string) ([]SecretScanningAlert, error) {
endpoint := fmt.Sprintf("repos/%s/secret-scanning/alerts?state=open", repoFullName)
endpoint := core.Concat("repos/", repoFullName, "/secret-scanning/alerts?state=open")
output, err := runGHAPI(endpoint)
if err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("fetch secret-scanning alerts for %s", repoFullName))
return nil, cli.Wrap(err, core.Sprintf("fetch secret-scanning alerts for %s", repoFullName))
}
var alerts []SecretScanningAlert
if err := json.Unmarshal(output, &alerts); err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("parse secret-scanning alerts for %s", repoFullName))
if decoded := core.JSONUnmarshal(output, &alerts); !decoded.OK {
return nil, cli.Wrap(decoded.Value.(error), core.Sprintf("parse secret-scanning alerts for %s", repoFullName))
}
return alerts, nil
}

View file

@ -1,11 +1,9 @@
package security
import (
"encoding/json"
"fmt"
"forge.lthn.ai/core/cli/pkg/cli"
"dappco.re/go/core"
"dappco.re/go/core/i18n"
"forge.lthn.ai/core/cli/pkg/cli"
)
func addDepsCommand(parent *cli.Command) {
@ -28,6 +26,10 @@ func addDepsCommand(parent *cli.Command) {
}
// DepAlert represents a dependency vulnerability for output.
//
// Example:
//
// var alert DepAlert
type DepAlert struct {
Repo string `json:"repo"`
Severity string `json:"severity"`
@ -64,7 +66,7 @@ func runDeps() error {
summary := &AlertSummary{}
for _, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
repoFullName := core.Concat(reg.Org, "/", repo.Name)
alerts, err := fetchDependabotAlerts(repoFullName)
if err != nil {
@ -100,12 +102,7 @@ func runDeps() error {
}
if securityJSON {
output, err := json.MarshalIndent(allAlerts, "", " ")
if err != nil {
return cli.Wrap(err, "marshal JSON output")
}
cli.Text(string(output))
return nil
return printJSON(allAlerts)
}
// Print summary
@ -124,12 +121,12 @@ func runDeps() error {
// Format upgrade suggestion
upgrade := alert.Vulnerable
if alert.PatchedVersion != "" {
upgrade = fmt.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
upgrade = core.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
}
cli.Print("%-16s %s %-16s %-30s %s\n",
cli.ValueStyle.Render(alert.Repo),
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
alert.CVE,
alert.Package,
upgrade,
@ -178,12 +175,7 @@ func runDepsForTarget(target string) error {
}
if securityJSON {
output, err := json.MarshalIndent(allAlerts, "", " ")
if err != nil {
return cli.Wrap(err, "marshal JSON output")
}
cli.Text(string(output))
return nil
return printJSON(allAlerts)
}
cli.Blank()
@ -194,11 +186,11 @@ func runDepsForTarget(target string) error {
sevStyle := severityStyle(alert.Severity)
upgrade := alert.Vulnerable
if alert.PatchedVersion != "" {
upgrade = fmt.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
upgrade = core.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
}
cli.Print("%-16s %s %-16s %-30s %s\n",
cli.ValueStyle.Render(alert.Repo),
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
alert.CVE,
alert.Package,
upgrade,

View file

@ -1,15 +1,12 @@
package security
import (
"fmt"
"os/exec"
"strings"
"time"
"forge.lthn.ai/core/cli/pkg/cli"
"dappco.re/go/core"
"dappco.re/go/core/ai/ai"
"dappco.re/go/core/i18n"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/cli/pkg/cli"
)
var (
@ -67,9 +64,9 @@ func runJobs() error {
}
func createJobForTarget(target string) error {
parts := strings.SplitN(target, "/", 2)
parts := core.SplitN(target, "/", 2)
if len(parts) != 2 {
return coreerr.E("security.createJobForTarget", "invalid target format: use owner/repo", nil)
return core.E("security.createJobForTarget", "invalid target format: use owner/repo", nil)
}
// Gather findings
@ -93,8 +90,8 @@ func createJobForTarget(target string) error {
severity = "medium"
}
summary.Add(severity)
findings = append(findings, fmt.Sprintf("- [%s] %s: %s (%s:%d)",
strings.ToUpper(severity), alert.Tool.Name, alert.Rule.Description,
findings = append(findings, core.Sprintf("- [%s] %s: %s (%s:%d)",
core.Upper(severity), alert.Tool.Name, alert.Rule.Description,
alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine))
}
}
@ -111,8 +108,8 @@ func createJobForTarget(target string) error {
continue
}
summary.Add(alert.Advisory.Severity)
findings = append(findings, fmt.Sprintf("- [%s] %s: %s (%s)",
strings.ToUpper(alert.Advisory.Severity), alert.Dependency.Package.Name,
findings = append(findings, core.Sprintf("- [%s] %s: %s (%s)",
core.Upper(alert.Advisory.Severity), alert.Dependency.Package.Name,
alert.Advisory.Summary, alert.Advisory.CVEID))
}
}
@ -129,12 +126,12 @@ func createJobForTarget(target string) error {
continue
}
summary.Add("high")
findings = append(findings, fmt.Sprintf("- [HIGH] Secret: %s (#%d)", alert.SecretType, alert.Number))
findings = append(findings, core.Sprintf("- [HIGH] Secret: %s (#%d)", alert.SecretType, alert.Number))
}
}
if fetchErrors == 3 {
return coreerr.E("security.createJobForTarget", fmt.Sprintf("failed to fetch any alerts for %s", target), nil)
return core.E("security.createJobForTarget", core.Sprintf("failed to fetch any alerts for %s", target), nil)
}
if summary.Total == 0 {
@ -143,13 +140,13 @@ func createJobForTarget(target string) error {
}
// Build issue body
title := fmt.Sprintf("Security scan: %s", target)
title := core.Sprintf("Security scan: %s", target)
body := buildJobIssueBody(target, summary, findings)
for i := range jobsCopies {
issueTitle := title
if jobsCopies > 1 {
issueTitle = fmt.Sprintf("%s (#%d)", title, i+1)
issueTitle = core.Sprintf("%s (#%d)", title, i+1)
}
if jobsDryRun {
@ -161,20 +158,16 @@ func createJobForTarget(target string) error {
continue
}
// Create issue via gh CLI
cmd := exec.Command("gh", "issue", "create",
"--repo", jobsIssueRepo,
"--title", issueTitle,
"--body", body,
"--label", "type:security-scan,repo:"+target,
issueURL, err := createGitHubIssue(
jobsIssueRepo,
issueTitle,
body,
[]string{"type:security-scan", core.Concat("repo:", target)},
)
output, err := cmd.CombinedOutput()
if err != nil {
return cli.Wrap(err, fmt.Sprintf("create issue for %s: %s", target, string(output)))
return cli.Wrap(err, core.Sprintf("create issue for %s", target))
}
issueURL := strings.TrimSpace(string(output))
cli.Print("%s %s: %s\n", cli.SuccessStyle.Render(">>"), issueTitle, issueURL)
// Record metrics
@ -196,10 +189,10 @@ func createJobForTarget(target string) error {
}
func buildJobIssueBody(target string, summary *AlertSummary, findings []string) string {
var sb strings.Builder
sb := core.NewBuilder()
fmt.Fprintf(&sb, "## Security Scan: %s\n\n", target)
fmt.Fprintf(&sb, "**Summary:** %s\n\n", summary.String())
sb.WriteString(core.Sprintf("## Security Scan: %s\n\n", target))
sb.WriteString(core.Sprintf("**Summary:** %s\n\n", summary.PlainString()))
sb.WriteString("### Findings\n\n")
if len(findings) > 50 {
@ -207,7 +200,7 @@ func buildJobIssueBody(target string, summary *AlertSummary, findings []string)
for _, f := range findings[:50] {
sb.WriteString(f + "\n")
}
fmt.Fprintf(&sb, "\n... and %d more\n", len(findings)-50)
sb.WriteString(core.Sprintf("\n... and %d more\n", len(findings)-50))
} else {
for _, f := range findings {
sb.WriteString(f + "\n")
@ -222,7 +215,7 @@ func buildJobIssueBody(target string, summary *AlertSummary, findings []string)
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(core.Sprintf("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")

View file

@ -1,13 +1,12 @@
package security
import (
"encoding/json"
"fmt"
"time"
"dappco.re/go/core"
"dappco.re/go/core/ai/ai"
"forge.lthn.ai/core/cli/pkg/cli"
"dappco.re/go/core/i18n"
"forge.lthn.ai/core/cli/pkg/cli"
)
var (
@ -35,6 +34,10 @@ func addScanCommand(parent *cli.Command) {
}
// ScanAlert represents a code scanning alert for output.
//
// Example:
//
// var alert ScanAlert
type ScanAlert struct {
Repo string `json:"repo"`
Severity string `json:"severity"`
@ -70,7 +73,7 @@ func runScan() error {
summary := &AlertSummary{}
for _, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
repoFullName := core.Concat(reg.Org, "/", repo.Name)
alerts, err := fetchCodeScanningAlerts(repoFullName)
if err != nil {
@ -127,12 +130,7 @@ func runScan() error {
})
if securityJSON {
output, err := json.MarshalIndent(allAlerts, "", " ")
if err != nil {
return cli.Wrap(err, "marshal JSON output")
}
cli.Text(string(output))
return nil
return printJSON(allAlerts)
}
// Print summary
@ -148,11 +146,11 @@ func runScan() error {
for _, alert := range allAlerts {
sevStyle := severityStyle(alert.Severity)
location := fmt.Sprintf("%s:%d", alert.Path, alert.Line)
location := core.Sprintf("%s:%d", alert.Path, alert.Line)
cli.Print("%-16s %s %-20s %-40s %s\n",
cli.ValueStyle.Render(alert.Repo),
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
alert.RuleID,
location,
cli.DimStyle.Render(alert.Tool),
@ -221,12 +219,7 @@ func runScanForTarget(target string) error {
})
if securityJSON {
output, err := json.MarshalIndent(allAlerts, "", " ")
if err != nil {
return cli.Wrap(err, "marshal JSON output")
}
cli.Text(string(output))
return nil
return printJSON(allAlerts)
}
cli.Blank()
@ -239,10 +232,10 @@ func runScanForTarget(target string) error {
for _, alert := range allAlerts {
sevStyle := severityStyle(alert.Severity)
location := fmt.Sprintf("%s:%d", alert.Path, alert.Line)
location := core.Sprintf("%s:%d", alert.Path, alert.Line)
cli.Print("%-16s %s %-20s %-40s %s\n",
cli.ValueStyle.Render(alert.Repo),
sevStyle.Render(fmt.Sprintf("%-8s", alert.Severity)),
sevStyle.Render(core.Sprintf("%-8s", alert.Severity)),
alert.RuleID,
location,
cli.DimStyle.Render(alert.Tool),

View file

@ -1,11 +1,9 @@
package security
import (
"encoding/json"
"fmt"
"forge.lthn.ai/core/cli/pkg/cli"
"dappco.re/go/core"
"dappco.re/go/core/i18n"
"forge.lthn.ai/core/cli/pkg/cli"
)
func addSecretsCommand(parent *cli.Command) {
@ -27,6 +25,10 @@ func addSecretsCommand(parent *cli.Command) {
}
// SecretAlert represents a secret scanning alert for output.
//
// Example:
//
// var alert SecretAlert
type SecretAlert struct {
Repo string `json:"repo"`
Number int `json:"number"`
@ -60,10 +62,11 @@ func runSecrets() error {
openCount := 0
for _, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
repoFullName := core.Concat(reg.Org, "/", repo.Name)
alerts, err := fetchSecretScanningAlerts(repoFullName)
if err != nil {
cli.Print("%s %s: %v\n", cli.WarningStyle.Render(">>"), repoFullName, err)
continue
}
@ -86,18 +89,13 @@ func runSecrets() error {
}
if securityJSON {
output, err := json.MarshalIndent(allAlerts, "", " ")
if err != nil {
return cli.Wrap(err, "marshal JSON output")
}
cli.Text(string(output))
return nil
return printJSON(allAlerts)
}
// Print summary
cli.Blank()
if openCount > 0 {
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets:"), cli.ErrorStyle.Render(fmt.Sprintf("%d open", openCount)))
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets:"), cli.ErrorStyle.Render(core.Sprintf("%d open", openCount)))
} else {
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets:"), cli.SuccessStyle.Render("No exposed secrets"))
}
@ -157,19 +155,14 @@ func runSecretsForTarget(target string) error {
}
if securityJSON {
output, err := json.MarshalIndent(allAlerts, "", " ")
if err != nil {
return cli.Wrap(err, "marshal JSON output")
}
cli.Text(string(output))
return nil
return printJSON(allAlerts)
}
cli.Blank()
if openCount > 0 {
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets ("+fullName+"):"), cli.ErrorStyle.Render(fmt.Sprintf("%d open", openCount)))
cli.Print("%s %s\n", cli.DimStyle.Render(core.Concat("Secrets (", fullName, "):")), cli.ErrorStyle.Render(core.Sprintf("%d open", openCount)))
} else {
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets ("+fullName+"):"), cli.SuccessStyle.Render("No exposed secrets"))
cli.Print("%s %s\n", cli.DimStyle.Render(core.Concat("Secrets (", fullName, "):")), cli.SuccessStyle.Render("No exposed secrets"))
}
cli.Blank()

View file

@ -1,15 +1,12 @@
package security
import (
"fmt"
"os/exec"
"slices"
"strings"
"forge.lthn.ai/core/cli/pkg/cli"
"dappco.re/go/core"
"dappco.re/go/core/i18n"
"dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/go-scm/repos"
)
@ -23,6 +20,10 @@ var (
)
// AddSecurityCommands adds the 'security' command to the root.
//
// Example:
//
// AddSecurityCommands(root)
func AddSecurityCommands(root *cli.Command) {
secCmd := &cli.Command{
Use: "security",
@ -40,6 +41,10 @@ func AddSecurityCommands(root *cli.Command) {
}
// DependabotAlert represents a Dependabot vulnerability alert.
//
// Example:
//
// var alert DependabotAlert
type DependabotAlert struct {
Number int `json:"number"`
State string `json:"state"`
@ -69,6 +74,10 @@ type DependabotAlert struct {
}
// CodeScanningAlert represents a code scanning alert.
//
// Example:
//
// var alert CodeScanningAlert
type CodeScanningAlert struct {
Number int `json:"number"`
State string `json:"state"`
@ -96,6 +105,10 @@ type CodeScanningAlert struct {
}
// SecretScanningAlert represents a secret scanning alert.
//
// Example:
//
// var alert SecretScanningAlert
type SecretScanningAlert struct {
Number int `json:"number"`
State string `json:"state"`
@ -126,37 +139,9 @@ func loadRegistry(registryPath string) (*repos.Registry, error) {
return reg, nil
}
// checkGH verifies gh CLI is available.
func checkGH() error {
if _, err := exec.LookPath("gh"); err != nil {
return coreerr.E("security.checkGH", i18n.T("error.gh_not_found"), nil)
}
return nil
}
// runGHAPI runs a gh api command and returns the output.
func runGHAPI(endpoint string) ([]byte, error) {
cmd := exec.Command("gh", "api", endpoint, "--paginate")
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
stderr := string(exitErr.Stderr)
// Handle common errors gracefully
if strings.Contains(stderr, "404") || strings.Contains(stderr, "Not Found") {
return []byte("[]"), nil // Return empty array for not found
}
if strings.Contains(stderr, "403") {
return nil, coreerr.E("security.runGHAPI", "access denied (check token permissions)", nil)
}
}
return nil, cli.Wrap(err, "run gh api")
}
return output, nil
}
// severityStyle returns the appropriate style for a severity level.
func severityStyle(severity string) *cli.AnsiStyle {
switch strings.ToLower(severity) {
switch core.Lower(severity) {
case "critical":
return cli.ErrorStyle
case "high":
@ -174,9 +159,9 @@ func filterBySeverity(severity, filter string) bool {
return true
}
sev := strings.ToLower(severity)
return slices.ContainsFunc(slices.Collect(strings.SplitSeq(strings.ToLower(filter), ",")), func(s string) bool {
return strings.TrimSpace(s) == sev
sev := core.Lower(severity)
return slices.ContainsFunc(core.Split(core.Lower(filter), ","), func(s string) bool {
return core.Trim(s) == sev
})
}
@ -193,7 +178,7 @@ func getReposToCheck(reg *repos.Registry, repoFilter string) []*repos.Repo {
// buildTargetRepo creates a synthetic Repo entry for an external target (e.g. "wailsapp/wails").
func buildTargetRepo(target string) (*repos.Repo, string) {
parts := strings.SplitN(target, "/", 2)
parts := core.SplitN(target, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return nil, ""
}
@ -201,6 +186,10 @@ func buildTargetRepo(target string) (*repos.Repo, string) {
}
// AlertSummary holds aggregated alert counts.
//
// Example:
//
// summary := &AlertSummary{}
type AlertSummary struct {
Critical int
High int
@ -211,9 +200,13 @@ type AlertSummary struct {
}
// Add increments summary counters for the provided severity.
//
// Example:
//
// summary.Add("high")
func (s *AlertSummary) Add(severity string) {
s.Total++
switch strings.ToLower(severity) {
switch core.Lower(severity) {
case "critical":
s.Critical++
case "high":
@ -228,25 +221,51 @@ func (s *AlertSummary) Add(severity string) {
}
// String renders a human-readable summary of alert counts.
//
// Example:
//
// text := summary.String()
func (s *AlertSummary) String() string {
parts := []string{}
if s.Critical > 0 {
parts = append(parts, cli.ErrorStyle.Render(fmt.Sprintf("%d critical", s.Critical)))
}
if s.High > 0 {
parts = append(parts, cli.WarningStyle.Render(fmt.Sprintf("%d high", s.High)))
}
if s.Medium > 0 {
parts = append(parts, cli.ValueStyle.Render(fmt.Sprintf("%d medium", s.Medium)))
}
if s.Low > 0 {
parts = append(parts, cli.DimStyle.Render(fmt.Sprintf("%d low", s.Low)))
}
if s.Unknown > 0 {
parts = append(parts, cli.DimStyle.Render(fmt.Sprintf("%d unknown", s.Unknown)))
}
if len(parts) == 0 {
return cli.SuccessStyle.Render("No alerts")
}
return strings.Join(parts, " | ")
return s.renderSummary(func(level, text string) string {
switch level {
case "critical":
return cli.ErrorStyle.Render(text)
case "high":
return cli.WarningStyle.Render(text)
case "medium":
return cli.ValueStyle.Render(text)
case "none":
return cli.SuccessStyle.Render(text)
default:
return cli.DimStyle.Render(text)
}
})
}
// PlainString renders an unstyled summary suitable for logs and issue bodies.
func (s *AlertSummary) PlainString() string {
return s.renderSummary(func(_ string, text string) string {
return text
})
}
func (s *AlertSummary) renderSummary(render func(level, text string) string) string {
parts := []string{}
appendPart := func(level string, count int) {
if count > 0 {
parts = append(parts, render(level, core.Sprintf("%d %s", count, level)))
}
}
appendPart("critical", s.Critical)
appendPart("high", s.High)
appendPart("medium", s.Medium)
appendPart("low", s.Low)
appendPart("unknown", s.Unknown)
if len(parts) == 0 {
return render("none", "No alerts")
}
return core.Join(" | ", parts...)
}

View file

@ -0,0 +1,48 @@
// SPDX-License-Identifier: EUPL-1.2
package security
import (
"strings"
"testing"
)
func TestAlertSummary_PlainString_Good(t *testing.T) {
t.Run("RendersWithoutANSI", func(t *testing.T) {
summary := &AlertSummary{
Critical: 1,
High: 2,
Medium: 3,
Low: 4,
Unknown: 5,
}
got := summary.PlainString()
want := "1 critical | 2 high | 3 medium | 4 low | 5 unknown"
if got != want {
t.Fatalf("PlainString() = %q, want %q", got, want)
}
if strings.Contains(got, "\x1b[") {
t.Fatalf("PlainString() unexpectedly contains ANSI escapes: %q", got)
}
})
t.Run("RendersEmpty", func(t *testing.T) {
summary := &AlertSummary{}
if got := summary.PlainString(); got != "No alerts" {
t.Fatalf("PlainString() = %q, want %q", got, "No alerts")
}
})
}
func TestSecurity_BuildJobIssueBody_Good(t *testing.T) {
summary := &AlertSummary{Critical: 1, High: 2}
body := buildJobIssueBody("owner/repo", summary, []string{"- [HIGH] Finding"})
if strings.Contains(body, "\x1b[") {
t.Fatalf("issue body unexpectedly contains ANSI escapes: %q", body)
}
if !strings.Contains(body, "**Summary:** 1 critical | 2 high") {
t.Fatalf("issue body missing plain summary: %q", body)
}
}

176
cmd/security/github.go Normal file
View file

@ -0,0 +1,176 @@
// SPDX-License-Identifier: EUPL-1.2
package security
import (
"net/http"
"dappco.re/go/core"
"forge.lthn.ai/core/cli/pkg/cli"
)
const githubAPIBase = "https://api.github.com"
func printJSON(value any) error {
output := core.JSONMarshal(value)
if !output.OK {
return cli.Wrap(output.Value.(error), "marshal JSON output")
}
cli.Text(string(output.Value.([]byte)))
return nil
}
func githubToken() string {
if token := core.Env("GH_TOKEN"); token != "" {
return token
}
return core.Env("GITHUB_TOKEN")
}
// checkGH verifies GitHub API authentication is configured.
func checkGH() error {
if githubToken() == "" {
return core.E("security.checkGH", "missing GitHub token (set GH_TOKEN or GITHUB_TOKEN)", nil)
}
return nil
}
// runGHAPI fetches all paginated GitHub API results for an array endpoint.
func runGHAPI(endpoint string) ([]byte, error) {
pages, err := runGHAPIPages(endpoint)
if err != nil {
return nil, err
}
return []byte(combineGitHubPages(pages)), nil
}
func runGHAPIPages(endpoint string) ([]string, error) {
nextURL := core.Concat(githubAPIBase, "/", endpoint)
pages := make([]string, 0, 1)
for nextURL != "" {
status, body, headers, err := executeGitHubRequest(http.MethodGet, nextURL, "")
if err != nil {
return nil, err
}
switch status {
case http.StatusOK:
case http.StatusNotFound:
return nil, nil
case http.StatusForbidden:
return nil, core.E("security.runGHAPI", "access denied (check token permissions)", nil)
default:
msg := core.Sprintf("GitHub API GET %d", status)
if trimmed := core.Trim(body); trimmed != "" {
msg = core.Concat(msg, ": ", trimmed)
}
return nil, core.E("security.runGHAPI", msg, nil)
}
pages = append(pages, body)
nextURL = nextGitHubPage(headers.Get("Link"))
}
return pages, nil
}
func createGitHubIssue(repo, title, body string, labels []string) (string, error) {
payload := core.JSONMarshal(map[string]any{
"title": title,
"body": body,
"labels": labels,
})
if !payload.OK {
return "", core.E("security.createGitHubIssue", "marshal issue request", payload.Value.(error))
}
status, responseBody, _, err := executeGitHubRequest(
http.MethodPost,
core.Concat(githubAPIBase, "/repos/", repo, "/issues"),
string(payload.Value.([]byte)),
)
if err != nil {
return "", err
}
if status < http.StatusOK || status >= http.StatusMultipleChoices {
msg := core.Sprintf("GitHub issue create %d", status)
if trimmed := core.Trim(responseBody); trimmed != "" {
msg = core.Concat(msg, ": ", trimmed)
}
return "", core.E("security.createGitHubIssue", msg, nil)
}
var response struct {
HTMLURL string `json:"html_url"`
}
if decoded := core.JSONUnmarshalString(responseBody, &response); !decoded.OK {
return "", core.E("security.createGitHubIssue", "decode issue response", decoded.Value.(error))
}
if response.HTMLURL == "" {
return "", core.E("security.createGitHubIssue", "issue response missing html_url", nil)
}
return response.HTMLURL, nil
}
func executeGitHubRequest(method, targetURL, body string) (int, string, http.Header, error) {
req, err := http.NewRequest(method, targetURL, core.NewReader(body))
if err != nil {
return 0, "", nil, core.E("security.executeGitHubRequest", "build request", err)
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "core-ai-security")
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
if token := githubToken(); token != "" {
req.Header.Set("Authorization", core.Concat("Bearer ", token))
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return 0, "", nil, core.E("security.executeGitHubRequest", "send request", err)
}
payload := core.ReadAll(resp.Body)
if !payload.OK {
return 0, "", nil, core.E("security.executeGitHubRequest", "read response body", payload.Value.(error))
}
return resp.StatusCode, payload.Value.(string), resp.Header, nil
}
func combineGitHubPages(pages []string) string {
parts := make([]string, 0, len(pages))
for _, page := range pages {
trimmed := core.Trim(page)
if trimmed == "" || trimmed == "[]" {
continue
}
trimmed = core.TrimPrefix(trimmed, "[")
trimmed = core.TrimSuffix(trimmed, "]")
trimmed = core.Trim(trimmed)
if trimmed != "" {
parts = append(parts, trimmed)
}
}
if len(parts) == 0 {
return "[]"
}
return core.Concat("[", core.Join(",", parts...), "]")
}
func nextGitHubPage(linkHeader string) string {
for _, part := range core.Split(linkHeader, ",") {
if !core.Contains(part, `rel="next"`) {
continue
}
urlPart := core.Trim(core.SplitN(part, ";", 2)[0])
urlPart = core.TrimPrefix(urlPart, "<")
return core.TrimSuffix(urlPart, ">")
}
return ""
}

28
go.mod
View file

@ -3,16 +3,16 @@ module dappco.re/go/core/ai
go 1.26.0
require (
dappco.re/go/core/i18n v0.1.7
dappco.re/go/core/io v0.1.7
dappco.re/go/core/log v0.0.4
dappco.re/go/core v0.8.0-alpha.1
dappco.re/go/core/i18n v0.2.0
dappco.re/go/core/io v0.2.0
forge.lthn.ai/core/cli v0.3.7
forge.lthn.ai/core/go-rag v0.1.11
forge.lthn.ai/core/go-scm v0.2.0
)
require (
dappco.re/go/core v0.4.7 // indirect
dappco.re/go/core/log v0.1.0 // indirect
forge.lthn.ai/core/go v0.3.3 // indirect
forge.lthn.ai/core/go-i18n v0.1.7 // indirect
forge.lthn.ai/core/go-inference v0.1.7 // indirect
@ -57,23 +57,3 @@ require (
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
dappco.re/go/core => ../go
dappco.re/go/core/i18n => ../go-i18n
dappco.re/go/core/io => ../go-io
dappco.re/go/core/log => ../go-log
dappco.re/go/core/webview => ../go-webview
dappco.re/go/core/ws => ../go-ws
forge.lthn.ai/core/api => ../api
forge.lthn.ai/core/cli => ../cli
forge.lthn.ai/core/go-crypt => ../go-crypt
forge.lthn.ai/core/go-inference => ../go-inference
forge.lthn.ai/core/go-ml => ../go-ml
forge.lthn.ai/core/go-mlx => ../go-mlx
forge.lthn.ai/core/go-process => ../go-process
forge.lthn.ai/core/go-rag => ../go-rag
forge.lthn.ai/core/go-scm => ../go-scm
forge.lthn.ai/core/mcp => ../mcp
forge.lthn.ai/lthn/lem => ../../lthn/LEM
)

180
go.sum
View file

@ -1,17 +1,67 @@
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0/go.mod h1:ZglEEDj+qkxYUb+SQIeqGtFxQrbaMYqIOgahNKb7uxs=
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
dappco.re/go/core/i18n v0.2.0 h1:NHzk6RCU93/qVRA3f2jvMr9P1R6FYheR/sHL+TnvKbI=
dappco.re/go/core/i18n v0.2.0/go.mod h1:9eSVJXr3OpIGWQvDynfhqcp27xnLMwlYLgsByU+p7ok=
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg=
forge.lthn.ai/core/api v0.1.0/go.mod h1:c86Lk9AmaS0xbiRCEG/+du8s9KyYNHnp8RED35gR/Fo=
forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg=
forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs=
forge.lthn.ai/core/config v0.1.0/go.mod h1:8HYA29drAWlX+bO4VI1JhmKUgGU66E2Xge8D3tKd3Dg=
forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0=
forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ=
forge.lthn.ai/core/go-crypt v0.1.6/go.mod h1:4VZAGqxlbadhSB66sJkdj54/HSJ+bSxVgwWK5kMMYDo=
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8=
forge.lthn.ai/core/go-inference v0.1.7 h1:9Dy6v03jX5ZRH3n5iTzlYyGtucuBIgSe+S7GWvBzx9Q=
forge.lthn.ai/core/go-inference v0.1.7/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
forge.lthn.ai/core/go-io v0.1.7 h1:Tdb6sqh+zz1lsGJaNX9RFWM6MJ/RhSAyxfulLXrJsbk=
forge.lthn.ai/core/go-io v0.1.7/go.mod h1:8lRLFk4Dnp5cR/Cyzh9WclD5566TbpdRgwcH7UZLWn4=
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
forge.lthn.ai/core/go-rag v0.1.11 h1:KXTOtnOdrx8YKmvnj0EOi2EI/+cKjE8w2PpJCQIrSd8=
forge.lthn.ai/core/go-rag v0.1.11/go.mod h1:vIlOKVD1SdqqjkJ2XQyXPuKPtiajz/STPLCaDpqOzk8=
forge.lthn.ai/core/go-scm v0.2.0 h1:TvDyCzw0HWzXjmqe6uPc46nPaRzc7MPGswmwZt0CmXo=
forge.lthn.ai/core/go-scm v0.2.0/go.mod h1:Q/PV2FbqDlWnAOsXAd1pgSiHOlRCPW4HcPmOt8Z9H+E=
forge.lthn.ai/core/go-ws v0.1.0/go.mod h1:wBQLXDUod6FqESh1CM4OnAjyP3cmWg8Vd5M43RIdTwA=
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs=
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
@ -24,90 +74,198 @@ github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0=
github.com/chewxy/math32 v1.11.0/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/d4l3k/go-bfloat16 v0.0.0-20211005043715-690c3bdd05f1/go.mod h1:uw2gLcxEuYUlAd/EXyjc/v55nd3+47YAgWbSXVxPrNI=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728/go.mod h1:1fEHWurg7pvf5SG6XNE5Q8UZmOwex51Mkx3SLhrW5B4=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M=
github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nlpodyssey/gopickle v0.3.0/go.mod h1:f070HJ/yR+eLi5WmM1OXJEGaTpuJEUiib19olXgYha0=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/ollama/ollama v0.18.1 h1:7K6anW64C2keASpToYfuOa00LuP8aCmofLKcT2c1mlY=
github.com/ollama/ollama v0.18.1/go.mod h1:tCX4IMV8DHjl3zY0THxuEkpWDZSOchJpzTuLACpMwFw=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pdevine/tensor v0.0.0-20240510204454-f88f4562727c/go.mod h1:PSojXDXF7TbgQiD6kkd98IHOS0QqTyUEaWRiS8+BLu8=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/qdrant/go-client v1.17.1 h1:7QmPwDddrHL3hC4NfycwtQlraVKRLcRi++BX6TTm+3g=
github.com/qdrant/go-client v1.17.1/go.mod h1:n1h6GhkdAzcohoXt/5Z19I2yxbCkMA6Jejob3S6NZT8=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/tkrajina/typescriptify-golang-structs v0.2.0/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls=
github.com/tree-sitter/go-tree-sitter v0.25.0/go.mod h1:r77ig7BikoZhHrrsjAnv8RqGti5rtSyvDHPzgTPsUuU=
github.com/tree-sitter/tree-sitter-cpp v0.23.4/go.mod h1:doqNW64BriC7WBCQ1klf0KmJpdEvfxyXtoEybnBo6v8=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xtgo/set v1.0.0/go.mod h1:d3NHzGzSa0NmB2NhFyECA+QdRp29oEn2xbT+TpeFoM8=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
@ -116,8 +274,12 @@ golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
@ -129,3 +291,9 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorgonia.org/vecf32 v0.9.0/go.mod h1:NCc+5D2oxddRL11hd+pCB1PEyXWOyiQxfZ/1wwhOXCA=
gorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=

57
specs/ai.md Normal file
View file

@ -0,0 +1,57 @@
# ai
**Import:** `dappco.re/go/core/ai/ai`
**Files:** 3
Package ai provides the unified AI package 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.
Sub-packages composed:
- pkg/rag: Qdrant vector database + Ollama embeddings
- pkg/agentic: Task queue client and context building
## Types
### Event
- **File:** metrics.go
- **Purpose:** Event represents a recorded AI/security metric event.
- **Fields:**
- `Type string` — Type.
- `Timestamp time.Time` — Timestamp.
- `AgentID string` — Agent ID.
- `Repo string` — Repo.
- `Duration time.Duration` — Duration.
- `Data map[string]any` — Data.
### TaskInfo
- **File:** rag.go
- **Purpose:** TaskInfo carries the minimal task data needed for RAG queries, avoiding a direct dependency on pkg/agentic (which imports pkg/ai).
- **Fields:**
- `Title string` — Title.
- `Description string` — Description.
## Functions
### QueryRAGForTask
- **File:** rag.go
- **Signature:** `func QueryRAGForTask(task TaskInfo) (string, error)`
- **Purpose:** QueryRAGForTask queries Qdrant for documentation relevant to a task.
### ReadEvents
- **File:** metrics.go
- **Signature:** `func ReadEvents(since time.Time) ([]Event, error)`
- **Purpose:** ReadEvents reads events from JSONL files within the given time range.
### Record
- **File:** metrics.go
- **Signature:** `func Record(event Event) (err error)`
- **Purpose:** Record appends an event to the daily JSONL file at ~/.core/ai/metrics/YYYY-MM-DD.jsonl.
### Summary
- **File:** metrics.go
- **Signature:** `func Summary(events []Event) map[string]any`
- **Purpose:** Summary aggregates events into counts by type, repo, and agent.

55
specs/ai/RFC.md Normal file
View file

@ -0,0 +1,55 @@
# ai
**Import:** `dappco.re/go/core/ai/ai`
**Files:** 3
Package ai provides the unified AI package 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.
## Types
### Event
- **Kind:** `struct`
- **File:** `metrics.go`
- **Purpose:** Event represents a recorded AI/security metric event.
- **Fields:**
- `Type string`
- `Timestamp time.Time`
- `AgentID string`
- `Repo string`
- `Duration time.Duration`
- `Data map[string]any`
### TaskInfo
- **Kind:** `struct`
- **File:** `rag.go`
- **Purpose:** TaskInfo carries the minimal task data needed for RAG queries, avoiding a direct dependency on pkg/agentic (which imports pkg/ai).
- **Fields:**
- `Title string`
- `Description string`
## Functions
### QueryRAGForTask
- **File:** `rag.go`
- **Signature:** `func QueryRAGForTask(task TaskInfo) (string, error)`
- **Purpose:** QueryRAGForTask concatenates the task title and description, truncates the query to 500 runes, queries the `hostuk-docs` collection with `Limit: 3` and `Threshold: 0.5`, and returns `rag.FormatResultsContext(results)`.
### ReadEvents
- **File:** `metrics.go`
- **Signature:** `func ReadEvents(since time.Time) ([]Event, error)`
- **Purpose:** ReadEvents reads daily JSONL metric files from the day containing `since` through `time.Now()`, returning only events whose `Timestamp` is not before `since`.
### Record
- **File:** `metrics.go`
- **Signature:** `func Record(event Event) (err error)`
- **Purpose:** Record fills a zero timestamp with `time.Now()`, ensures the metrics directory exists, and appends one JSON line to `~/.core/ai/metrics/YYYY-MM-DD.jsonl`.
### Summary
- **File:** `metrics.go`
- **Signature:** `func Summary(events []Event) map[string]any`
- **Purpose:** Summary returns aggregate counts under the keys `total`, `by_type`, `by_repo`, and `by_agent`; grouped entries are sorted by count descending and then key ascending.

20
specs/cmd/embed-bench.md Normal file
View file

@ -0,0 +1,20 @@
# main
**Import:** `dappco.re/go/core/ai/cmd/embed-bench`
**Files:** 1
embed-bench compares embedding models for OpenBrain by testing how well
they separate semantically related vs unrelated agent memory pairs.
Usage:
go run ./cmd/embed-bench
go run ./cmd/embed-bench -ollama http://localhost:11434
## Types
None.
## Functions
None.

15
specs/cmd/lab.md Normal file
View file

@ -0,0 +1,15 @@
# lab
**Import:** `dappco.re/go/core/ai/cmd/lab`
**Files:** 1
## Types
None.
## Functions
### AddLabCommands
- **File:** cmd_lab.go
- **Signature:** `func AddLabCommands(root *cli.Command)`
- **Purpose:** AddLabCommands registers the 'lab' command and subcommands.

17
specs/cmd/metrics.md Normal file
View file

@ -0,0 +1,17 @@
# metrics
**Import:** `dappco.re/go/core/ai/cmd/metrics`
**Files:** 1
Package metrics implements the metrics viewing command.
## Types
None.
## Functions
### AddMetricsCommand
- **File:** cmd.go
- **Signature:** `func AddMetricsCommand(parent *cli.Command)`
- **Purpose:** AddMetricsCommand adds the 'metrics' command to the parent.

21
specs/cmd/rag.md Normal file
View file

@ -0,0 +1,21 @@
# rag
**Import:** `dappco.re/go/core/ai/cmd/rag`
**Files:** 1
Package rag re-exports go-rag's CLI commands for use in the core CLI.
## Variables
### AddRAGSubcommands
- **File:** cmd.go
- **Signature:** `var AddRAGSubcommands = ragcmd.AddRAGSubcommands`
- **Purpose:** AddRAGSubcommands registers RAG commands as subcommands of parent.
## Types
None.
## Functions
None.

111
specs/cmd/security.md Normal file
View file

@ -0,0 +1,111 @@
# security
**Import:** `dappco.re/go/core/ai/cmd/security`
**Files:** 8
## Types
### AlertOutput
- **File:** cmd_alerts.go
- **Purpose:** AlertOutput represents a unified alert for output.
- **Fields:**
- `Repo string` — Repo.
- `Severity string` — Severity.
- `ID string` — ID.
- `Package string` — Package.
- `Version string` — Version.
- `Location string` — Location.
- `Type string` — Type.
- `Message string` — Message.
### AlertSummary
- **File:** cmd_security.go
- **Purpose:** AlertSummary holds aggregated alert counts.
- **Fields:**
- `Critical int` — Critical.
- `High int` — High.
- `Medium int` — Medium.
- `Low int` — Low.
- `Unknown int` — Unknown.
- `Total int` — Total.
- **Methods:**
- `func (s *AlertSummary) Add(severity string)` (cmd_security.go) — Add increments summary counters for the provided severity.
- `func (s *AlertSummary) PlainString() string` (cmd_security.go) — PlainString renders an unstyled summary suitable for logs and issue bodies.
- `func (s *AlertSummary) String() string` (cmd_security.go) — String renders a human-readable summary of alert counts.
### CodeScanningAlert
- **File:** cmd_security.go
- **Purpose:** CodeScanningAlert represents a code scanning alert.
- **Fields:**
- `Number int` — Number.
- `State string` — State.
- `DismissedReason string` — Dismissed Reason.
- `Rule struct{...}` — Rule.
- `Tool struct{...}` — Tool.
- `MostRecentInstance struct{...}` — Most Recent Instance.
### DepAlert
- **File:** cmd_deps.go
- **Purpose:** DepAlert represents a dependency vulnerability for output.
- **Fields:**
- `Repo string` — Repo.
- `Severity string` — Severity.
- `CVE string` — CVE.
- `Package string` — Package.
- `Ecosystem string` — Ecosystem.
- `Vulnerable string` — Vulnerable.
- `PatchedVersion string` — Patched Version.
- `Manifest string` — Manifest.
- `Summary string` — Summary.
### DependabotAlert
- **File:** cmd_security.go
- **Purpose:** DependabotAlert represents a Dependabot vulnerability alert.
- **Fields:**
- `Number int` — Number.
- `State string` — State.
- `Advisory struct{...}` — Advisory.
- `Dependency struct{...}` — Dependency.
- `SecurityVulnerability struct{...}` — Security Vulnerability.
### ScanAlert
- **File:** cmd_scan.go
- **Purpose:** ScanAlert represents a code scanning alert for output.
- **Fields:**
- `Repo string` — Repo.
- `Severity string` — Severity.
- `RuleID string` — Rule ID.
- `Tool string` — Tool.
- `Path string` — Path.
- `Line int` — Line.
- `Description string` — Description.
- `Message string` — Message.
### SecretAlert
- **File:** cmd_secrets.go
- **Purpose:** SecretAlert represents a secret scanning alert for output.
- **Fields:**
- `Repo string` — Repo.
- `Number int` — Number.
- `SecretType string` — Secret Type.
- `State string` — State.
- `Resolution string` — Resolution.
- `PushProtection bool` — Push Protection.
### SecretScanningAlert
- **File:** cmd_security.go
- **Purpose:** SecretScanningAlert represents a secret scanning alert.
- **Fields:**
- `Number int` — Number.
- `State string` — State.
- `SecretType string` — Secret Type.
- `Secret string` — Secret.
- `PushProtection bool` — Push Protection.
- `Resolution string` — Resolution.
## Functions
### AddSecurityCommands
- **File:** cmd_security.go
- **Signature:** `func AddSecurityCommands(root *cli.Command)`
- **Purpose:** AddSecurityCommands adds the 'security' command to the root.

18
specs/embed-bench/RFC.md Normal file
View file

@ -0,0 +1,18 @@
# main
**Import:** `dappco.re/go/core/ai/cmd/embed-bench`
**Files:** 1
Package main benchmarks Ollama embedding models against grouped memory
and query examples.
It compares how well candidate embedding models separate semantically
related agent-memory pairs from unrelated ones.
## Types
None.
## Functions
None.

17
specs/lab/RFC.md Normal file
View file

@ -0,0 +1,17 @@
# lab
**Import:** `dappco.re/go/core/ai/cmd/lab`
**Files:** 1
`cmd_lab.go` is guarded by `//go:build ignore`.
## Types
None.
## Functions
### AddLabCommands
- **File:** `cmd_lab.go`
- **Signature:** `func AddLabCommands(root *cli.Command)`
- **Purpose:** AddLabCommands adds the `lab` root command and registers its `serve` subcommand on `root`.

17
specs/metrics/RFC.md Normal file
View file

@ -0,0 +1,17 @@
# metrics
**Import:** `dappco.re/go/core/ai/cmd/metrics`
**Files:** 1
Package metrics implements the metrics viewing command.
## Types
None.
## Functions
### AddMetricsCommand
- **File:** `cmd.go`
- **Signature:** `func AddMetricsCommand(parent *cli.Command)`
- **Purpose:** AddMetricsCommand initializes the `metrics` command flags and adds the command to `parent`.

21
specs/rag/RFC.md Normal file
View file

@ -0,0 +1,21 @@
# rag
**Import:** `dappco.re/go/core/ai/cmd/rag`
**Files:** 1
Package rag re-exports go-rag's CLI commands for use in the core CLI.
## Variables
### AddRAGSubcommands
- **File:** `cmd.go`
- **Declaration:** `var AddRAGSubcommands = ragcmd.AddRAGSubcommands`
- **Purpose:** AddRAGSubcommands re-exports `ragcmd.AddRAGSubcommands`.
## Types
None.
## Functions
None.

122
specs/security/RFC.md Normal file
View file

@ -0,0 +1,122 @@
# security
**Import:** `dappco.re/go/core/ai/cmd/security`
**Files:** 8
The package exports the `security` command registration entry point and
the alert/output structs used by the security subcommands.
## Types
### AlertOutput
- **Kind:** `struct`
- **File:** `cmd_alerts.go`
- **Purpose:** AlertOutput represents a unified alert for output.
- **Fields:**
- `Repo string`
- `Severity string`
- `ID string`
- `Package string`
- `Version string`
- `Location string`
- `Type string`
- `Message string`
### AlertSummary
- **Kind:** `struct`
- **File:** `cmd_security.go`
- **Purpose:** AlertSummary holds aggregated alert counts.
- **Fields:**
- `Critical int`
- `High int`
- `Medium int`
- `Low int`
- `Unknown int`
- `Total int`
- **Methods:**
- `func (s *AlertSummary) Add(severity string)` - increments the summary counters for the provided severity.
- `func (s *AlertSummary) String() string` - renders a styled summary string.
- `func (s *AlertSummary) PlainString() string` - renders an unstyled summary string for logs and issue bodies.
### CodeScanningAlert
- **Kind:** `struct`
- **File:** `cmd_security.go`
- **Purpose:** CodeScanningAlert represents a code scanning alert.
- **Fields:**
- `Number int`
- `State string`
- `DismissedReason string`
- `Rule struct{ ID string; Severity string; Description string; Tags []string }`
- `Tool struct{ Name string; Version string }`
- `MostRecentInstance struct{ Location struct{ Path string; StartLine int; EndLine int }; Message struct{ Text string } }`
### DepAlert
- **Kind:** `struct`
- **File:** `cmd_deps.go`
- **Purpose:** DepAlert represents a dependency vulnerability for output.
- **Fields:**
- `Repo string`
- `Severity string`
- `CVE string`
- `Package string`
- `Ecosystem string`
- `Vulnerable string`
- `PatchedVersion string`
- `Manifest string`
- `Summary string`
### DependabotAlert
- **Kind:** `struct`
- **File:** `cmd_security.go`
- **Purpose:** DependabotAlert represents a Dependabot vulnerability alert.
- **Fields:**
- `Number int`
- `State string`
- `Advisory struct{ Severity string; CVEID string; Summary string; Description string }`
- `Dependency struct{ Package struct{ Name string; Ecosystem string }; ManifestPath string }`
- `SecurityVulnerability struct{ Package struct{ Name string; Ecosystem string }; FirstPatchedVersion struct{ Identifier string }; VulnerableVersionRange string }`
### ScanAlert
- **Kind:** `struct`
- **File:** `cmd_scan.go`
- **Purpose:** ScanAlert represents a code scanning alert for output.
- **Fields:**
- `Repo string`
- `Severity string`
- `RuleID string`
- `Tool string`
- `Path string`
- `Line int`
- `Description string`
- `Message string`
### SecretAlert
- **Kind:** `struct`
- **File:** `cmd_secrets.go`
- **Purpose:** SecretAlert represents a secret scanning alert for output.
- **Fields:**
- `Repo string`
- `Number int`
- `SecretType string`
- `State string`
- `Resolution string`
- `PushProtection bool`
### SecretScanningAlert
- **Kind:** `struct`
- **File:** `cmd_security.go`
- **Purpose:** SecretScanningAlert represents a secret scanning alert.
- **Fields:**
- `Number int`
- `State string`
- `SecretType string`
- `Secret string`
- `PushProtection bool`
- `Resolution string`
## Functions
### AddSecurityCommands
- **File:** `cmd_security.go`
- **Signature:** `func AddSecurityCommands(root *cli.Command)`
- **Purpose:** AddSecurityCommands creates the `security` command and registers the `alerts`, `deps`, `scan`, `secrets`, and `jobs` subcommands on `root`.