b409dff4ed
Build & push image / build-and-push (push) Successful in 8s
A section that led with '**Blocking issues**' (no 'found') fell through to unknown, so the consolidated header wrongly read 'No material issues found' (seen live on gpt-oss). Now matches 'blocking issue'/'minor issue'/'no material issue' and picks the earliest-appearing phrase (the lead verdict). + tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
3.6 KiB
Go
124 lines
3.6 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"
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
body := strings.TrimSpace(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
|
|
}
|