diff-hound 1.0.2 → 1.2.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.
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const parseUnifiedDiff_1 = require("./parseUnifiedDiff");
5
+ (0, vitest_1.describe)("parseUnifiedDiff", () => {
6
+ (0, vitest_1.describe)("basic functionality", () => {
7
+ (0, vitest_1.it)("should return empty array for empty input", () => {
8
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)([]);
9
+ (0, vitest_1.expect)(result).toEqual([]);
10
+ });
11
+ (0, vitest_1.it)("should handle file with no patch", () => {
12
+ const files = [
13
+ {
14
+ filename: "src/unchanged.ts",
15
+ status: "modified",
16
+ additions: 0,
17
+ deletions: 0,
18
+ },
19
+ ];
20
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
21
+ (0, vitest_1.expect)(result[0].patch).toBeUndefined();
22
+ });
23
+ (0, vitest_1.it)("should skip deleted files", () => {
24
+ const files = [
25
+ {
26
+ filename: "src/deleted.ts",
27
+ status: "deleted",
28
+ additions: 0,
29
+ deletions: 20,
30
+ patch: "diff content here",
31
+ },
32
+ ];
33
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
34
+ (0, vitest_1.expect)(result[0].patch).toBe("diff content here");
35
+ });
36
+ });
37
+ (0, vitest_1.describe)("line number annotation", () => {
38
+ (0, vitest_1.it)("should annotate added lines with line numbers", () => {
39
+ const files = [
40
+ {
41
+ filename: "src/utils.ts",
42
+ status: "added",
43
+ additions: 3,
44
+ deletions: 0,
45
+ patch: `@@ -0,0 +1,3 @@
46
+ +line one
47
+ +line two
48
+ +line three`,
49
+ },
50
+ ];
51
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
52
+ const lines = result[0].patch?.split("\n") ?? [];
53
+ (0, vitest_1.expect)(lines[0]).toBe("@@ -0,0 +1,3 @@");
54
+ (0, vitest_1.expect)(lines[1]).toBe("+line one // LINE_NUMBER: 1");
55
+ (0, vitest_1.expect)(lines[2]).toBe("+line two // LINE_NUMBER: 2");
56
+ (0, vitest_1.expect)(lines[3]).toBe("+line three // LINE_NUMBER: 3");
57
+ });
58
+ (0, vitest_1.it)("should handle multiple hunks correctly", () => {
59
+ const files = [
60
+ {
61
+ filename: "src/app.ts",
62
+ status: "modified",
63
+ additions: 6,
64
+ deletions: 2,
65
+ patch: `@@ -1,5 +1,5 @@
66
+ context line
67
+ -old line
68
+ +new line 1
69
+ +new line 2
70
+ context line
71
+ context line
72
+ @@ -20,3 +20,5 @@
73
+ context line
74
+ -old line 2
75
+ +new line 3
76
+ +new line 4
77
+ +new line 5`,
78
+ },
79
+ ];
80
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
81
+ const lines = result[0].patch?.split("\n") ?? [];
82
+ // First hunk starts at line 1
83
+ (0, vitest_1.expect)(lines).toContain("+new line 1 // LINE_NUMBER: 2");
84
+ (0, vitest_1.expect)(lines).toContain("+new line 2 // LINE_NUMBER: 3");
85
+ // Second hunk starts at line 20
86
+ (0, vitest_1.expect)(lines).toContain("+new line 3 // LINE_NUMBER: 21");
87
+ (0, vitest_1.expect)(lines).toContain("+new line 4 // LINE_NUMBER: 22");
88
+ (0, vitest_1.expect)(lines).toContain("+new line 5 // LINE_NUMBER: 23");
89
+ });
90
+ (0, vitest_1.it)("should handle context lines without annotation", () => {
91
+ const files = [
92
+ {
93
+ filename: "src/file.ts",
94
+ status: "modified",
95
+ additions: 1,
96
+ deletions: 0,
97
+ patch: `@@ -5,5 +5,6 @@
98
+ context line 1
99
+ context line 2
100
+ +added line
101
+ context line 3
102
+ context line 4`,
103
+ },
104
+ ];
105
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
106
+ const lines = result[0].patch?.split("\n") ?? [];
107
+ (0, vitest_1.expect)(lines[1]).toBe(" context line 1");
108
+ (0, vitest_1.expect)(lines[2]).toBe(" context line 2");
109
+ (0, vitest_1.expect)(lines[3]).toBe("+added line // LINE_NUMBER: 7");
110
+ (0, vitest_1.expect)(lines[4]).toBe(" context line 3");
111
+ });
112
+ (0, vitest_1.it)("should handle deleted lines without line numbers", () => {
113
+ const files = [
114
+ {
115
+ filename: "src/file.ts",
116
+ status: "modified",
117
+ additions: 0,
118
+ deletions: 2,
119
+ patch: `@@ -10,5 +8,3 @@
120
+ context line
121
+ -deleted line 1
122
+ -deleted line 2
123
+ context line`,
124
+ },
125
+ ];
126
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
127
+ const lines = result[0].patch?.split("\n") ?? [];
128
+ (0, vitest_1.expect)(lines).toContain("-deleted line 1");
129
+ (0, vitest_1.expect)(lines).toContain("-deleted line 2");
130
+ (0, vitest_1.expect)(lines[2]).not.toContain("LINE_NUMBER");
131
+ (0, vitest_1.expect)(lines[3]).not.toContain("LINE_NUMBER");
132
+ });
133
+ });
134
+ (0, vitest_1.describe)("hunk header parsing", () => {
135
+ (0, vitest_1.it)("should parse hunk header with single line addition", () => {
136
+ const files = [
137
+ {
138
+ filename: "src/single.ts",
139
+ status: "modified",
140
+ additions: 1,
141
+ deletions: 0,
142
+ patch: `@@ -10 +10,2 @@
143
+ context
144
+ +added`,
145
+ },
146
+ ];
147
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
148
+ const lines = result[0].patch?.split("\n") ?? [];
149
+ (0, vitest_1.expect)(lines[0]).toBe("@@ -10 +10,2 @@");
150
+ // Line 10 is context, added line is at line 11
151
+ (0, vitest_1.expect)(lines[2]).toBe("+added // LINE_NUMBER: 11");
152
+ });
153
+ (0, vitest_1.it)("should parse hunk header starting at line 1", () => {
154
+ const files = [
155
+ {
156
+ filename: "src/start.ts",
157
+ status: "modified",
158
+ additions: 2,
159
+ deletions: 0,
160
+ patch: `@@ -0,0 +1,2 @@
161
+ +first
162
+ +second`,
163
+ },
164
+ ];
165
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
166
+ const lines = result[0].patch?.split("\n") ?? [];
167
+ (0, vitest_1.expect)(lines[1]).toBe("+first // LINE_NUMBER: 1");
168
+ (0, vitest_1.expect)(lines[2]).toBe("+second // LINE_NUMBER: 2");
169
+ });
170
+ (0, vitest_1.it)("should reset line offset for each hunk", () => {
171
+ const files = [
172
+ {
173
+ filename: "src/multi.ts",
174
+ status: "modified",
175
+ additions: 4,
176
+ deletions: 0,
177
+ patch: `@@ -1,3 +1,5 @@
178
+ line1
179
+ +added1
180
+ +added2
181
+ line2
182
+ line3
183
+ @@ -50,3 +52,5 @@
184
+ line50
185
+ +added3
186
+ +added4
187
+ line51
188
+ line52`,
189
+ },
190
+ ];
191
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
192
+ const lines = result[0].patch?.split("\n") ?? [];
193
+ // First hunk
194
+ (0, vitest_1.expect)(lines).toContain("+added1 // LINE_NUMBER: 2");
195
+ (0, vitest_1.expect)(lines).toContain("+added2 // LINE_NUMBER: 3");
196
+ // Second hunk should start at 53
197
+ (0, vitest_1.expect)(lines).toContain("+added3 // LINE_NUMBER: 53");
198
+ (0, vitest_1.expect)(lines).toContain("+added4 // LINE_NUMBER: 54");
199
+ });
200
+ });
201
+ (0, vitest_1.describe)("edge cases", () => {
202
+ (0, vitest_1.it)("should handle file paths with special characters", () => {
203
+ const files = [
204
+ {
205
+ filename: "src/components/my-component.ts",
206
+ status: "modified",
207
+ additions: 1,
208
+ deletions: 0,
209
+ patch: `@@ -1,2 +1,3 @@
210
+ import React from 'react';
211
+ +import { useEffect } from 'react';
212
+ export function Component() {},
213
+ `,
214
+ },
215
+ ];
216
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
217
+ (0, vitest_1.expect)(result[0].filename).toBe("src/components/my-component.ts");
218
+ (0, vitest_1.expect)(result[0].patch).toContain("LINE_NUMBER: 2");
219
+ });
220
+ (0, vitest_1.it)("should handle empty lines in patches", () => {
221
+ const files = [
222
+ {
223
+ filename: "src/spacing.ts",
224
+ status: "modified",
225
+ additions: 2,
226
+ deletions: 0,
227
+ patch: `@@ -1,3 +1,5 @@
228
+ line1
229
+ +
230
+ +line between
231
+ line2`,
232
+ },
233
+ ];
234
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
235
+ const lines = result[0].patch?.split("\n") ?? [];
236
+ (0, vitest_1.expect)(lines[2]).toBe("+ // LINE_NUMBER: 2");
237
+ (0, vitest_1.expect)(lines[3]).toBe("+line between // LINE_NUMBER: 3");
238
+ });
239
+ (0, vitest_1.it)("should preserve original file metadata", () => {
240
+ const files = [
241
+ {
242
+ filename: "src/test.ts",
243
+ status: "renamed",
244
+ additions: 1,
245
+ deletions: 1,
246
+ previousFilename: "src/old-test.ts",
247
+ patch: `@@ -1,1 +1,1 @@
248
+ -old
249
+ +new`,
250
+ },
251
+ ];
252
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
253
+ (0, vitest_1.expect)(result[0].filename).toBe("src/test.ts");
254
+ (0, vitest_1.expect)(result[0].status).toBe("renamed");
255
+ (0, vitest_1.expect)(result[0].previousFilename).toBe("src/old-test.ts");
256
+ (0, vitest_1.expect)(result[0].additions).toBe(1);
257
+ (0, vitest_1.expect)(result[0].deletions).toBe(1);
258
+ });
259
+ (0, vitest_1.it)("should handle multiple files in a single call", () => {
260
+ const files = [
261
+ {
262
+ filename: "src/file1.ts",
263
+ status: "modified",
264
+ additions: 1,
265
+ deletions: 0,
266
+ patch: `@@ -1,1 +1,2 @@
267
+ line1
268
+ +added`,
269
+ },
270
+ {
271
+ filename: "src/file2.ts",
272
+ status: "modified",
273
+ additions: 1,
274
+ deletions: 0,
275
+ patch: `@@ -5,1 +5,2 @@
276
+ line5
277
+ +added here`,
278
+ },
279
+ ];
280
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
281
+ (0, vitest_1.expect)(result).toHaveLength(2);
282
+ (0, vitest_1.expect)(result[0].patch).toContain("LINE_NUMBER: 2");
283
+ // Line 5 is context, added line is at line 6
284
+ (0, vitest_1.expect)(result[1].patch).toContain("LINE_NUMBER: 6");
285
+ });
286
+ (0, vitest_1.it)("should handle lines starting with +++ or --- (diff metadata)", () => {
287
+ const files = [
288
+ {
289
+ filename: "src/file.ts",
290
+ status: "modified",
291
+ additions: 0,
292
+ deletions: 0,
293
+ patch: `--- a/src/file.ts
294
+ +++ b/src/file.ts
295
+ @@ -1,3 +1,3 @@
296
+ context
297
+ -old
298
+ +new`,
299
+ },
300
+ ];
301
+ const result = (0, parseUnifiedDiff_1.parseUnifiedDiff)(files);
302
+ const lines = result[0].patch?.split("\n") ?? [];
303
+ // These should not be annotated as added lines
304
+ (0, vitest_1.expect)(lines[0]).toBe("--- a/src/file.ts");
305
+ (0, vitest_1.expect)(lines[1]).toBe("+++ b/src/file.ts");
306
+ (0, vitest_1.expect)(lines[0]).not.toContain("LINE_NUMBER");
307
+ (0, vitest_1.expect)(lines[1]).not.toContain("LINE_NUMBER");
308
+ });
309
+ });
310
+ });
package/dist/index.js CHANGED
@@ -17,18 +17,23 @@ async function main() {
17
17
  const config = (0, config_1.validateConfig)(cliOptions, fileConfig);
18
18
  (0, cli_1.verboseLog)(config, `Bot configuration: ${JSON.stringify(config, null, 2)}`);
19
19
  // Get platform adapter
20
- const platform = await (0, platforms_1.getPlatform)(config.gitPlatform);
20
+ const platform = await (0, platforms_1.getPlatform)(config.gitPlatform, config);
21
21
  (0, cli_1.verboseLog)(config, `Using platform: ${config.gitPlatform}`);
22
22
  // Get model adapter
23
23
  const model = (0, models_1.getModel)(config.provider, config.model, config.endpoint);
24
24
  (0, cli_1.verboseLog)(config, `Using model: ${config.model}`);
25
- // Ensure repository is specified
26
- if (!config.repo) {
25
+ // Ensure repository is specified (not needed for local mode)
26
+ if (!config.local && !config.repo) {
27
27
  console.error("Error: Repository is not specified. Please add it to the config file or use the --repo CLI option.");
28
28
  process.exit(1);
29
29
  }
30
+ // In local mode, use a placeholder repo identifier
31
+ const repo = config.local ? "local" : config.repo;
32
+ if (config.local) {
33
+ console.log("Running in local mode (dry-run)");
34
+ }
30
35
  // Get pull requests that need review
31
- const pullRequests = await platform.getPullRequests(config.repo);
36
+ const pullRequests = await platform.getPullRequests(repo);
32
37
  (0, cli_1.verboseLog)(config, `Found ${pullRequests.length} PRs`);
33
38
  if (pullRequests.length === 0) {
34
39
  console.log("No pull requests found that need review");
@@ -39,7 +44,7 @@ async function main() {
39
44
  for (const pr of pullRequests) {
40
45
  (0, cli_1.verboseLog)(config, `Processing PR #${pr.number}: ${pr.title}`);
41
46
  // Check if AI has already commented since the last update
42
- const hasCommented = await platform.hasAICommented(config.repo, pr.id);
47
+ const hasCommented = await platform.hasAICommented(repo, pr.id);
43
48
  if (hasCommented) {
44
49
  (0, cli_1.verboseLog)(config, `Skipping PR #${pr.number} - already reviewed since last update`);
45
50
  results.push({
@@ -51,16 +56,19 @@ async function main() {
51
56
  }
52
57
  try {
53
58
  // Get PR diff
54
- const diff = await platform.getPullRequestDiff(config.repo, pr.id);
59
+ const diff = await platform.getPullRequestDiff(repo, pr.id);
55
60
  (0, cli_1.verboseLog)(config, `Got diff for PR #${pr.number} with ${diff.length} changed files`);
56
61
  const parsedDiff = (0, parseUnifiedDiff_1.parseUnifiedDiff)(diff);
57
62
  (0, cli_1.verboseLog)(config, `Parsed diff for PR #${pr.number} with ${diff.length} changed files`);
58
63
  // Get AI review
59
64
  const comments = await model.review(parsedDiff, config);
60
65
  (0, cli_1.verboseLog)(config, `Generated ${comments.length} comments for PR #${pr.number}`);
61
- if (cliOptions.dryRun) {
66
+ if (config.dryRun) {
62
67
  // Just print comments in dry run mode
63
- console.log(`\n== Comments for PR #${pr.number}: ${pr.title} ==`);
68
+ const header = config.local
69
+ ? `\n== Review: ${pr.title} ==`
70
+ : `\n== Comments for PR #${pr.number}: ${pr.title} ==`;
71
+ console.log(header);
64
72
  comments.forEach((comment) => {
65
73
  console.log(`\n${comment.type === "inline"
66
74
  ? `${comment.path}:${comment.line}`
@@ -71,7 +79,7 @@ async function main() {
71
79
  else {
72
80
  // Post comments to PR
73
81
  for (const comment of comments) {
74
- await platform.postComment(config.repo, pr.id, comment);
82
+ await platform.postComment(repo, pr.id, comment);
75
83
  }
76
84
  console.log(`Posted ${comments.length} comments to PR #${pr.number}`);
77
85
  }
@@ -0,0 +1,74 @@
1
+ import { CodeReviewModel, FileChange, AIComment, ReviewConfig } from "../types";
2
+ /**
3
+ * Message format for LLM conversations
4
+ */
5
+ export interface LLMMessage {
6
+ role: "system" | "user" | "assistant";
7
+ content: string;
8
+ }
9
+ /**
10
+ * Abstract base class for model adapters.
11
+ * Providers share ~70% of logic (prompt gen, parsing, filtering).
12
+ * Provider-specific implementations only need to implement:
13
+ * - callLLM(): Make the actual API call to the LLM
14
+ * - supportsStructuredOutput(): Whether the provider supports JSON schema output
15
+ */
16
+ export declare abstract class BaseReviewModel implements CodeReviewModel {
17
+ /**
18
+ * Make an LLM API call with the given messages.
19
+ * Provider-specific implementations handle authentication, request formatting,
20
+ * and response extraction.
21
+ * @param messages Array of messages to send to the LLM
22
+ * @param config Review configuration
23
+ * @returns Raw response string from the LLM
24
+ */
25
+ protected abstract callLLM(messages: LLMMessage[], config: ReviewConfig): Promise<string>;
26
+ /**
27
+ * Check if this provider supports structured JSON output.
28
+ * If true, the model will request JSON schema output and parse it accordingly.
29
+ * If false, the model will request free-text output and use legacy parsing.
30
+ */
31
+ protected abstract supportsStructuredOutput(): boolean;
32
+ /**
33
+ * Get the model name/identifier for this adapter
34
+ */
35
+ protected abstract getModelName(): string;
36
+ /**
37
+ * Review code changes and generate comments.
38
+ * This is the main entry point that orchestrates the review process.
39
+ * @param diff File changes to review
40
+ * @param config Review configuration
41
+ * @returns List of AI comments
42
+ */
43
+ review(diff: FileChange[], config: ReviewConfig): Promise<AIComment[]>;
44
+ /**
45
+ * Filter out files that match ignore patterns
46
+ */
47
+ protected filterIgnoredFiles(diff: FileChange[], config: ReviewConfig): FileChange[];
48
+ /**
49
+ * Generate the system prompt for the LLM
50
+ */
51
+ protected getSystemPrompt(_config: ReviewConfig): string;
52
+ /**
53
+ * Generate a code review prompt for the given diff
54
+ * @param diff File changes to review
55
+ * @param config Review configuration
56
+ * @returns Prompt for the AI
57
+ */
58
+ protected generatePrompt(diff: FileChange[], config: ReviewConfig): string;
59
+ /**
60
+ * Parse a structured JSON response from the LLM
61
+ * @param response Raw LLM response
62
+ * @param config Review configuration
63
+ * @returns List of AI comments
64
+ */
65
+ protected parseStructuredResponse(response: string, config: ReviewConfig): AIComment[];
66
+ /**
67
+ * Parse a free-text response from the LLM (legacy mode)
68
+ * Used when structured output is not supported or parsing fails.
69
+ * @param response Raw LLM response
70
+ * @param config Review configuration
71
+ * @returns List of AI comments
72
+ */
73
+ protected parseFreeTextResponse(response: string, config: ReviewConfig): AIComment[];
74
+ }
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseReviewModel = void 0;
4
+ const review_response_1 = require("../schemas/review-response");
5
+ const validate_1 = require("../schemas/validate");
6
+ /**
7
+ * Abstract base class for model adapters.
8
+ * Providers share ~70% of logic (prompt gen, parsing, filtering).
9
+ * Provider-specific implementations only need to implement:
10
+ * - callLLM(): Make the actual API call to the LLM
11
+ * - supportsStructuredOutput(): Whether the provider supports JSON schema output
12
+ */
13
+ class BaseReviewModel {
14
+ /**
15
+ * Review code changes and generate comments.
16
+ * This is the main entry point that orchestrates the review process.
17
+ * @param diff File changes to review
18
+ * @param config Review configuration
19
+ * @returns List of AI comments
20
+ */
21
+ async review(diff, config) {
22
+ // Filter out ignored files
23
+ const filteredDiff = this.filterIgnoredFiles(diff, config);
24
+ // Skip if no files to review
25
+ if (filteredDiff.length === 0) {
26
+ return [
27
+ {
28
+ type: "summary",
29
+ content: "No files to review after applying ignore patterns.",
30
+ severity: "suggestion",
31
+ },
32
+ ];
33
+ }
34
+ const prompt = this.generatePrompt(filteredDiff, config);
35
+ const systemPrompt = this.getSystemPrompt(config);
36
+ try {
37
+ const raw = await this.callLLM([
38
+ { role: "system", content: systemPrompt },
39
+ { role: "user", content: prompt },
40
+ ], config);
41
+ return this.supportsStructuredOutput()
42
+ ? this.parseStructuredResponse(raw, config)
43
+ : this.parseFreeTextResponse(raw, config);
44
+ }
45
+ catch (error) {
46
+ console.error(`Error generating review with ${this.getModelName()}:`, error);
47
+ throw error;
48
+ }
49
+ }
50
+ /**
51
+ * Filter out files that match ignore patterns
52
+ */
53
+ filterIgnoredFiles(diff, config) {
54
+ if (!config.ignoreFiles || config.ignoreFiles.length === 0) {
55
+ return diff;
56
+ }
57
+ return diff.filter((file) => {
58
+ return !config.ignoreFiles?.some((pattern) => {
59
+ // Basic glob pattern matching for *.ext
60
+ if (pattern.startsWith("*") && pattern.indexOf(".") > 0) {
61
+ const ext = pattern.substring(1);
62
+ return file.filename.endsWith(ext);
63
+ }
64
+ return file.filename === pattern;
65
+ });
66
+ });
67
+ }
68
+ /**
69
+ * Generate the system prompt for the LLM
70
+ */
71
+ getSystemPrompt(_config) {
72
+ return "You are a senior software engineer doing a peer code review. Your job is to spot all logic, syntax, and semantic issues in a code diff. Always respond with valid JSON matching the requested schema.";
73
+ }
74
+ /**
75
+ * Generate a code review prompt for the given diff
76
+ * @param diff File changes to review
77
+ * @param config Review configuration
78
+ * @returns Prompt for the AI
79
+ */
80
+ generatePrompt(diff, config) {
81
+ const rules = config.rules && config.rules.length > 0
82
+ ? `\nApply these specific rules:\n${config.rules
83
+ .map((rule) => `- ${rule}`)
84
+ .join("\n")}`
85
+ : "";
86
+ const diffText = diff
87
+ .map((file) => {
88
+ return `File: ${file.filename} (${file.status})
89
+ ${file.patch || "No changes"}
90
+ `;
91
+ })
92
+ .join("\n\n");
93
+ return `
94
+ Review the following code changes and provide specific, actionable feedback.
95
+
96
+ Output your response as a JSON object matching this structure:
97
+ {
98
+ "summary": "Brief overall assessment (optional)",
99
+ "comments": [
100
+ {
101
+ "file": "path/to/file.ts",
102
+ "line": 42,
103
+ "severity": "critical|warning|suggestion|nitpick",
104
+ "category": "bug|security|performance|style|architecture|testing",
105
+ "confidence": 0.95,
106
+ "title": "One-line summary of the issue (max 80 chars)",
107
+ "explanation": "Detailed explanation of why this is an issue",
108
+ "suggestion": "Suggested fix or improvement (use empty string if none)"
109
+ }
110
+ ]
111
+ }
112
+
113
+ Severity levels:
114
+ - critical: Bugs, security vulnerabilities, or data loss risks
115
+ - warning: Potential issues or code smells
116
+ - suggestion: Improvements that would be nice to have
117
+ - nitpick: Minor style preferences
118
+
119
+ Categories:
120
+ - bug: Logic errors, incorrect behavior
121
+ - security: Security vulnerabilities, unsafe practices
122
+ - performance: Performance bottlenecks, inefficiencies
123
+ - style: Code style, formatting, naming
124
+ - architecture: Design patterns, code organization
125
+ - testing: Test coverage, test quality
126
+
127
+ Important rules:
128
+ - Only comment on lines that are part of the diff (added or modified)
129
+ - Do not comment on unchanged context lines unless directly impacted
130
+ - Be specific and actionable in your feedback
131
+ - Use direct, professional tone
132
+ - Include a suggestion when you can provide a concrete improvement
133
+ ${rules}
134
+
135
+ Here are the changes to review:
136
+
137
+ ${diffText}
138
+
139
+ ${config.customPrompt || ""}`;
140
+ }
141
+ /**
142
+ * Parse a structured JSON response from the LLM
143
+ * @param response Raw LLM response
144
+ * @param config Review configuration
145
+ * @returns List of AI comments
146
+ */
147
+ parseStructuredResponse(response, config) {
148
+ const comments = [];
149
+ // Check if the response looks like structured JSON
150
+ if (!(0, validate_1.looksLikeStructuredResponse)(response)) {
151
+ // Fall back to legacy parsing if it doesn't look like JSON
152
+ return this.parseFreeTextResponse(response, config);
153
+ }
154
+ const result = (0, validate_1.parseStructuredResponse)(response);
155
+ if (!result.success || !result.data) {
156
+ console.warn("Structured response parsing failed, falling back to legacy parsing:", result.error);
157
+ return this.parseFreeTextResponse(response, config);
158
+ }
159
+ const structuredResponse = result.data;
160
+ // Add summary comment if present
161
+ if (structuredResponse.summary) {
162
+ comments.push({
163
+ type: "summary",
164
+ content: structuredResponse.summary,
165
+ severity: config.severity,
166
+ });
167
+ }
168
+ // Convert structured comments to AIComment format
169
+ for (const structuredComment of structuredResponse.comments) {
170
+ // Apply severity filtering
171
+ const severityOrder = [
172
+ "critical",
173
+ "warning",
174
+ "suggestion",
175
+ "nitpick",
176
+ ];
177
+ const minSeverity = config.severity || "suggestion";
178
+ const commentSeverityIndex = severityOrder.indexOf(structuredComment.severity);
179
+ const minSeverityIndex = severityOrder.indexOf(minSeverity);
180
+ // Skip comments below the minimum severity threshold
181
+ if (commentSeverityIndex > minSeverityIndex) {
182
+ continue;
183
+ }
184
+ // Apply confidence filtering (default min: 0.6)
185
+ const minConfidence = 0.6;
186
+ if (structuredComment.confidence < minConfidence) {
187
+ continue;
188
+ }
189
+ const aiComment = (0, review_response_1.toAIComment)(structuredComment);
190
+ comments.push(aiComment);
191
+ }
192
+ return comments;
193
+ }
194
+ /**
195
+ * Parse a free-text response from the LLM (legacy mode)
196
+ * Used when structured output is not supported or parsing fails.
197
+ * @param response Raw LLM response
198
+ * @param config Review configuration
199
+ * @returns List of AI comments
200
+ */
201
+ parseFreeTextResponse(response, config) {
202
+ const comments = [];
203
+ if (config.commentStyle === "summary") {
204
+ comments.push({
205
+ type: "summary",
206
+ content: response.trim(),
207
+ severity: config.severity,
208
+ });
209
+ return comments;
210
+ }
211
+ // Look for patterns like "filename.ext:123 — comment text"
212
+ const inlineCommentRegex = /([\w/.-]+):(\d+)\s*[—–-]\s*(.*?)(?=\s+[\w/.-]+:\d+\s*[—–-]|$)/gs;
213
+ let match;
214
+ while ((match = inlineCommentRegex.exec(response + "\n\n")) !== null) {
215
+ const [, path, lineStr, content] = match;
216
+ const line = parseInt(lineStr, 10);
217
+ comments.push({
218
+ type: "inline",
219
+ path,
220
+ line,
221
+ content: content.trim(),
222
+ severity: config.severity,
223
+ });
224
+ }
225
+ // If no inline comments were parsed, create a summary comment
226
+ if (comments.length === 0) {
227
+ comments.push({
228
+ type: "summary",
229
+ content: response.trim(),
230
+ severity: config.severity,
231
+ });
232
+ }
233
+ return comments;
234
+ }
235
+ }
236
+ exports.BaseReviewModel = BaseReviewModel;
@@ -0,0 +1 @@
1
+ export {};