a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
368 lines
9.6 KiB
Go
368 lines
9.6 KiB
Go
// Command baselines runs the per-baseline retrieval comparison:
|
|
// dispatch the same query set through each registered adapter,
|
|
// compare the per-query hit lists against a shared ground truth,
|
|
// and emit a per-adapter NDCG@10 + latency table.
|
|
//
|
|
// Smoke mode (`--smoke`) skips actual runs and reports which adapters
|
|
// are available on the local box — useful from CI to verify wiring
|
|
// without paying for the heavy Python baselines.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func main() {
|
|
repo := flag.String("repo", ".", "indexed corpus path")
|
|
queriesPath := flag.String("queries", "bench/baselines/queries.json", "JSON query set")
|
|
truthPath := flag.String("groundtruth", "bench/baselines/groundtruth.json", "JSON per-query expected file paths")
|
|
against := flag.String("against", "ripgrep,probe,colgrep,grepai,coderankembed,semble", "comma-separated adapter names to run (default: all)")
|
|
topK := flag.Int("top-k", 10, "top-K candidates per query (NDCG@10 uses K=10)")
|
|
out := flag.String("out", "", "output table path (default stdout)")
|
|
jsonOut := flag.String("json", "", "optional JSON metrics output")
|
|
format := flag.String("format", "markdown", "markdown | json")
|
|
smoke := flag.Bool("smoke", false, "skip runs; only probe availability of each requested adapter")
|
|
timeout := flag.Duration("timeout", 5*time.Minute, "per-adapter wall-clock cap")
|
|
flag.Parse()
|
|
|
|
requested, err := resolveAdapters(*against)
|
|
if err != nil {
|
|
die("%v", err)
|
|
}
|
|
|
|
if *smoke {
|
|
smokeReport(requested, *format, *out)
|
|
return
|
|
}
|
|
|
|
absRepo, err := filepath.Abs(*repo)
|
|
if err != nil {
|
|
die("repo path: %v", err)
|
|
}
|
|
if _, err := os.Stat(absRepo); err != nil {
|
|
die("repo path: %v", err)
|
|
}
|
|
|
|
queries, err := loadQueries(*queriesPath)
|
|
if err != nil {
|
|
die("queries: %v", err)
|
|
}
|
|
truth, err := loadGroundTruth(*truthPath)
|
|
if err != nil {
|
|
die("groundtruth: %v", err)
|
|
}
|
|
|
|
rows := make([]adapterRow, 0, len(requested))
|
|
for _, a := range requested {
|
|
fmt.Fprintf(os.Stderr, "[baselines] %s ... ", a.Name())
|
|
avail, why := a.Available()
|
|
if !avail {
|
|
fmt.Fprintf(os.Stderr, "skipped (%s)\n", why)
|
|
rows = append(rows, adapterRow{
|
|
Adapter: a.Name(),
|
|
Skipped: why,
|
|
})
|
|
continue
|
|
}
|
|
t0 := time.Now()
|
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
|
results := a.Run(ctx, queries, absRepo, *topK)
|
|
cancel()
|
|
elapsed := time.Since(t0)
|
|
|
|
row := adapterRow{
|
|
Adapter: a.Name(),
|
|
PerQuery: results,
|
|
TotalDuration: elapsed.String(),
|
|
}
|
|
row.MedianLatencyMs = medianLatency(results)
|
|
row.NDCGAt10 = ndcgAt10(results, truth, *topK)
|
|
fmt.Fprintf(os.Stderr, "NDCG@10 %.3f · median %.1fms · total %.1fs\n",
|
|
row.NDCGAt10, row.MedianLatencyMs, elapsed.Seconds())
|
|
rows = append(rows, row)
|
|
}
|
|
|
|
var primary []byte
|
|
switch strings.ToLower(*format) {
|
|
case "markdown", "md":
|
|
primary = []byte(renderMarkdown(rows))
|
|
case "json":
|
|
primary = mustMarshalJSON(rows)
|
|
default:
|
|
die("unknown --format %q", *format)
|
|
}
|
|
if err := writeOutput(*out, primary); err != nil {
|
|
die("write output: %v", err)
|
|
}
|
|
if *jsonOut != "" {
|
|
if err := writeOutput(*jsonOut, mustMarshalJSON(rows)); err != nil {
|
|
die("write json: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// adapterRow is the per-baseline outcome with the columns the
|
|
// published table cares about.
|
|
type adapterRow struct {
|
|
Adapter string `json:"adapter"`
|
|
NDCGAt10 float64 `json:"ndcg_at_10"`
|
|
MedianLatencyMs float64 `json:"median_latency_ms"`
|
|
TotalDuration string `json:"total_duration"`
|
|
PerQuery []queryResult `json:"per_query,omitempty"`
|
|
Skipped string `json:"skipped,omitempty"`
|
|
}
|
|
|
|
// resolveAdapters expands the --against flag into the requested
|
|
// adapter set, preserving the caller's order. Unknown names error
|
|
// out so a typo doesn't silently drop a baseline.
|
|
func resolveAdapters(spec string) ([]adapter, error) {
|
|
if strings.TrimSpace(spec) == "" {
|
|
return allAdapters(), nil
|
|
}
|
|
var out []adapter
|
|
for tok := range strings.SplitSeq(spec, ",") {
|
|
name := strings.TrimSpace(tok)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
a := adapterByName(name)
|
|
if a == nil {
|
|
return nil, fmt.Errorf("unknown adapter %q (known: %s)",
|
|
name, strings.Join(adapterNames(), ", "))
|
|
}
|
|
out = append(out, a)
|
|
}
|
|
if len(out) == 0 {
|
|
return allAdapters(), nil
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// smokeReport prints one row per requested adapter showing
|
|
// available / not-available with the install hint. Output respects
|
|
// --format; default markdown.
|
|
func smokeReport(adapters []adapter, format, out string) {
|
|
if strings.ToLower(format) == "json" {
|
|
rows := make([]map[string]any, 0, len(adapters))
|
|
for _, a := range adapters {
|
|
avail, why := a.Available()
|
|
row := map[string]any{
|
|
"adapter": a.Name(),
|
|
"available": avail,
|
|
}
|
|
if why != "" {
|
|
row["why"] = why
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
raw, _ := json.MarshalIndent(rows, "", " ")
|
|
_ = writeOutput(out, append(raw, '\n'))
|
|
return
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintln(&b, "# Baseline-adapter smoke check")
|
|
fmt.Fprintln(&b)
|
|
fmt.Fprintln(&b, "| adapter | available | reason |")
|
|
fmt.Fprintln(&b, "|---------|:---------:|--------|")
|
|
for _, a := range adapters {
|
|
avail, why := a.Available()
|
|
mark := "✗"
|
|
if avail {
|
|
mark = "✓"
|
|
}
|
|
if why == "" {
|
|
why = "—"
|
|
}
|
|
fmt.Fprintf(&b, "| %s | %s | %s |\n", a.Name(), mark, why)
|
|
}
|
|
_ = writeOutput(out, []byte(b.String()))
|
|
}
|
|
|
|
// ndcgAt10 computes NDCG@10 across the result set. Per query: gain
|
|
// is 1 for ground-truth hits, 0 otherwise; DCG = sum gain_i /
|
|
// log2(i+2); IDCG = DCG of the perfect ordering. Mean across
|
|
// queries; 0 when the result set is empty.
|
|
func ndcgAt10(results []queryResult, truth map[string][]string, k int) float64 {
|
|
if len(results) == 0 {
|
|
return 0
|
|
}
|
|
var sum float64
|
|
counted := 0
|
|
for _, r := range results {
|
|
if r.Error != "" {
|
|
continue
|
|
}
|
|
expected := truth[r.Query]
|
|
if len(expected) == 0 {
|
|
continue
|
|
}
|
|
expSet := map[string]bool{}
|
|
for _, e := range expected {
|
|
expSet[e] = true
|
|
}
|
|
// DCG@k
|
|
var dcg float64
|
|
hits := r.Hits
|
|
if len(hits) > k {
|
|
hits = hits[:k]
|
|
}
|
|
for i, h := range hits {
|
|
if expSet[h] {
|
|
dcg += 1.0 / math.Log2(float64(i+2))
|
|
}
|
|
}
|
|
// IDCG@k: best case is min(len(expected), k) hits at the top.
|
|
idealHits := min(len(expected), k)
|
|
var idcg float64
|
|
for i := range idealHits {
|
|
idcg += 1.0 / math.Log2(float64(i+2))
|
|
}
|
|
if idcg == 0 {
|
|
continue
|
|
}
|
|
sum += dcg / idcg
|
|
counted++
|
|
}
|
|
if counted == 0 {
|
|
return 0
|
|
}
|
|
return sum / float64(counted)
|
|
}
|
|
|
|
func medianLatency(results []queryResult) float64 {
|
|
vs := make([]float64, 0, len(results))
|
|
for _, r := range results {
|
|
if r.Error == "" {
|
|
vs = append(vs, r.LatencyMs)
|
|
}
|
|
}
|
|
if len(vs) == 0 {
|
|
return 0
|
|
}
|
|
sort.Float64s(vs)
|
|
return vs[len(vs)/2]
|
|
}
|
|
|
|
// --- rendering ------------------------------------------------------
|
|
|
|
func renderMarkdown(rows []adapterRow) string {
|
|
var b strings.Builder
|
|
fmt.Fprintln(&b, "# Retrieval-baselines NDCG@10 + speed")
|
|
fmt.Fprintln(&b)
|
|
fmt.Fprintln(&b, "_NDCG@10 against the ground-truth set + median per-query latency for each baseline. Adapters reporting `skipped` weren't available on this box; see the install hints in `bench/baselines/README.md`._")
|
|
fmt.Fprintln(&b)
|
|
fmt.Fprintln(&b, "| adapter | NDCG@10 | median latency | total | status |")
|
|
fmt.Fprintln(&b, "|---------|--------:|---------------:|------:|--------|")
|
|
for _, r := range rows {
|
|
if r.Skipped != "" {
|
|
fmt.Fprintf(&b, "| %s | — | — | — | skipped: %s |\n", r.Adapter, r.Skipped)
|
|
continue
|
|
}
|
|
fmt.Fprintf(&b, "| %s | %.3f | %s | %s | ✓ |\n",
|
|
r.Adapter, r.NDCGAt10, fmtMs(r.MedianLatencyMs), r.TotalDuration)
|
|
}
|
|
fmt.Fprintln(&b)
|
|
fmt.Fprintln(&b, summaryLine(rows))
|
|
return b.String()
|
|
}
|
|
|
|
func summaryLine(rows []adapterRow) string {
|
|
ran := 0
|
|
bestName := ""
|
|
var bestScore float64
|
|
for _, r := range rows {
|
|
if r.Skipped != "" {
|
|
continue
|
|
}
|
|
ran++
|
|
if r.NDCGAt10 > bestScore {
|
|
bestScore = r.NDCGAt10
|
|
bestName = r.Adapter
|
|
}
|
|
}
|
|
if ran == 0 {
|
|
return "_no adapters available — install at least one baseline (see bench/baselines/README.md)_"
|
|
}
|
|
return fmt.Sprintf("**Summary:** %d/%d adapter(s) ran. Best NDCG@10: %s @ %.3f.",
|
|
ran, len(rows), bestName, bestScore)
|
|
}
|
|
|
|
func fmtMs(v float64) string {
|
|
if v == 0 {
|
|
return "—"
|
|
}
|
|
if v < 1 {
|
|
return fmt.Sprintf("%.2fms", v)
|
|
}
|
|
if v < 1000 {
|
|
return fmt.Sprintf("%.1fms", v)
|
|
}
|
|
return fmt.Sprintf("%.2fs", v/1000.0)
|
|
}
|
|
|
|
// --- helpers --------------------------------------------------------
|
|
|
|
func die(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "baselines: "+format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func writeOutput(path string, body []byte) error {
|
|
if path == "" {
|
|
_, err := os.Stdout.Write(body)
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, body, 0o644)
|
|
}
|
|
|
|
func mustMarshalJSON(rows []adapterRow) []byte {
|
|
b, err := json.MarshalIndent(rows, "", " ")
|
|
if err != nil {
|
|
die("marshal json: %v", err)
|
|
}
|
|
return append(b, '\n')
|
|
}
|
|
|
|
func loadQueries(path string) ([]string, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var doc struct {
|
|
Queries []string `json:"queries"`
|
|
}
|
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
|
return nil, fmt.Errorf("parse %s: %w", path, err)
|
|
}
|
|
return doc.Queries, nil
|
|
}
|
|
|
|
func loadGroundTruth(path string) (map[string][]string, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var doc struct {
|
|
Queries map[string][]string `json:"queries"`
|
|
}
|
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
|
return nil, fmt.Errorf("parse %s: %w", path, err)
|
|
}
|
|
if doc.Queries == nil {
|
|
doc.Queries = map[string][]string{}
|
|
}
|
|
return doc.Queries, nil
|
|
}
|