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.
- package/README.md +105 -25
- package/dist/cli/index.js +14 -3
- package/dist/config/index.js +3 -3
- package/dist/config/index.test.d.ts +1 -0
- package/dist/config/index.test.js +330 -0
- package/dist/core/parseUnifiedDiff.test.d.ts +1 -0
- package/dist/core/parseUnifiedDiff.test.js +310 -0
- package/dist/index.js +17 -9
- package/dist/models/base.d.ts +74 -0
- package/dist/models/base.js +236 -0
- package/dist/models/base.test.d.ts +1 -0
- package/dist/models/base.test.js +241 -0
- package/dist/models/index.d.ts +6 -2
- package/dist/models/index.js +9 -2
- package/dist/models/ollama.d.ts +28 -0
- package/dist/models/ollama.js +88 -0
- package/dist/models/ollama.test.d.ts +1 -0
- package/dist/models/ollama.test.js +235 -0
- package/dist/models/openai.d.ts +14 -17
- package/dist/models/openai.js +41 -125
- package/dist/models/openai.test.d.ts +1 -0
- package/dist/models/openai.test.js +209 -0
- package/dist/platforms/index.d.ts +3 -2
- package/dist/platforms/index.js +8 -1
- package/dist/platforms/local.d.ts +41 -0
- package/dist/platforms/local.js +247 -0
- package/dist/schemas/review-response.d.ts +37 -0
- package/dist/schemas/review-response.js +39 -0
- package/dist/schemas/review-response.json +68 -0
- package/dist/schemas/validate.d.ts +27 -0
- package/dist/schemas/validate.js +108 -0
- package/dist/schemas/validate.test.d.ts +1 -0
- package/dist/schemas/validate.test.js +484 -0
- package/dist/types/index.d.ts +7 -2
- package/package.json +12 -3
package/dist/models/openai.d.ts
CHANGED
|
@@ -1,30 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ReviewConfig } from "../types";
|
|
2
|
+
import { BaseReviewModel, LLMMessage } from "./base";
|
|
2
3
|
/**
|
|
3
4
|
* OpenAI model adapter for code review
|
|
5
|
+
* Uses structured JSON output via response_format for reliable parsing
|
|
4
6
|
*/
|
|
5
|
-
export declare class OpenAIModel
|
|
7
|
+
export declare class OpenAIModel extends BaseReviewModel {
|
|
6
8
|
private client;
|
|
7
9
|
private model;
|
|
10
|
+
private endpoint?;
|
|
8
11
|
constructor(model: string, endpoint?: string);
|
|
9
12
|
/**
|
|
10
|
-
*
|
|
11
|
-
* @param diff File changes to review
|
|
12
|
-
* @param config Review configuration
|
|
13
|
-
* @returns Prompt for the AI
|
|
13
|
+
* Get the model name for this adapter
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
protected getModelName(): string;
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @param response AI generated response
|
|
19
|
-
* @param config Review configuration
|
|
20
|
-
* @returns List of comments
|
|
17
|
+
* OpenAI supports structured JSON output via response_format
|
|
21
18
|
*/
|
|
22
|
-
|
|
19
|
+
protected supportsStructuredOutput(): boolean;
|
|
23
20
|
/**
|
|
24
|
-
*
|
|
25
|
-
* @param
|
|
26
|
-
* @param
|
|
27
|
-
* @returns
|
|
21
|
+
* Make an LLM API call to OpenAI
|
|
22
|
+
* @param messages Array of messages to send to the LLM
|
|
23
|
+
* @param _config Review configuration (unused but kept for interface consistency)
|
|
24
|
+
* @returns Raw response string from the LLM
|
|
28
25
|
*/
|
|
29
|
-
|
|
26
|
+
protected callLLM(messages: LLMMessage[], _config: ReviewConfig): Promise<string>;
|
|
30
27
|
}
|
package/dist/models/openai.js
CHANGED
|
@@ -5,11 +5,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.OpenAIModel = void 0;
|
|
7
7
|
const openai_1 = __importDefault(require("openai"));
|
|
8
|
+
const base_1 = require("./base");
|
|
9
|
+
// Import the JSON schema for OpenAI
|
|
10
|
+
const review_response_json_1 = __importDefault(require("../schemas/review-response.json"));
|
|
8
11
|
/**
|
|
9
12
|
* OpenAI model adapter for code review
|
|
13
|
+
* Uses structured JSON output via response_format for reliable parsing
|
|
10
14
|
*/
|
|
11
|
-
class OpenAIModel {
|
|
15
|
+
class OpenAIModel extends base_1.BaseReviewModel {
|
|
12
16
|
constructor(model, endpoint) {
|
|
17
|
+
super();
|
|
13
18
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
14
19
|
if (!apiKey) {
|
|
15
20
|
throw new Error("OPENAI_API_KEY environment variable is required");
|
|
@@ -18,145 +23,56 @@ class OpenAIModel {
|
|
|
18
23
|
throw new Error("Model is required");
|
|
19
24
|
}
|
|
20
25
|
this.model = model;
|
|
26
|
+
this.endpoint = endpoint;
|
|
21
27
|
this.client = new openai_1.default({
|
|
22
28
|
apiKey,
|
|
23
29
|
baseURL: endpoint,
|
|
24
30
|
});
|
|
25
31
|
}
|
|
26
32
|
/**
|
|
27
|
-
*
|
|
28
|
-
* @param diff File changes to review
|
|
29
|
-
* @param config Review configuration
|
|
30
|
-
* @returns Prompt for the AI
|
|
33
|
+
* Get the model name for this adapter
|
|
31
34
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
? `\nApply these specific rules:\n${config.rules
|
|
35
|
-
.map((rule) => `- ${rule}`)
|
|
36
|
-
.join("\n")}`
|
|
37
|
-
: "";
|
|
38
|
-
const diffText = diff
|
|
39
|
-
.map((file) => {
|
|
40
|
-
return `File: ${file.filename} (${file.status})
|
|
41
|
-
${file.patch || "No changes"}
|
|
42
|
-
`;
|
|
43
|
-
})
|
|
44
|
-
.join("\n\n");
|
|
45
|
-
return `
|
|
46
|
-
Only provide brief, actionable, file-specific feedback related to the actual code diff.
|
|
47
|
-
Do not include general advice, documentation-style summaries, or best practices unless they directly relate to the diff.
|
|
48
|
-
Use a direct tone. No greetings. No summaries. No repeated advice.
|
|
49
|
-
|
|
50
|
-
Important formatting rules:
|
|
51
|
-
- Do not comment on lines that are unchanged or just context unless it's directly impacted by a change.
|
|
52
|
-
${config.commentStyle === "inline"
|
|
53
|
-
? "- Output only inline comments, Use this format: 'filename.py:<line number in the new file> — comment'"
|
|
54
|
-
: "- Provide a single short summary of your review."}
|
|
55
|
-
|
|
56
|
-
${rules}
|
|
57
|
-
|
|
58
|
-
Here are the changes to review:
|
|
59
|
-
|
|
60
|
-
${diffText}
|
|
61
|
-
|
|
62
|
-
${config.customPrompt || ""}`;
|
|
35
|
+
getModelName() {
|
|
36
|
+
return `OpenAI (${this.model})`;
|
|
63
37
|
}
|
|
64
38
|
/**
|
|
65
|
-
*
|
|
66
|
-
* @param response AI generated response
|
|
67
|
-
* @param config Review configuration
|
|
68
|
-
* @returns List of comments
|
|
39
|
+
* OpenAI supports structured JSON output via response_format
|
|
69
40
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (config.commentStyle === "summary") {
|
|
73
|
-
// Generate a single summary comment
|
|
74
|
-
comments.push({
|
|
75
|
-
type: "summary",
|
|
76
|
-
content: response.trim(),
|
|
77
|
-
severity: config.severity,
|
|
78
|
-
});
|
|
79
|
-
return comments;
|
|
80
|
-
}
|
|
81
|
-
// Look for patterns like "filename.ext:123 — comment text"
|
|
82
|
-
const inlineCommentRegex = /([\w/.-]+):(\d+)\s*[—–-]\s*(.*?)(?=\s+[\w/.-]+:\d+\s*[—–-]|$)/gs;
|
|
83
|
-
let match;
|
|
84
|
-
while ((match = inlineCommentRegex.exec(response + "\n\n")) !== null) {
|
|
85
|
-
const [, path, lineStr, content] = match;
|
|
86
|
-
const line = parseInt(lineStr, 10);
|
|
87
|
-
comments.push({
|
|
88
|
-
type: "inline",
|
|
89
|
-
path,
|
|
90
|
-
line,
|
|
91
|
-
content: content.trim(),
|
|
92
|
-
severity: config.severity,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
// If no inline comments were parsed, create a summary comment
|
|
96
|
-
if (comments.length === 0) {
|
|
97
|
-
comments.push({
|
|
98
|
-
type: "summary",
|
|
99
|
-
content: response.trim(),
|
|
100
|
-
severity: config.severity,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
return comments;
|
|
41
|
+
supportsStructuredOutput() {
|
|
42
|
+
return true;
|
|
104
43
|
}
|
|
105
44
|
/**
|
|
106
|
-
*
|
|
107
|
-
* @param
|
|
108
|
-
* @param
|
|
109
|
-
* @returns
|
|
45
|
+
* Make an LLM API call to OpenAI
|
|
46
|
+
* @param messages Array of messages to send to the LLM
|
|
47
|
+
* @param _config Review configuration (unused but kept for interface consistency)
|
|
48
|
+
* @returns Raw response string from the LLM
|
|
110
49
|
*/
|
|
111
|
-
async
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
type: "summary",
|
|
130
|
-
content: "No files to review after applying ignore patterns.",
|
|
131
|
-
severity: "suggestion",
|
|
50
|
+
async callLLM(messages, _config) {
|
|
51
|
+
// Use structured output with JSON schema
|
|
52
|
+
const response = await this.client.chat.completions.create({
|
|
53
|
+
model: this.model,
|
|
54
|
+
messages: messages.map((m) => ({
|
|
55
|
+
role: m.role,
|
|
56
|
+
content: m.content,
|
|
57
|
+
})),
|
|
58
|
+
temperature: 0.1,
|
|
59
|
+
max_tokens: 4000,
|
|
60
|
+
store: true,
|
|
61
|
+
response_format: {
|
|
62
|
+
type: "json_schema",
|
|
63
|
+
json_schema: {
|
|
64
|
+
name: "review_response",
|
|
65
|
+
description: "Structured code review response",
|
|
66
|
+
schema: review_response_json_1.default,
|
|
67
|
+
strict: true,
|
|
132
68
|
},
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
model: this.model,
|
|
139
|
-
messages: [
|
|
140
|
-
{
|
|
141
|
-
role: "system",
|
|
142
|
-
content: "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.",
|
|
143
|
-
},
|
|
144
|
-
{ role: "user", content: prompt },
|
|
145
|
-
],
|
|
146
|
-
temperature: 0.1,
|
|
147
|
-
max_tokens: 4000,
|
|
148
|
-
store: true,
|
|
149
|
-
});
|
|
150
|
-
const content = response.choices[0]?.message.content;
|
|
151
|
-
if (!content) {
|
|
152
|
-
throw new Error("No response from OpenAI");
|
|
153
|
-
}
|
|
154
|
-
return this.parseResponse(content, config);
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
console.error("Error generating review with OpenAI:", error);
|
|
158
|
-
throw error;
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const content = response.choices[0]?.message.content;
|
|
72
|
+
if (!content) {
|
|
73
|
+
throw new Error("No response from OpenAI");
|
|
159
74
|
}
|
|
75
|
+
return content;
|
|
160
76
|
}
|
|
161
77
|
}
|
|
162
78
|
exports.OpenAIModel = OpenAIModel;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
// Test the response parsing function directly
|
|
5
|
+
// We need to extract the parseResponse function for testing
|
|
6
|
+
// Re-implement the parsing logic for testing (same as in openai.ts)
|
|
7
|
+
function parseResponse(response, commentStyle, severity) {
|
|
8
|
+
const comments = [];
|
|
9
|
+
if (commentStyle === "summary") {
|
|
10
|
+
comments.push({
|
|
11
|
+
type: "summary",
|
|
12
|
+
content: response.trim(),
|
|
13
|
+
severity,
|
|
14
|
+
});
|
|
15
|
+
return comments;
|
|
16
|
+
}
|
|
17
|
+
// Look for patterns like "filename.ext:123 — comment text"
|
|
18
|
+
const inlineCommentRegex = /([\w/.-]+):(\d+)\s*[—–-]\s*(.*?)(?=\s+[\w/.-]+:\d+\s*[—–-]|$)/gs;
|
|
19
|
+
let match;
|
|
20
|
+
while ((match = inlineCommentRegex.exec(response + "\n\n")) !== null) {
|
|
21
|
+
const [, path, lineStr, content] = match;
|
|
22
|
+
const line = parseInt(lineStr, 10);
|
|
23
|
+
comments.push({
|
|
24
|
+
type: "inline",
|
|
25
|
+
path,
|
|
26
|
+
line,
|
|
27
|
+
content: content.trim(),
|
|
28
|
+
severity,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// If no inline comments were parsed, create a summary comment
|
|
32
|
+
if (comments.length === 0) {
|
|
33
|
+
comments.push({
|
|
34
|
+
type: "summary",
|
|
35
|
+
content: response.trim(),
|
|
36
|
+
severity,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return comments;
|
|
40
|
+
}
|
|
41
|
+
(0, vitest_1.describe)("OpenAI Response Parsing", () => {
|
|
42
|
+
const severity = "suggestion";
|
|
43
|
+
(0, vitest_1.describe)("summary style", () => {
|
|
44
|
+
(0, vitest_1.it)("should return summary comment when commentStyle is summary", () => {
|
|
45
|
+
const summaryContent = "This is a summary of the changes.";
|
|
46
|
+
const result = parseResponse(summaryContent, "summary", severity);
|
|
47
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
48
|
+
(0, vitest_1.expect)(result[0]).toEqual({
|
|
49
|
+
type: "summary",
|
|
50
|
+
content: summaryContent,
|
|
51
|
+
severity: "suggestion",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.describe)("inline style", () => {
|
|
56
|
+
(0, vitest_1.it)("should parse inline comments correctly", () => {
|
|
57
|
+
const response = `src/utils.ts:3 — Missing null check on the user object
|
|
58
|
+
src/utils.ts:8 — Unused import 'fs'
|
|
59
|
+
src/utils.ts:15 — Consider using const instead of let`;
|
|
60
|
+
const result = parseResponse(response, "inline", severity);
|
|
61
|
+
(0, vitest_1.expect)(result).toHaveLength(3);
|
|
62
|
+
(0, vitest_1.expect)(result[0]).toEqual({
|
|
63
|
+
type: "inline",
|
|
64
|
+
path: "src/utils.ts",
|
|
65
|
+
line: 3,
|
|
66
|
+
content: "Missing null check on the user object",
|
|
67
|
+
severity: "suggestion",
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.expect)(result[1]).toEqual({
|
|
70
|
+
type: "inline",
|
|
71
|
+
path: "src/utils.ts",
|
|
72
|
+
line: 8,
|
|
73
|
+
content: "Unused import 'fs'",
|
|
74
|
+
severity: "suggestion",
|
|
75
|
+
});
|
|
76
|
+
(0, vitest_1.expect)(result[2]).toEqual({
|
|
77
|
+
type: "inline",
|
|
78
|
+
path: "src/utils.ts",
|
|
79
|
+
line: 15,
|
|
80
|
+
content: "Consider using const instead of let",
|
|
81
|
+
severity: "suggestion",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)("should handle em-dash separator in inline comments", () => {
|
|
85
|
+
const response = `src/file.ts:10 — Comment with em-dash`;
|
|
86
|
+
const result = parseResponse(response, "inline", severity);
|
|
87
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
88
|
+
(0, vitest_1.expect)(result[0].line).toBe(10);
|
|
89
|
+
(0, vitest_1.expect)(result[0].content).toBe("Comment with em-dash");
|
|
90
|
+
});
|
|
91
|
+
(0, vitest_1.it)("should handle en-dash separator in inline comments", () => {
|
|
92
|
+
const response = `src/file.ts:5 – Comment with en-dash`;
|
|
93
|
+
const result = parseResponse(response, "inline", severity);
|
|
94
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
95
|
+
(0, vitest_1.expect)(result[0].line).toBe(5);
|
|
96
|
+
});
|
|
97
|
+
(0, vitest_1.it)("should handle hyphen separator in inline comments", () => {
|
|
98
|
+
const response = `src/file.ts:5 - Comment with hyphen`;
|
|
99
|
+
const result = parseResponse(response, "inline", severity);
|
|
100
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
101
|
+
(0, vitest_1.expect)(result[0].line).toBe(5);
|
|
102
|
+
});
|
|
103
|
+
(0, vitest_1.it)("should fall back to summary if no inline comments are parsed", () => {
|
|
104
|
+
const response = "This is just a general review comment without specific line numbers.";
|
|
105
|
+
const result = parseResponse(response, "inline", severity);
|
|
106
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
107
|
+
(0, vitest_1.expect)(result[0].type).toBe("summary");
|
|
108
|
+
(0, vitest_1.expect)(result[0].content).toBe(response);
|
|
109
|
+
});
|
|
110
|
+
(0, vitest_1.it)("should handle comments with colons in content", () => {
|
|
111
|
+
const response = `src/file.ts:10 — Warning: this is a warning with: multiple colons`;
|
|
112
|
+
const result = parseResponse(response, "inline", severity);
|
|
113
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
114
|
+
(0, vitest_1.expect)(result[0].line).toBe(10);
|
|
115
|
+
(0, vitest_1.expect)(result[0].content).toContain("Warning:");
|
|
116
|
+
(0, vitest_1.expect)(result[0].content).toContain("multiple colons");
|
|
117
|
+
});
|
|
118
|
+
(0, vitest_1.it)("should handle file paths with dots and dashes", () => {
|
|
119
|
+
const response = `src/my-component.test.ts:42 — Test comment`;
|
|
120
|
+
const result = parseResponse(response, "inline", severity);
|
|
121
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
122
|
+
(0, vitest_1.expect)(result[0].path).toBe("src/my-component.test.ts");
|
|
123
|
+
(0, vitest_1.expect)(result[0].line).toBe(42);
|
|
124
|
+
});
|
|
125
|
+
(0, vitest_1.it)("should handle file paths with nested directories", () => {
|
|
126
|
+
const response = `src/components/user/profile/settings.ts:100 — Deeply nested file`;
|
|
127
|
+
const result = parseResponse(response, "inline", severity);
|
|
128
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
129
|
+
(0, vitest_1.expect)(result[0].path).toBe("src/components/user/profile/settings.ts");
|
|
130
|
+
(0, vitest_1.expect)(result[0].line).toBe(100);
|
|
131
|
+
});
|
|
132
|
+
(0, vitest_1.it)("should handle comments at line 1", () => {
|
|
133
|
+
const response = `src/file.ts:1 — Comment on first line`;
|
|
134
|
+
const result = parseResponse(response, "inline", severity);
|
|
135
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
136
|
+
(0, vitest_1.expect)(result[0].line).toBe(1);
|
|
137
|
+
});
|
|
138
|
+
(0, vitest_1.it)("should handle comments at high line numbers", () => {
|
|
139
|
+
const response = `src/file.ts:9999 — Comment on high line number`;
|
|
140
|
+
const result = parseResponse(response, "inline", severity);
|
|
141
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
142
|
+
(0, vitest_1.expect)(result[0].line).toBe(9999);
|
|
143
|
+
});
|
|
144
|
+
(0, vitest_1.it)("should handle empty content", () => {
|
|
145
|
+
const response = "";
|
|
146
|
+
const result = parseResponse(response, "inline", severity);
|
|
147
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
148
|
+
(0, vitest_1.expect)(result[0].type).toBe("summary");
|
|
149
|
+
(0, vitest_1.expect)(result[0].content).toBe("");
|
|
150
|
+
});
|
|
151
|
+
(0, vitest_1.it)("should handle whitespace-only content", () => {
|
|
152
|
+
const response = " \n\n ";
|
|
153
|
+
const result = parseResponse(response, "inline", severity);
|
|
154
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
155
|
+
(0, vitest_1.expect)(result[0].type).toBe("summary");
|
|
156
|
+
});
|
|
157
|
+
(0, vitest_1.it)("should handle multiple files in same response", () => {
|
|
158
|
+
const response = `src/utils.ts:5 — First file comment
|
|
159
|
+
src/helpers.ts:10 — Second file comment
|
|
160
|
+
src/constants.ts:1 — Third file comment`;
|
|
161
|
+
const result = parseResponse(response, "inline", severity);
|
|
162
|
+
(0, vitest_1.expect)(result).toHaveLength(3);
|
|
163
|
+
(0, vitest_1.expect)(result[0].path).toBe("src/utils.ts");
|
|
164
|
+
(0, vitest_1.expect)(result[1].path).toBe("src/helpers.ts");
|
|
165
|
+
(0, vitest_1.expect)(result[2].path).toBe("src/constants.ts");
|
|
166
|
+
});
|
|
167
|
+
(0, vitest_1.it)("should trim whitespace from comment content", () => {
|
|
168
|
+
const response = `src/file.ts:10 — Comment with extra spaces `;
|
|
169
|
+
const result = parseResponse(response, "inline", severity);
|
|
170
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
171
|
+
(0, vitest_1.expect)(result[0].content).toBe("Comment with extra spaces");
|
|
172
|
+
});
|
|
173
|
+
(0, vitest_1.it)("should handle comments with quotes and special characters", () => {
|
|
174
|
+
const response = `src/file.ts:10 — This has "quotes" and 'apostrophes' and \`backticks\``;
|
|
175
|
+
const result = parseResponse(response, "inline", severity);
|
|
176
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
177
|
+
(0, vitest_1.expect)(result[0].content).toContain('"quotes"');
|
|
178
|
+
(0, vitest_1.expect)(result[0].content).toContain("'apostrophes'");
|
|
179
|
+
(0, vitest_1.expect)(result[0].content).toContain("`backticks`");
|
|
180
|
+
});
|
|
181
|
+
(0, vitest_1.it)("should handle comments with parentheses and brackets", () => {
|
|
182
|
+
const response = `src/file.ts:10 — Check the array[index] and call function(arg1, arg2)`;
|
|
183
|
+
const result = parseResponse(response, "inline", severity);
|
|
184
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
185
|
+
(0, vitest_1.expect)(result[0].content).toContain("array[index]");
|
|
186
|
+
(0, vitest_1.expect)(result[0].content).toContain("function(arg1, arg2)");
|
|
187
|
+
});
|
|
188
|
+
(0, vitest_1.it)("should handle comments with URL-like paths", () => {
|
|
189
|
+
const response = `src/file.ts:10 — See https://example.com/docs for more info`;
|
|
190
|
+
const result = parseResponse(response, "inline", severity);
|
|
191
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
192
|
+
(0, vitest_1.expect)(result[0].content).toContain("https://example.com/docs");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
(0, vitest_1.describe)("OpenAIModel constructor validation", () => {
|
|
197
|
+
// We need to dynamically import the OpenAIModel to avoid module-level side effects
|
|
198
|
+
(0, vitest_1.it)("should throw error if OPENAI_API_KEY is not set", async () => {
|
|
199
|
+
delete process.env.OPENAI_API_KEY;
|
|
200
|
+
const { OpenAIModel } = await import("./openai.js");
|
|
201
|
+
(0, vitest_1.expect)(() => new OpenAIModel("gpt-4o")).toThrow("OPENAI_API_KEY environment variable is required");
|
|
202
|
+
});
|
|
203
|
+
(0, vitest_1.it)("should throw error if model is not provided", async () => {
|
|
204
|
+
process.env.OPENAI_API_KEY = "test-key";
|
|
205
|
+
const { OpenAIModel } = await import("./openai.js");
|
|
206
|
+
(0, vitest_1.expect)(() => new OpenAIModel("")).toThrow("Model is required");
|
|
207
|
+
delete process.env.OPENAI_API_KEY;
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Platform, CodeReviewPlatform } from "../types";
|
|
1
|
+
import { Platform, CodeReviewPlatform, ReviewConfig } from "../types";
|
|
2
2
|
/**
|
|
3
3
|
* Get platform adapter for the specified platform
|
|
4
4
|
* @param platform Platform to use
|
|
5
|
+
* @param config Full review config (needed for local platform options)
|
|
5
6
|
* @returns Platform adapter instance
|
|
6
7
|
*/
|
|
7
|
-
export declare function getPlatform(platform: Platform): Promise<CodeReviewPlatform>;
|
|
8
|
+
export declare function getPlatform(platform: Platform, config?: ReviewConfig): Promise<CodeReviewPlatform>;
|
package/dist/platforms/index.js
CHANGED
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getPlatform = getPlatform;
|
|
4
4
|
const github_1 = require("./github");
|
|
5
|
+
const local_1 = require("./local");
|
|
5
6
|
/**
|
|
6
7
|
* Get platform adapter for the specified platform
|
|
7
8
|
* @param platform Platform to use
|
|
9
|
+
* @param config Full review config (needed for local platform options)
|
|
8
10
|
* @returns Platform adapter instance
|
|
9
11
|
*/
|
|
10
|
-
async function getPlatform(platform) {
|
|
12
|
+
async function getPlatform(platform, config) {
|
|
11
13
|
switch (platform) {
|
|
12
14
|
case "github":
|
|
13
15
|
return await github_1.GithubPlatform.init();
|
|
16
|
+
case "local":
|
|
17
|
+
if (!config) {
|
|
18
|
+
throw new Error("ReviewConfig is required for local platform");
|
|
19
|
+
}
|
|
20
|
+
return await local_1.LocalPlatform.create(config);
|
|
14
21
|
default:
|
|
15
22
|
throw new Error(`Unsupported platform: ${platform}`);
|
|
16
23
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CodeReviewPlatform, PullRequest, FileChange, AIComment, ReviewConfig } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Local platform adapter — reviews local git diffs without any remote API calls.
|
|
4
|
+
* Always operates in dry-run mode (output to stdout).
|
|
5
|
+
*/
|
|
6
|
+
export declare class LocalPlatform implements CodeReviewPlatform {
|
|
7
|
+
private repoPath;
|
|
8
|
+
private base;
|
|
9
|
+
private head;
|
|
10
|
+
private patchFile?;
|
|
11
|
+
constructor(config: ReviewConfig);
|
|
12
|
+
static create(config: ReviewConfig): Promise<LocalPlatform>;
|
|
13
|
+
/**
|
|
14
|
+
* Determine a sensible default base ref:
|
|
15
|
+
* 1. If current branch has an upstream, use the merge base with it
|
|
16
|
+
* 2. Otherwise, use HEAD~1
|
|
17
|
+
*/
|
|
18
|
+
private resolveDefaultBase;
|
|
19
|
+
private validateRef;
|
|
20
|
+
/**
|
|
21
|
+
* Returns a single synthetic PullRequest from local git metadata.
|
|
22
|
+
*/
|
|
23
|
+
getPullRequests(_repo: string): Promise<PullRequest[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Get file changes by running `git diff` or reading a patch file.
|
|
26
|
+
*/
|
|
27
|
+
getPullRequestDiff(_repo: string, _prId: string | number): Promise<FileChange[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Parse raw `git diff` output into FileChange objects.
|
|
30
|
+
* This parses the full unified diff format including file headers.
|
|
31
|
+
*/
|
|
32
|
+
private parseGitDiff;
|
|
33
|
+
/**
|
|
34
|
+
* In local mode, "posting" a comment means printing to stdout.
|
|
35
|
+
*/
|
|
36
|
+
postComment(_repo: string, _prId: string | number, _comment: AIComment): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Always returns false — no duplicate tracking in local mode.
|
|
39
|
+
*/
|
|
40
|
+
hasAICommented(_repo: string, _prId: string | number): Promise<boolean>;
|
|
41
|
+
}
|