Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

296 lines
11 KiB
Go

package review
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
)
// CritiqueVerdict is the second-pass adjudication of a single prior finding:
// whether the critique keeps it, drops it as a false positive / low-value, or
// is uncertain. The default — applied whenever the model is silent, unavailable,
// or ambiguous — is CritiqueKeep, so a critique pass can only ever remove a
// finding on an explicit, reasoned drop.
type CritiqueVerdict string
const (
// CritiqueKeep retains the finding. The conservative default.
CritiqueKeep CritiqueVerdict = "keep"
// CritiqueDrop removes the finding as a false positive or low-value noise.
CritiqueDrop CritiqueVerdict = "drop"
// CritiqueUncertain flags a finding the critique could not confidently
// adjudicate. Like keep it never removes the finding — it only annotates
// the doubt so a human reviewer can look closer.
CritiqueUncertain CritiqueVerdict = "uncertain"
)
// CritiquedFinding pairs a prior finding with the second pass's verdict over it
// and the reason the model gave. The finding itself is carried verbatim so the
// caller can re-emit the kept set or render the dropped set with its reasons.
type CritiquedFinding struct {
Finding Finding `json:"finding"`
Verdict CritiqueVerdict `json:"critique_verdict"`
Reason string `json:"critique_reason,omitempty"`
}
// CritiqueResult is the outcome of a self-critique pass over a prior review's
// findings: the kept findings (the filtered set the caller should keep acting
// on), the annotated drops (each with the reason it was removed), the count of
// uncertain-but-kept findings, and the revised worst-of verdict recomputed over
// the kept set. Summary is a one-line headline.
type CritiqueResult struct {
Kept []Finding `json:"kept"`
Dropped []CritiquedFinding `json:"dropped"`
Annotated []CritiquedFinding `json:"annotated"`
Uncertain int `json:"uncertain"`
Verdict Verdict `json:"verdict"`
Summary string `json:"summary"`
// LLMUsed reports whether the critique LLM pass actually ran (a configured,
// non-nil gen that produced a parseable response). False means the result is
// the no-op pass-through: every finding kept, nothing dropped.
LLMUsed bool `json:"llm_used"`
}
// critiqueVerdictRow is one finding's adjudication as the model is asked to
// return it. The Index ties the verdict back to the input finding's position so
// the model never has to echo the whole finding body.
type critiqueVerdictRow struct {
Index int `json:"index"`
Verdict string `json:"verdict"`
Reason string `json:"reason"`
}
// Critique runs a second, adversarial LLM pass over a prior review's findings.
// It asks the model — grounded in the changeset diff and any per-finding source
// context already on each Finding — to decide which findings are false positives
// or low-value. Each finding is annotated with a keep / drop / uncertain verdict
// plus a reason; only explicit drops are removed. The pass is TOTAL: a nil /
// disabled gen, a failing model, or an unparseable response is a no-op
// pass-through — every finding is kept, no error is returned, and LLMUsed is
// false. maxTokens caps the critique request; <= 0 falls back to a sane default.
func Critique(ctx context.Context, gen LLMGen, findings []Finding, diff string, maxTokens int) CritiqueResult {
// Default to the conservative pass-through so the no-op / unavailable path is
// indistinguishable from "the model kept everything".
res := CritiqueResult{
Kept: append([]Finding(nil), findings...),
Dropped: []CritiquedFinding{},
Annotated: make([]CritiquedFinding, 0, len(findings)),
Verdict: verdictForFindings(findings),
}
for _, f := range findings {
res.Annotated = append(res.Annotated, CritiquedFinding{Finding: f, Verdict: CritiqueKeep})
}
res.Summary = critiqueSummary(res)
if gen == nil || len(findings) == 0 {
return res
}
if maxTokens <= 0 {
maxTokens = defaultMaxLLMTokens
}
out, err := gen(ctx, BuildCritiquePrompt(findings, diff), maxTokens)
if err != nil {
// A failing model is not an error for the pass — degrade to pass-through.
return res
}
rows, ok := parseCritiqueRows(out)
if !ok {
return res
}
return applyCritique(findings, rows)
}
// applyCritique folds the model's per-finding verdicts into the result: an
// explicit drop removes the finding (and records it with its reason); keep and
// uncertain both retain it; any finding the model did not adjudicate defaults to
// keep. The kept set's worst-of verdict is recomputed so a critique that drops
// the only error downgrades BLOCK → REVIEW/APPROVE honestly.
func applyCritique(findings []Finding, rows []critiqueVerdictRow) CritiqueResult {
byIndex := make(map[int]critiqueVerdictRow, len(rows))
for _, r := range rows {
// Last write wins for a duplicated index; harmless and deterministic.
byIndex[r.Index] = r
}
res := CritiqueResult{
Kept: make([]Finding, 0, len(findings)),
Dropped: []CritiquedFinding{},
Annotated: make([]CritiquedFinding, 0, len(findings)),
LLMUsed: true,
}
for i, f := range findings {
verdict, reason := CritiqueKeep, ""
if r, found := byIndex[i]; found {
verdict = normalizeCritiqueVerdict(r.Verdict)
reason = strings.TrimSpace(r.Reason)
}
annotated := CritiquedFinding{Finding: f, Verdict: verdict, Reason: reason}
res.Annotated = append(res.Annotated, annotated)
switch verdict {
case CritiqueDrop:
res.Dropped = append(res.Dropped, annotated)
case CritiqueUncertain:
res.Uncertain++
res.Kept = append(res.Kept, f)
default: // keep
res.Kept = append(res.Kept, f)
}
}
res.Verdict = verdictForFindings(res.Kept)
res.Summary = critiqueSummary(res)
return res
}
// normalizeCritiqueVerdict maps a model-returned verdict token to a known
// CritiqueVerdict, defaulting to keep for anything unrecognised — so an
// ambiguous or malformed token can never silently drop a finding.
func normalizeCritiqueVerdict(s string) CritiqueVerdict {
switch strings.ToLower(strings.TrimSpace(s)) {
case "drop", "false_positive", "false-positive", "remove", "discard":
return CritiqueDrop
case "uncertain", "unsure", "maybe", "unknown":
return CritiqueUncertain
default:
return CritiqueKeep
}
}
// verdictForFindings recomputes the worst-of verdict over a finding set: any
// critical/error → BLOCK, any warning → REVIEW, else APPROVE. Shared with the
// envelope's statVerdict ladder so the critique never invents a second ladder.
func verdictForFindings(findings []Finding) Verdict {
return statVerdict(recount(findings))
}
// critiqueSummary renders the one-line headline of a critique pass.
func critiqueSummary(res CritiqueResult) string {
if len(res.Dropped) == 0 {
return fmt.Sprintf("%s: kept all %d finding(s)", res.Verdict, len(res.Kept))
}
return fmt.Sprintf("%s: dropped %d false-positive / low-value finding(s), kept %d",
res.Verdict, len(res.Dropped), len(res.Kept))
}
// BuildCritiquePrompt assembles the adversarial second-pass prompt: the change
// diff as grounding, the numbered prior findings (with each finding's flagged
// source line when the generator carried one), and an instruction to return a
// strict JSON array of {index, verdict, reason} adjudications. The index ties a
// verdict back to its finding so the model never re-emits a finding body.
func BuildCritiquePrompt(findings []Finding, diff string) string {
var b strings.Builder
b.WriteString("You are a senior reviewer performing an adversarial SECOND PASS over an automated code review.\n")
b.WriteString("For EACH prior finding below, decide whether it is a genuine issue worth a reviewer's attention, ")
b.WriteString("a false positive, or low-value noise. Be skeptical: drop findings that the diff does not actually support, ")
b.WriteString("that restate a non-issue, or that are too speculative to act on. Keep findings that point at a real defect.\n\n")
if strings.TrimSpace(diff) != "" {
b.WriteString("CHANGESET DIFF (grounding):\n")
b.WriteString(strings.TrimSpace(diff))
b.WriteString("\n\n")
}
b.WriteString("PRIOR FINDINGS:\n")
for i, f := range findings {
fmt.Fprintf(&b, "[%d] rule=%s severity=%s category=%s file=%s line=%d\n",
i, valueOr(f.Rule, "-"), valueOr(string(f.Severity), "-"),
valueOr(f.Category, "-"), valueOr(f.File, "-"), f.Line)
if msg := strings.TrimSpace(f.Message); msg != "" {
fmt.Fprintf(&b, " message: %s\n", msg)
}
if src := strings.TrimSpace(f.SourceLine); src != "" {
fmt.Fprintf(&b, " flagged source: %s\n", src)
}
}
b.WriteString("\nRespond with ONLY a JSON array, one object per finding, no prose:\n")
b.WriteString(`[{"index":0,"verdict":"keep|drop|uncertain","reason":"<one short sentence>"}]` + "\n")
b.WriteString("Use \"drop\" only when you are confident the finding is a false positive or low-value. ")
b.WriteString("Use \"uncertain\" when you cannot tell. When in doubt, \"keep\".")
return b.String()
}
// parseCritiqueRows extracts the model's adjudication array from a freeform
// response. It tolerates surrounding prose / code fences by slicing the first
// top-level JSON array out of the text before unmarshalling. Returns ok=false
// when no array parses — the caller then degrades to the keep-everything
// pass-through.
func parseCritiqueRows(out string) ([]critiqueVerdictRow, bool) {
raw := extractJSONArray(out)
if raw == "" {
return nil, false
}
var rows []critiqueVerdictRow
if err := json.Unmarshal([]byte(raw), &rows); err != nil {
return nil, false
}
return rows, true
}
// extractJSONArray returns the substring spanning the first balanced top-level
// JSON array in s, or "" when none is present. It ignores brackets inside
// double-quoted strings (honouring backslash escapes) so a "[" in a reason
// string does not confuse the scan.
func extractJSONArray(s string) string {
start := strings.IndexByte(s, '[')
if start < 0 {
return ""
}
depth := 0
inStr := false
escaped := false
for i := start; i < len(s); i++ {
c := s[i]
if inStr {
switch {
case escaped:
escaped = false
case c == '\\':
escaped = true
case c == '"':
inStr = false
}
continue
}
switch c {
case '"':
inStr = true
case '[':
depth++
case ']':
depth--
if depth == 0 {
return s[start : i+1]
}
}
}
return ""
}
// valueOr returns v, or fallback when v is empty after trimming.
func valueOr(v, fallback string) string {
if strings.TrimSpace(v) == "" {
return fallback
}
return v
}
// SortCritiquedBySeverity orders critiqued findings worst-severity-first for a
// deterministic dropped-list rendering.
func SortCritiquedBySeverity(rows []CritiquedFinding) {
sort.SliceStable(rows, func(i, j int) bool {
si, sj := severityRank(rows[i].Finding.Severity), severityRank(rows[j].Finding.Severity)
if si != sj {
return si > sj
}
if rows[i].Finding.File != rows[j].Finding.File {
return rows[i].Finding.File < rows[j].Finding.File
}
return rows[i].Finding.Line < rows[j].Finding.Line
})
}