Files
gadfly/cmd/gadfly/consolidate.go
T
steve 53971603d3
Build & push image / build-and-push (push) Successful in 5s
feat: structured findings contract (machine-readable gadfly-findings block) (#16)
Co-authored-by: Steve Dudenhoeffer <[email protected]>
Co-committed-by: Steve Dudenhoeffer <[email protected]>
2026-06-28 22:23:02 +00:00

142 lines
4.3 KiB
Go

package main
import (
"fmt"
"strings"
)
// verdict is the severity a specialist (or the whole review) lands on.
type verdict int
const (
verdictUnknown verdict = iota // couldn't parse; treated below Minor for display, not blocking
verdictClean // "No material issues found"
verdictMinor // "Minor issues"
verdictBlocking // "Blocking issues found"
)
func (v verdict) label() string {
switch v {
case verdictClean:
return "No material issues found"
case verdictMinor:
return "Minor issues"
case verdictBlocking:
return "Blocking issues found"
default:
return "Reviewed"
}
}
// severity maps a verdict to a canonical severity word (the same vocabulary as
// the structured findings: critical/high/medium/small/trivial). Used as the
// raw_severity for heuristic-scraped findings, which carry no per-finding
// severity of their own — so the telemetry store sees one consistent vocabulary
// instead of a mix of canonical words and full verdict phrases.
func (v verdict) severity() string {
switch v {
case verdictBlocking:
return "high"
case verdictMinor:
return "small"
default:
return "trivial"
}
}
// parseVerdict extracts a specialist's self-reported verdict from its output.
// The base prompt tells each lens to lead with one of the three phrases.
func parseVerdict(out string) verdict {
l := strings.ToLower(out)
// Match the EARLIEST-appearing verdict phrase (the lead verdict), and match
// leniently — models write "Blocking issues found", "**Blocking issues**",
// "blocking issue", etc. (A strict "blocking issues found" check let a
// gpt-oss section that said "**Blocking issues**" fall through to unknown,
// so the overall header wrongly read "No material issues found".)
best, bestIdx := verdictUnknown, -1
for _, c := range []struct {
phrase string
v verdict
}{
{"blocking issue", verdictBlocking},
{"minor issue", verdictMinor},
{"no material issue", verdictClean},
} {
if i := strings.Index(l, c.phrase); i >= 0 && (bestIdx == -1 || i < bestIdx) {
best, bestIdx = c.v, i
}
}
return best
}
// specialistResult pairs a specialist with its rendered review and verdict.
type specialistResult struct {
spec Specialist
out string
verdict verdict
errored bool // the review pass failed (timeout/model error) — not a clean result
}
// worstVerdict returns the most severe verdict across results. The optional
// "improvements" lens never escalates the overall verdict (it's advisory).
func worstVerdict(results []specialistResult) verdict {
worst := verdictClean
for _, r := range results {
if r.spec.Name == "improvements" {
continue
}
if r.verdict > worst {
worst = r.verdict
}
}
return worst
}
// renderConsolidated assembles the single comment body: an overall verdict line
// followed by one verbatim section per specialist. run.sh wraps this with the
// "🪰 Gadfly review — <model>" header and the advisory footer.
func renderConsolidated(results []specialistResult) string {
errored := 0
for _, r := range results {
if r.errored {
errored++
}
}
headline := "Verdict: " + worstVerdict(results).label()
if len(results) > 0 && errored == len(results) {
// Every lens errored — do NOT report this as "clean".
headline = "Review incomplete — all lenses errored"
} else if errored > 0 {
headline += fmt.Sprintf(" · ⚠️ %d/%d lens(es) errored", errored, len(results))
}
var b strings.Builder
fmt.Fprintf(&b, "**%s** — %d reviewers: %s\n",
headline, len(results), strings.Join(specialistNames(results), ", "))
for _, r := range results {
// Strip the machine-readable ```gadfly-findings block — it's for tooling
// (telemetry/consolidation), not for human readers of the comment.
body := strings.TrimSpace(stripFindingsBlock(r.out))
if body == "" {
body = "_(no output)_"
}
summary := r.verdict.label()
if r.errored {
summary = "⚠️ could not complete"
}
fmt.Fprintf(&b, "\n<details><summary><b>%s</b> — %s</summary>\n\n%s\n\n</details>\n",
r.spec.Title, summary, body)
}
return strings.TrimRight(b.String(), "\n")
}
func specialistNames(results []specialistResult) []string {
names := make([]string, 0, len(results))
for _, r := range results {
names = append(names, r.spec.Name)
}
return names
}