@mrclrchtr/supi-review 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-review",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "SuPi Review extension — structured code review via /supi-review command",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/review.ts CHANGED
@@ -260,20 +260,60 @@ function maybeQueueReviewFollowUp(pi: ExtensionAPI, result: ReviewResult): void
260
260
  function buildReviewFollowUpInstruction(
261
261
  result: Extract<ReviewResult, { kind: "success" }>,
262
262
  ): string {
263
- const findings = result.output.findings.map(
264
- (finding, index) => `- #${index + 1}: ${finding.title}`,
265
- );
263
+ const { findings, overall_correctness } = result.output;
264
+ const criticalCount = findings.filter((f) => f.priority === 3).length;
265
+ const majorCount = findings.filter((f) => f.priority === 2).length;
266
+
267
+ const findingList = findings.map((finding, index) => `- #${index + 1}: ${finding.title}`);
266
268
 
267
- return [
268
- "A code review just completed and the result is available in the preceding `supi-review` message.",
269
- "Do not start fixing code immediately.",
270
- "Your next task is to ask the user what to do with the review findings.",
271
- "If the `ask_user` tool is available, use it for this decision.",
272
- "Offer exactly these options: Done, Fix all, Fix selected, Verify findings.",
273
- "If the user chooses Fix selected, ask a follow-up question listing the findings by number/title.",
274
- "If the user chooses Verify findings, verify the findings first and then ask again whether to Fix all or Fix selected.",
269
+ const header =
270
+ "A code review just completed and the result is available in the preceding `supi-review` message.";
271
+ const noFixing = "Do not start fixing code immediately.";
272
+ const useAskUser = "If the `ask_user` tool is available, use it for this decision.";
273
+
274
+ const appendFindings = (lines: string[]): string[] => [
275
+ ...lines,
275
276
  "",
276
277
  "Current findings:",
277
- ...findings,
278
- ].join("\n");
278
+ ...findingList,
279
+ ];
280
+
281
+ if (criticalCount > 0) {
282
+ return appendFindings([
283
+ header,
284
+ `⚠️ ${criticalCount} critical finding(s) found. Urge the user to fix before merging.`,
285
+ noFixing,
286
+ useAskUser,
287
+ "Offer these options: Fix all, Fix critical only, Done.",
288
+ ]).join("\n");
289
+ }
290
+
291
+ const contradictionNote =
292
+ overall_correctness === "patch is correct" && findings.length > 0
293
+ ? "The reviewer marked the patch as correct but found issues — verify the verdict before acting."
294
+ : undefined;
295
+
296
+ if (majorCount > 0) {
297
+ const lines: string[] = [header];
298
+ if (contradictionNote) lines.push(contradictionNote);
299
+ lines.push(
300
+ noFixing,
301
+ useAskUser,
302
+ "Offer exactly these options: Done, Fix all, Fix selected, Verify findings.",
303
+ "If the user chooses Fix selected, ask a follow-up question listing the findings by number/title.",
304
+ "If the user chooses Verify findings, verify the findings first and then ask again whether to Fix all or Fix selected.",
305
+ );
306
+ return appendFindings(lines).join("\n");
307
+ }
308
+
309
+ // Only minor/info findings
310
+ return appendFindings([
311
+ header,
312
+ ...(contradictionNote
313
+ ? [contradictionNote]
314
+ : ["Only minor/info suggestions — no blocking issues."]),
315
+ noFixing,
316
+ useAskUser,
317
+ "Offer these options: Apply suggestions, Skip.",
318
+ ]).join("\n");
279
319
  }
@@ -5,9 +5,11 @@ import type {
5
5
  SynthesizedReviewBrief,
6
6
  } from "../types.ts";
7
7
 
