Skip to content

Commit 2c35207

Browse files
authored
feat(review): port reviewbot blockers[]/nits[] shape for extensive reviews (#1089)
The free Workers-AI pair produced shallow, hedging reviews under the open-ended assessment/suggestions/risks/criticalDefect shape. reviewbot's gold-standard depth comes from forcing the model to ENUMERATE findings via explicit blockers[] (concrete must-fix defects) + nits[] (non-blocking) arrays with a severity rubric. Port it: - REVIEW_SYSTEM_PROMPT: blockers[]/nits[]/suggestions arrays + severity discipline (a blocker points to a real diff defect; nits/hypotheticals never block; CI status is never a code finding), 'do not rubber-stamp', no hedging language. - ModelReview / parseModelReview: parse blockers + nits (finding cap 6 -> 12). - composeAdvisoryNotes: render **Blockers** + **Nits** (nits + suggestions merged). - consensusDefectOf: a consensus defect = a concrete blocker in BOTH reviews (severity-disciplined; a lone blocker is a split) -> replaces the numeric floor. Internal to services/ai-review.ts; the external advisoryNotes string + consensusDefect types are unchanged, so the gate + unified comment are unaffected. Diff patches were verified present in storage -- the gap was purely the prompt structure.
1 parent 01c315b commit 2c35207

4 files changed

Lines changed: 99 additions & 94 deletions

File tree

src/services/ai-review.ts

Lines changed: 40 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,16 @@ export const AI_CONSENSUS_FLOOR = 0.9;
3838

3939
const REVIEW_SYSTEM_PROMPT = [
4040
"You are a senior open-source maintainer giving a THOROUGH code review of a single pull request diff.",
41-
"Review like a careful human: read each meaningful hunk and give SPECIFIC, concrete feedback — correctness",
42-
"bugs, logic errors, risky patterns, missing/incorrect error handling, unhandled edge cases, security issues,",
43-
"performance problems, race conditions, and API/contract or backward-compat breaks. Reference the file (and the",
44-
"function/line where you can) for every point. Do NOT rubber-stamp: if the diff is genuinely clean, state",
45-
"specifically WHY it is safe (what you checked); otherwise give real, actionable findings.",
46-
"Judge only the diff and the context provided. `assessment` must be a substantive walkthrough of the change",
47-
"(several sentences — what it does, whether it is correct, and the notable details), NOT one generic line.",
48-
"`suggestions` = concrete improvements (file-referenced); `risks` = things that could break or need a human's",
49-
"attention. Aim for real depth over brevity.",
50-
"Report a criticalDefect ONLY when you are highly confident the change introduces a real bug, a security",
51-
"hole, data loss, or a build break — NOT for style, nits, naming, or merely-missing tests (those belong in",
52-
"suggestions/risks and must NOT block).",
53-
"Never mention rewards, rankings, payouts, wallets, hotkeys, coldkeys, trust scores, scoreability,",
54-
"reviewability, or farming.",
55-
'Respond with ONLY a JSON object of this exact shape (no prose, no code fence):',
56-
'{"assessment": string, "suggestions": string[], "risks": string[],',
57-
' "criticalDefect": {"present": boolean, "confidence": number, "title": string, "detail": string}}',
41+
"Read each meaningful hunk and review like a careful human; judge ONLY the diff and the context provided.",
42+
"Respond with ONLY a JSON object of this exact shape (no prose, no code fence):",
43+
'{"assessment": string, "blockers": string[], "nits": string[], "suggestions": string[]}',
44+
"- assessment: a SUBSTANTIVE walkthrough (several sentences) — what the change does, whether it is correct, and the notable details. Specific to THIS diff; NEVER a generic one-liner and never hedging ('appears to', 'seems to').",
45+
"- blockers: each ONE sentence naming a CONCRETE must-fix defect IN THIS DIFF — a correctness/logic bug, a security hole, data loss, a build/test breakage, a race condition, or an API/contract/backward-compat break. Reference the file (and function/line). Empty [] if there are genuinely none.",
46+
"- nits: each ONE sentence — a NON-blocking point: style, naming, a 'consider…', a missing doc/comment, an unhandled edge case worth noting, or a minor improvement. File-reference where you can.",
47+
"- suggestions: concrete, file-referenced improvements (may overlap nits).",
48+
"Do NOT rubber-stamp. If the diff is genuinely clean, the assessment must state SPECIFICALLY why it is safe (what you checked) and blockers must be []. Otherwise give real, specific findings — aim for depth, list every concern you actually see.",
49+
"SEVERITY DISCIPLINE: a BLOCKER is a real defect you can point to in the diff; a NIT is style / preference / hypothetical / optional / docs. CI or check status ITSELF (failing, pending, unverified) is NOT a code defect — NEVER list it as a blocker or nit (the gate evaluates CI separately). Nits and hypotheticals are never blockers.",
50+
"Never mention rewards, rankings, payouts, wallets, hotkeys, coldkeys, trust scores, scoreability, reviewability, or farming.",
5851
].join(" ");
5952

6053
/** A maintainer's BYOK provider credential, decrypted at call time. Never logged, never returned. */
@@ -107,9 +100,11 @@ export type GittensoryAiReviewResult =
107100

108101
type ModelReview = {
109102
assessment: string;
103+
// blockers = concrete must-fix defects in the diff (drive the consensus defect / gate); nits = non-blocking
104+
// points; suggestions = concrete improvements (rendered alongside nits). reviewbot-parity shape. (#extensive-reviews)
105+
blockers: string[];
106+
nits: string[];
110107
suggestions: string[];
111-
risks: string[];
112-
criticalDefect: { present: boolean; confidence: number; title: string; detail: string };
113108
};
114109

115110
type AiGatewayOptions = { gateway?: { id: string } };
@@ -191,23 +186,13 @@ export function parseModelReview(text: string): ModelReview | null {
191186
try {
192187
const obj = JSON.parse(match[0]) as Record<string, unknown>;
193188
const toList = (value: unknown): string[] =>
194-
Array.isArray(value) ? value.filter((x): x is string => typeof x === "string").map((x) => x.trim()).filter(Boolean).slice(0, 6) : [];
189+
Array.isArray(value) ? value.filter((x): x is string => typeof x === "string").map((x) => x.trim()).filter(Boolean).slice(0, 12) : [];
195190
const assessment = typeof obj.assessment === "string" ? obj.assessment.trim() : "";
196-
const defectRaw = obj.criticalDefect && typeof obj.criticalDefect === "object" ? (obj.criticalDefect as Record<string, unknown>) : {};
197-
const present = defectRaw.present === true;
198-
const confidence = typeof defectRaw.confidence === "number" ? Math.max(0, Math.min(1, defectRaw.confidence)) : 0;
199-
if (!assessment && !present && !Array.isArray(obj.suggestions)) return null;
200-
return {
201-
assessment,
202-
suggestions: toList(obj.suggestions),
203-
risks: toList(obj.risks),
204-
criticalDefect: {
205-
present,
206-
confidence,
207-
title: typeof defectRaw.title === "string" ? defectRaw.title.trim().slice(0, 140) : "",
208-
detail: typeof defectRaw.detail === "string" ? defectRaw.detail.trim().slice(0, 400) : "",
209-
},
210-
};
191+
const blockers = toList(obj.blockers);
192+
const nits = toList(obj.nits);
193+
const suggestions = toList(obj.suggestions);
194+
if (!assessment && blockers.length === 0 && nits.length === 0 && suggestions.length === 0) return null;
195+
return { assessment, blockers, nits, suggestions };
211196
} catch {
212197
return null;
213198
}
@@ -348,35 +333,38 @@ async function runProviderReview(providerKey: AiReviewProviderKey, system: strin
348333
/** Compose a public-safe markdown advisory blurb from one or two model reviews. Null if nothing safe. */
349334
export function composeAdvisoryNotes(reviews: ModelReview[]): string | null {
350335
const assessments = reviews.map((r) => r.assessment).filter(Boolean);
351-
const suggestions = [...new Set(reviews.flatMap((r) => r.suggestions))].slice(0, 5);
352-
const risks = [...new Set(reviews.flatMap((r) => r.risks))].slice(0, 4);
336+
const blockers = [...new Set(reviews.flatMap((r) => r.blockers))].slice(0, 8);
337+
// nits + suggestions are both non-blocking — merge + dedupe for the write-up.
338+
const nits = [...new Set(reviews.flatMap((r) => [...r.nits, ...r.suggestions]))].slice(0, 12);
353339
const assessment = toPublicSafe(assessments[0] ?? "");
354-
const safeSuggestions = suggestions.map((s) => toPublicSafe(s)).filter((s): s is string => Boolean(s));
355-
const safeRisks = risks.map((s) => toPublicSafe(s)).filter((s): s is string => Boolean(s));
356-
if (!assessment && safeSuggestions.length === 0 && safeRisks.length === 0) return null;
340+
const safeBlockers = blockers.map((s) => toPublicSafe(s)).filter((s): s is string => Boolean(s));
341+
const safeNits = nits.map((s) => toPublicSafe(s)).filter((s): s is string => Boolean(s));
342+
if (!assessment && safeBlockers.length === 0 && safeNits.length === 0) return null;
357343
const lines: string[] = [];
358344
if (assessment) lines.push(assessment, "");
359-
if (safeSuggestions.length > 0) {
360-
lines.push("**Suggestions**");
361-
lines.push(...safeSuggestions.map((s) => `- ${s}`));
345+
if (safeBlockers.length > 0) {
346+
lines.push("**Blockers**");
347+
lines.push(...safeBlockers.map((s) => `- ${s}`));
362348
lines.push("");
363349
}
364-
if (safeRisks.length > 0) {
365-
lines.push("**Risks**");
366-
lines.push(...safeRisks.map((s) => `- ${s}`));
350+
if (safeNits.length > 0) {
351+
lines.push("**Nits**");
352+
lines.push(...safeNits.map((s) => `- ${s}`));
367353
}
368354
// Reaching here means at least one section was pushed (the all-empty case returned null above).
369355
return lines.join("\n").trim();
370356
}
371357

372-
/** True iff BOTH reviews independently report a critical defect at/above the floor. */
358+
/** A CONSENSUS defect = BOTH reviews independently name at least one concrete blocker (the severity-disciplined
359+
* reviewbot model: a lone blocker in a dual review is a split, not a hard block). `floor` retained for the
360+
* caller signature; the consensus here is blocker PRESENCE in both reviews (the prompt's rubric keeps nits out). */
373361
export function consensusDefectOf(a: ModelReview, b: ModelReview, floor: number): AiConsensusDefect | null {
374-
const both = a.criticalDefect.present && b.criticalDefect.present && a.criticalDefect.confidence >= floor && b.criticalDefect.confidence >= floor;
375-
if (!both) return null;
376-
const title = toPublicSafe(a.criticalDefect.title || b.criticalDefect.title || "AI reviewers agree on a likely critical defect");
377-
const detail = toPublicSafe(a.criticalDefect.detail || b.criticalDefect.detail);
362+
void floor;
363+
if (a.blockers.length === 0 || b.blockers.length === 0) return null;
364+
const title = toPublicSafe(a.blockers[0] || b.blockers[0] || "AI reviewers agree on a likely blocking defect");
365+
const detail = toPublicSafe([...new Set([...a.blockers, ...b.blockers])].slice(0, 4).join("; "));
378366
if (!title) return null; // unsafe title → drop the block entirely (fail-safe)
379-
return { title, detail: detail ?? "Both AI reviewers independently flagged a high-confidence critical defect in this change.", confidence: Math.min(a.criticalDefect.confidence, b.criticalDefect.confidence) };
367+
return { title, detail: detail ?? "Both AI reviewers independently flagged a concrete must-fix defect in this change.", confidence: 1 };
380368
}
381369

382370
/**

test/unit/ai-review-advisory.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ function advisory(over: Partial<Advisory> = {}): Advisory {
4949
const pr = { number: 3, title: "Add helper", body: "Adds a helper." };
5050

5151
function defectJson() {
52-
return JSON.stringify({ assessment: "Likely crash.", suggestions: ["Guard null."], risks: ["Null deref."], criticalDefect: { present: true, confidence: 0.97, title: "Null deref", detail: "Dereferences null." } });
52+
return JSON.stringify({ assessment: "Likely crash.", blockers: ["Null dereference of a possibly-null value in src/a.ts."], nits: ["Guard null."], suggestions: ["Guard null."] });
5353
}
5454
function notesOnlyJson() {
55-
return JSON.stringify({ assessment: "Looks fine.", suggestions: ["Add a test."], risks: [], criticalDefect: { present: false, confidence: 0, title: "", detail: "" } });
55+
return JSON.stringify({ assessment: "Looks fine.", blockers: [], nits: ["Add a test."], suggestions: ["Add a test."] });
5656
}
5757

5858
function aiEnv(run: () => Promise<unknown>, flags = true) {

0 commit comments

Comments
 (0)