feat: gadfly-reports — findings store + scoreboard daemon
Build & push image / build-and-push (push) Successful in 1m13s
CI / test (push) Successful in 10m39s

SQLite-backed HTTP store for Gadfly review findings, per-review run timings, and human/Claude grades, with a points-free per-model scoreboard. Pure fact store: it computes no points or rankings (the dashboard maps severity->points client-side and retunes without re-scoring). Findings are content-addressed by location so cross-model reports collapse for consensus; one grade per finding, latest wins. Pure-Go SQLite (CGO-free) + Docker image CI + tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 23:55:24 -04:00
parent 52dce5eb2f
commit ddcf42a3ce
16 changed files with 1269 additions and 27 deletions
+121
View File
@@ -0,0 +1,121 @@
package main
import (
"encoding/json"
"errors"
"log"
"net/http"
"strings"
)
// newServer wires the store to the HTTP API. If token is non-empty, every route
// except /healthz requires "Authorization: Bearer <token>".
//
// Routes:
//
// GET /healthz liveness
// POST /runs upsert one run (model review of a PR; timing/tokens)
// POST /reports record a batch of findings + this model's reports
// POST /findings/{id}/grade record a triage grade (is_real, severity, …)
// GET /export flat report×finding×grade rows (the dashboard feed)
// GET /scoreboard points-free per-model rollup
func newServer(store *Store, token string) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
mux.HandleFunc("POST /runs", func(w http.ResponseWriter, r *http.Request) {
var run Run
if !decode(w, r, &run) {
return
}
if err := store.AddRun(run); err != nil {
writeErr(w, http.StatusBadRequest, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{"run_id": run.RunID})
})
mux.HandleFunc("POST /reports", func(w http.ResponseWriter, r *http.Request) {
var reps []ReportIn
if !decode(w, r, &reps) {
return
}
ids, err := store.AddReports(reps)
if err != nil {
writeErr(w, http.StatusBadRequest, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"finding_ids": ids})
})
mux.HandleFunc("POST /findings/{id}/grade", func(w http.ResponseWriter, r *http.Request) {
var g Grade
if !decode(w, r, &g) {
return
}
g.FindingID = r.PathValue("id")
if err := store.AddGrade(g); err != nil {
writeErr(w, http.StatusBadRequest, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{"finding_id": g.FindingID})
})
mux.HandleFunc("GET /export", func(w http.ResponseWriter, _ *http.Request) {
rows, err := store.Export()
if err != nil {
writeErr(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, rows)
})
mux.HandleFunc("GET /scoreboard", func(w http.ResponseWriter, _ *http.Request) {
stats, err := store.Scoreboard()
if err != nil {
writeErr(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, stats)
})
return auth(token, mux)
}
// auth gates everything but /healthz behind a bearer token, when one is set.
func auth(token string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if token != "" && r.URL.Path != "/healthz" {
got := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if strings.TrimSpace(got) != token {
writeErr(w, http.StatusUnauthorized, errors.New("missing or invalid bearer token"))
return
}
}
next.ServeHTTP(w, r)
})
}
// decode reads a JSON body into v, writing a 400 and returning false on failure.
func decode(w http.ResponseWriter, r *http.Request, v any) bool {
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
writeErr(w, http.StatusBadRequest, errors.New("invalid JSON body: "+err.Error()))
return false
}
return true
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
if err := json.NewEncoder(w).Encode(v); err != nil {
log.Printf("gadfly-reports: write response: %v", err)
}
}
func writeErr(w http.ResponseWriter, code int, err error) {
writeJSON(w, code, map[string]string{"error": err.Error()})
}