8
- interface DiffSection {
8
+ export interface DiffSection {
9
9
  file: string;
10
10
  text: string;
11
+ additions: number;
12
+ deletions: number;
11
13
  }
12
14
 
13
15
  /** Build the final prompt packet for the reviewer child session. */
@@ -49,6 +51,8 @@ export function buildReviewPacket(
49
51
  ...snapshot.changedFiles.map((file) => `- ${file}`),
50
52
  ];
51
53
 
54
+ baseParts.push("", buildFileOverviewTable(snapshot.changedFiles, sections));
55
+
52
56
  if (preamble.trim()) {
53
57
  baseParts.push("", "## Snapshot notes", truncate(preamble.trim(), 1_500));
54
58
  }
@@ -115,7 +119,21 @@ export function getPacketCharBudget(model: ReviewModelSelection): number {
115
119
  return tokenBudget * 4;
116
120
  }
117
121
 
118
- function splitDiffSections(text: string): { preamble: string; sections: DiffSection[] } {
122
+ function countDiffLines(lines: string[]): { additions: number; deletions: number } {
123
+ let additions = 0;
124
+ let deletions = 0;
125
+ for (const line of lines) {
126
+ // Skip diff header lines (e.g. "--- a/file", "+++ b/file").
127
+ // The trailing space ensures we do not skip content lines whose first
128
+ // characters happen to be "+++" or "---".
129
+ if (line.startsWith("--- ") || line.startsWith("+++ ")) continue;
130
+ if (line.startsWith("+")) additions++;
131
+ else if (line.startsWith("-")) deletions++;
132
+ }
133
+ return { additions, deletions };
134
+ }
135
+
136
+ export function splitDiffSections(text: string): { preamble: string; sections: DiffSection[] } {
119
137
  const lines = text.split("\n");
120
138
  const preamble: string[] = [];
121
139
  const sections: DiffSection[] = [];
@@ -128,7 +146,13 @@ function splitDiffSections(text: string): { preamble: string; sections: DiffSect
128
146
  currentFile = undefined;
129
147
  return;
130
148
  }
131
- sections.push({ file: currentFile, text: current.join("\n").trimEnd() });
149
+ const stats = countDiffLines(current);
150
+ sections.push({
151
+ file: currentFile,
152
+ text: current.join("\n").trimEnd(),
153
+ additions: stats.additions,
154
+ deletions: stats.deletions,
155
+ });
132
156
  current = [];
133
157
  currentFile = undefined;
134
158
  };
@@ -206,6 +230,100 @@ function toPathTokens(path: string): string[] {
206
230
  .filter((part) => part.length >= 3);
207
231
  }
208
232
 
233
+ /** Categorize a file path for skip-list annotation, or undefined if it should be reviewed. */
234
+ export function classifySkipCategory(file: string): string | undefined {
235
+ const lockfiles = new Set([
236
+ "package-lock.json",
237
+ "yarn.lock",
238
+ "pnpm-lock.yaml",
239
+ "Gemfile.lock",
240
+ "Cargo.lock",
241
+ "poetry.lock",
242
+ "composer.lock",
243
+ ]);
244
+
245
+ const name = file.split("/").pop() ?? file;
246
+ if (lockfiles.has(name)) return "lockfile";
247
+ if (name.startsWith("CHANGELOG") || name.startsWith("CHANGES") || name === "CHANGE_LOG") {
248
+ return "changelog";
249
+ }
250
+
251
+ if (file.includes("__snapshots__/") || name.endsWith(".snap")) {
252
+ return "snapshot";
253
+ }
254
+
255
+ if (
256
+ file.includes("dist/") ||
257
+ file.includes("build/") ||
258
+ file.includes(".next/") ||
259
+ file.includes("__generated__/")
260
+ ) {
261
+ return "generated";
262
+ }
263
+
264
+ if (file.includes("vendor/") || file.includes("third_party/")) {
265
+ return "vendored";
266
+ }
267
+
268
+ if (name.endsWith(".min.js") || name.endsWith(".min.css")) return "generated";
269
+
270
+ return undefined;
271
+ }
272
+
273
+ function formatOverviewRow(
274
+ file: string,
275
+ statsMap: Map<string, { additions: number; deletions: number }>,
276
+ binaryFiles: Set<string>,
277
+ ): string {
278
+ const stats = statsMap.get(file);
279
+ const skipCategory = classifySkipCategory(file);
280
+ const annotations: string[] = [];
281
+
282
+ if (!stats) {
283
+ // File in the changed-files manifest but not in the parsed diff sections.
284
+ // This happens for untracked files in working-tree snapshots, which
285
+ // have no diff section to parse. Do not guess 0/0 or mark as trivial.
286
+ if (skipCategory) annotations.push(`skip — ${skipCategory}`);
287
+ const annotation = annotations.length > 0 ? ` (${annotations.join(", ")})` : "";
288
+ return `| ${file} | ? | ?${annotation} |`;
289
+ }
290
+
291
+ if (binaryFiles.has(file)) {
292
+ // Binary diffs have no diffable +/- lines. Show unknown stats.
293
+ if (skipCategory) annotations.push(`skip — ${skipCategory}`);
294
+ annotations.push("binary");
295
+ const annotation = annotations.length > 0 ? ` (${annotations.join(", ")})` : "";
296
+ return `| ${file} | ? | ?${annotation} |`;
297
+ }
298
+
299
+ const total = stats.additions + stats.deletions;
300
+ if (total < 5) annotations.push("trivial");
301
+ if (skipCategory) annotations.push(`skip — ${skipCategory}`);
302
+ const annotation = annotations.length > 0 ? ` (${annotations.join(", ")})` : "";
303
+ return `| ${file} | ${stats.additions} | ${stats.deletions}${annotation} |`;
304
+ }
305
+
306
+ function buildFileOverviewTable(changedFiles: string[], sections: DiffSection[]): string {
307
+ const statsMap = new Map<string, { additions: number; deletions: number }>();
308
+ const binaryFiles = new Set<string>();
309
+ for (const section of sections) {
310
+ const existing = statsMap.get(section.file);
311
+ statsMap.set(section.file, {
312
+ additions: (existing?.additions ?? 0) + section.additions,
313
+ deletions: (existing?.deletions ?? 0) + section.deletions,
314
+ });
315
+ if (section.text.includes("Binary files ")) {
316
+ binaryFiles.add(section.file);
317
+ }
318
+ }
319
+
320
+ const header = "| File | +Add | -Del |";
321
+ const separator = "|---|---|---|";
322
+ const rows = changedFiles.map((file) => formatOverviewRow(file, statsMap, binaryFiles));
323
+
324
+ return [`## File overview`, "", header, separator, ...rows].join("\n");
325
+ }
326
+
209
327
  function toBullets(items: string[], fallback: string): string[] {
210
328
  return items.length > 0 ? items.map((item) => `- ${item}`) : [fallback];
211
329
  }
@@ -69,7 +69,7 @@ async function createBriefSession(
69
69
  cwd: invocation.cwd,
70
70
  model: invocation.model,
71
71
  modelRegistry: invocation.modelRegistry,
72
- thinkingLevel: clampThinkingLevel(invocation.model, "high"),
72
+ thinkingLevel: clampThinkingLevel(invocation.model, "xhigh"),
73
73
  tools: ["submit_review_brief"],
74
74
  customTools: [submitBriefTool],
75
75
  resourceLoader,
@@ -65,9 +65,21 @@ export function buildReviewerSystemPrompt(): string {
65
65
  "- Title: concise and specific.",
66
66
  "- Body: explain the issue, why it matters, and a concrete fix direction.",
67
67
  "- code_location: 1-based inclusive line range.",
68
- "- confidence_score: 0.0-1.0.",
69
- "- priority: 0=info, 1=minor, 2=major, 3=critical.",
70
- "- overall_correctness: 'patch is correct' | 'mostly correct' | 'patch is incorrect'.",
68
+ "",
69
+ "--- Finding calibration ---",
70
+ "Priority:",
71
+ " 0 (info): style nits, naming suggestions, non-functional improvements without real impact",
72
+ " 1 (minor): unlikely edge cases, minor perf concerns without benchmarks, missing comments",
73
+ " 2 (major): logic errors, incorrect error handling, API contract violations, race conditions",
74
+ " 3 (critical): security vulnerabilities, data loss/corruption, crashes, breaking changes",
75
+ "Confidence:",
76
+ " 0.8-1.0: you verified the issue by reading surrounding code or grepping the codebase",
77
+ " 0.5-0.8: suspected from the diff, plausible but not fully verified",
78
+ " <0.5: do not report — too uncertain; either verify further or drop it",
79
+ "overall_correctness:",
80
+ " 'patch is correct': no issues found, or only info-level suggestions",
81
+ " 'mostly correct': minor issues present, safe to merge with fixes",
82
+ " 'patch is incorrect': major/critical issues that should block merge",
71
83
  "",
72
84
  "--- Tool strategy ---",
73
85
  "- Use read to inspect full files when the inline diff lacks context.",
@@ -75,6 +87,11 @@ export function buildReviewerSystemPrompt(): string {
75
87
  "- Use find to locate related files quickly.",
76
88
  "- Use ls when you need a quick directory overview.",
77
89
  "",
90
+ "--- Skipped files ---",
91
+ "- Skip reviewing: lockfiles, generated/bundled code (dist/, .next/, __generated__/),",
92
+ " vendored dependencies, changelogs, snapshot files, minified bundles, and binary files.",
93
+ "- Focus on application source and test code.",
94
+ "",
78
95
  "Do NOT output JSON directly — call submit_review with the structured result.",
79
96
  ].join("\n");
80
97
  }