@krotovm/gitlab-ai-review 1.0.21 → 1.0.23
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/dist/cli/ci-review.js +1 -5
- package/dist/gitlab/services.js +11 -4
- package/dist/prompt/index.js +3 -2
- package/dist/prompt/messages.js +30 -98
- package/dist/prompt/utils.js +62 -0
- package/package.json +1 -1
package/dist/cli/ci-review.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @format */
|
|
2
2
|
import OpenAI from "openai";
|
|
3
|
-
import {
|
|
3
|
+
import { buildAnswer, buildConsolidatePrompt, buildFileReviewPrompt, buildPrompt, buildTriagePrompt, buildVerificationPrompt, extractCompletionText, parseTriageResponse, } from "../prompt/index.js";
|
|
4
4
|
import { fetchFileAtRef, searchRepository, } from "../gitlab/services.js";
|
|
5
5
|
import { logToolUsageMinimal, MAX_FILE_TOOL_ROUNDS, MAX_TOOL_ROUNDS, TOOL_NAME_GET_FILE, TOOL_NAME_GREP, } from "./tooling.js";
|
|
6
6
|
function buildReviewMetadata(changes, refs) {
|
|
@@ -219,7 +219,6 @@ export async function reviewMergeRequestWithTools(params) {
|
|
|
219
219
|
const finalCompletion = await openaiInstance.chat.completions.create({
|
|
220
220
|
model: aiModel,
|
|
221
221
|
temperature: 0.2,
|
|
222
|
-
max_tokens: AI_MAX_OUTPUT_TOKENS,
|
|
223
222
|
stream: false,
|
|
224
223
|
messages,
|
|
225
224
|
});
|
|
@@ -330,7 +329,6 @@ async function runFileReviewWithTools(params) {
|
|
|
330
329
|
const final = await openaiInstance.chat.completions.create({
|
|
331
330
|
model: aiModel,
|
|
332
331
|
temperature: 0.2,
|
|
333
|
-
max_tokens: AI_MAX_OUTPUT_TOKENS,
|
|
334
332
|
stream: false,
|
|
335
333
|
messages,
|
|
336
334
|
});
|
|
@@ -421,7 +419,6 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
421
419
|
const consolidateCompletion = await openaiInstance.chat.completions.create({
|
|
422
420
|
model: aiModel,
|
|
423
421
|
temperature: 0.1,
|
|
424
|
-
max_tokens: AI_MAX_OUTPUT_TOKENS,
|
|
425
422
|
stream: false,
|
|
426
423
|
messages: consolidateMessages,
|
|
427
424
|
});
|
|
@@ -440,7 +437,6 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
440
437
|
const verificationCompletion = await openaiInstance.chat.completions.create({
|
|
441
438
|
model: aiModel,
|
|
442
439
|
temperature: 0.0,
|
|
443
|
-
max_tokens: AI_MAX_OUTPUT_TOKENS,
|
|
444
440
|
stream: false,
|
|
445
441
|
messages: verificationMessages,
|
|
446
442
|
});
|
package/dist/gitlab/services.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @format */
|
|
2
2
|
import OpenAI from "openai";
|
|
3
|
-
import {
|
|
3
|
+
import { AI_MODEL_TEMPERATURE } from "../prompt/index.js";
|
|
4
4
|
import { GitLabError, OpenAIError, } from "./types.js";
|
|
5
5
|
export const fetchPreEditFiles = async ({ gitLabBaseUrl, headers, changesOldPaths, ref }) => {
|
|
6
6
|
const oldFilesRequestUrls = changesOldPaths.map((filePath) => {
|
|
@@ -87,7 +87,6 @@ export async function generateAICompletion(messages, openaiInstance, aiModel) {
|
|
|
87
87
|
completion = await openaiInstance.chat.completions.create({
|
|
88
88
|
model: aiModel,
|
|
89
89
|
temperature: AI_MODEL_TEMPERATURE,
|
|
90
|
-
max_tokens: AI_MAX_OUTPUT_TOKENS,
|
|
91
90
|
stream: false,
|
|
92
91
|
messages,
|
|
93
92
|
});
|
|
@@ -164,10 +163,18 @@ export const searchRepository = async ({ gitLabBaseUrl, headers, query, ref, pro
|
|
|
164
163
|
if (res instanceof Error || !res.ok) {
|
|
165
164
|
const responseDetails = await (async () => {
|
|
166
165
|
if (res instanceof Error) {
|
|
167
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
url: url.toString(),
|
|
168
|
+
error: { name: res.name, message: res.message },
|
|
169
|
+
};
|
|
168
170
|
}
|
|
169
171
|
const bodyText = await res.text().catch(() => "");
|
|
170
|
-
return {
|
|
172
|
+
return {
|
|
173
|
+
url: url.toString(),
|
|
174
|
+
status: res.status,
|
|
175
|
+
statusText: res.statusText,
|
|
176
|
+
body: bodyText.slice(0, 1000),
|
|
177
|
+
};
|
|
171
178
|
})();
|
|
172
179
|
return new GitLabError({
|
|
173
180
|
name: "SEARCH_FAILED",
|
package/dist/prompt/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @format */
|
|
2
2
|
import { buildMainSystemMessages, FILE_REVIEW_SYSTEM, TRIAGE_SYSTEM, } from "./messages.js";
|
|
3
|
-
import { sanitizeGitLabMarkdown, truncateWithMarker } from "./utils.js";
|
|
3
|
+
import { normalizeReviewFindingsMarkdown, sanitizeGitLabMarkdown, truncateWithMarker, } from "./utils.js";
|
|
4
4
|
export const DEFAULT_PROMPT_LIMITS = {
|
|
5
5
|
maxDiffs: 50,
|
|
6
6
|
maxDiffChars: 16000,
|
|
@@ -273,6 +273,7 @@ export const buildAnswer = (completion) => {
|
|
|
273
273
|
if (content === "") {
|
|
274
274
|
return `${ERROR_ANSWER}\n\nError: Model returned an empty response body. Try another model (for example, gpt-4o-mini) or a different provider endpoint.\n\n---\n_${DISCLAIMER}_`;
|
|
275
275
|
}
|
|
276
|
-
const
|
|
276
|
+
const normalizedFindings = normalizeReviewFindingsMarkdown(content);
|
|
277
|
+
const safe = sanitizeGitLabMarkdown(normalizedFindings);
|
|
277
278
|
return `${safe}\n\n---\n_${DISCLAIMER}_`;
|
|
278
279
|
};
|
package/dist/prompt/messages.js
CHANGED
|
@@ -1,66 +1,26 @@
|
|
|
1
1
|
/** @format */
|
|
2
2
|
export const MAIN_SYSTEM_LINES = [
|
|
3
|
-
"You are
|
|
4
|
-
"
|
|
3
|
+
"You are an AI code reviewer for pull requests.",
|
|
4
|
+
"Find only real bugs introduced by the diff.",
|
|
5
|
+
"Return at most 3 findings. Prefer no finding over a weak one.",
|
|
5
6
|
"",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
"",
|
|
13
|
-
"SCOPE:",
|
|
14
|
-
"- Only flag issues introduced by this diff.",
|
|
15
|
-
"- Focus on added/changed lines (`+`). Context lines (` `) and removed lines (`-`) are reference only.",
|
|
16
|
-
"- Do not comment on untouched code unless required for a proven bug path.",
|
|
17
|
-
"- If you cannot point to a concrete problematic added/changed line and explain in one sentence why it is definitely wrong, skip the finding.",
|
|
18
|
-
"",
|
|
19
|
-
"ACCURACY:",
|
|
20
|
-
"- Only report issues directly supported by diff lines and/or visible imports/exports.",
|
|
21
|
-
"- Do not invent behavior, fields, or code paths not visible in evidence.",
|
|
22
|
-
"- Removed (`-`) lines are historical; do not claim current usage based solely on them.",
|
|
23
|
-
"- If a concept is consistently renamed across files (e.g. `*Type` -> `*Percent`), do not flag missing old-name checks without explicit conflicting evidence in current (`+`) lines.",
|
|
24
|
-
"- Do not report `missing dependency` when the dependency is removed from both usage and declarations in these diffs.",
|
|
25
|
-
"- Truncated diffs may hide context. State uncertainty rather than assuming correctness or incorrectness. If tools are available, fetch the full file before reporting.",
|
|
26
|
-
"- If any part of the execution path depends on code you cannot see in the diff (truncated sections, omitted files, missing context), treat it as uncertainty and do not report a finding.",
|
|
27
|
-
"- If truncation markers indicate omitted context, assume missing context might make the change correct.",
|
|
28
|
-
"",
|
|
29
|
-
"ANTI-HALLUCINATION (mandatory — violations make the review harmful):",
|
|
30
|
-
"- SYNTAX ERRORS: Do not claim syntax errors unless you can quote the exact invalid token from the diff. Count parentheses/brackets on the actual line before reporting.",
|
|
31
|
-
"- MISSING EXPORTS/FUNCTIONS: Do not report missing functions/variables/types/exports unless there is direct usage in added lines and you can see full relevant context. If the diff is truncated or incomplete, you CANNOT claim missing.",
|
|
32
|
-
"- UNDEFINED VARIABLES: Do not claim a variable is undefined unless you verified it is absent in visible added/context lines of the same scope.",
|
|
33
|
-
"- SIGNATURE CHANGES: A function signature change is NOT a bug by itself. Only flag it if you can point to a specific caller in the diff that passes wrong arguments or uses a stale return value.",
|
|
34
|
-
"- REFACTORING: When a diff consistently renames/replaces a concept (e.g. type→percent, contributionType→contributionPercent), this is intentional refactoring. Do not flag the removal of old code or the new pattern as a bug without contradictory evidence.",
|
|
35
|
-
"- If you are not sure that something is a real bug introduced by added/changed lines, do not report it.",
|
|
36
|
-
'- When in doubt between "some issue" and "No confirmed bugs or high-value optimizations found.", choose the no-issues output.',
|
|
37
|
-
"",
|
|
38
|
-
"SELF-CHECK (before outputting any finding):",
|
|
39
|
-
"- Re-read the specific diff lines you cite. Is the evidence actually there?",
|
|
40
|
-
"- Could this be intentional or context-dependent? If yes, skip it.",
|
|
41
|
-
"",
|
|
42
|
-
"PRIORITY: (1) correctness (typo, wrong var, missing await, off-by-one), (2) security (secrets, unsafe eval, unvalidated input), (3) perf regressions (N+1 queries, unbounded loops, missing pagination).",
|
|
43
|
-
"",
|
|
44
|
-
"SEVERITY:",
|
|
45
|
-
"- [high] = deterministic runtime or security issue with a concrete execution path visible in the diff. You must be able to describe exactly what breaks and why.",
|
|
46
|
-
"- [medium] = well-supported but probabilistic issue.",
|
|
7
|
+
"Rules:",
|
|
8
|
+
"- Focus on changed lines.",
|
|
9
|
+
"- Ignore style, refactoring suggestions, and general best practices.",
|
|
10
|
+
"- If uncertain, use tools: get_file_at_ref and grep_repository.",
|
|
11
|
+
"- Report only issues clearly visible in diff or verified by tools.",
|
|
12
|
+
'- If uncertain after checking, return exactly: "No confirmed bugs or high-value optimizations found."',
|
|
47
13
|
"",
|
|
48
|
-
"
|
|
49
|
-
"-
|
|
50
|
-
"-
|
|
51
|
-
"- Do not infer typos from names not present in visible evidence.",
|
|
52
|
-
"- Pay special attention to alterations in signatures of exported functions, shared types/interfaces, and global data structures — these have the highest cross-file breakage potential.",
|
|
53
|
-
"- On multi-file diffs: verify cross-file consistency — if a signature, interface, type, or enum changes in one file, check that callers/implementers in other changed files still match.",
|
|
14
|
+
"Severity:",
|
|
15
|
+
"- [high]: deterministic runtime/security breakage with clear path.",
|
|
16
|
+
"- [medium]: likely bug with strong evidence.",
|
|
54
17
|
"",
|
|
55
|
-
"
|
|
56
|
-
"- Each finding must use this 4-line markdown block:",
|
|
18
|
+
"Output format (strict):",
|
|
57
19
|
"- `- [high|medium] <title>`",
|
|
58
20
|
"- ` File: <path>`",
|
|
59
21
|
"- ` Line: ~<N>`",
|
|
60
|
-
"- ` Why: <one concise sentence with
|
|
61
|
-
|
|
62
|
-
'- If no confirmed issues: exactly "No confirmed bugs or high-value optimizations found."',
|
|
63
|
-
"- GitLab-flavoured markdown.",
|
|
22
|
+
"- ` Why: <one concise sentence with evidence>`",
|
|
23
|
+
'- If no issues: exactly "No confirmed bugs or high-value optimizations found."',
|
|
64
24
|
];
|
|
65
25
|
export const TRIAGE_SYSTEM_LINES = [
|
|
66
26
|
"You are a senior developer triaging files in a merge request.",
|
|
@@ -79,55 +39,27 @@ export const TRIAGE_SYSTEM_LINES = [
|
|
|
79
39
|
"- Config/CI/docs files are SKIP unless they modify build targets, env vars, or secrets.",
|
|
80
40
|
];
|
|
81
41
|
export const FILE_REVIEW_SYSTEM_LINES = [
|
|
82
|
-
"You are
|
|
83
|
-
"
|
|
42
|
+
"You are an AI reviewer for a single-file diff.",
|
|
43
|
+
"Find only real bugs introduced by changed lines.",
|
|
44
|
+
"Return at most 2 findings. Prefer no finding over a weak one.",
|
|
84
45
|
"",
|
|
85
|
-
"
|
|
86
|
-
"-
|
|
87
|
-
"-
|
|
88
|
-
"-
|
|
89
|
-
"-
|
|
90
|
-
"",
|
|
91
|
-
"ACCURACY:",
|
|
92
|
-
"- Only report issues directly supported by diff lines and/or visible imports/exports.",
|
|
93
|
-
"- Do not invent behavior, fields, or code paths not visible in evidence.",
|
|
94
|
-
"- Removed (`-`) lines are historical; do not claim current usage based solely on them.",
|
|
95
|
-
"- If a concept is consistently renamed (e.g. `*Type` -> `*Percent`), do not flag missing old-name checks without conflicting evidence in current (`+`) lines.",
|
|
96
|
-
"- Do not report `missing dependency` when the dependency is removed from both usage and declarations.",
|
|
97
|
-
"- If any part of the execution path depends on code you cannot see (truncated sections/missing files), treat this as uncertainty and do not report a finding.",
|
|
98
|
-
"",
|
|
99
|
-
"ANTI-HALLUCINATION (mandatory):",
|
|
100
|
-
"- SYNTAX ERRORS: Do not claim syntax errors unless you can quote the exact invalid token. Count parentheses/brackets on the actual line.",
|
|
101
|
-
"- MISSING EXPORTS/FUNCTIONS: Do not report missing functions/variables/types/exports unless there is direct usage in added lines and full relevant context is visible. If truncated, you CANNOT claim missing.",
|
|
102
|
-
"- UNDEFINED VARIABLES: Do not claim undefined unless the variable is absent from visible lines in the same scope.",
|
|
103
|
-
"- SIGNATURE CHANGES: A changed signature is NOT a bug. Only flag if a caller in the diff passes wrong arguments.",
|
|
104
|
-
"- REFACTORING: Consistent rename/replace across files is intentional. Do not flag it without contradictory evidence.",
|
|
105
|
-
"- If you are not sure it is a real bug introduced by added/changed lines, do not report it.",
|
|
106
|
-
'- When in doubt between "some issue" and "No issues found.", choose "No issues found."',
|
|
107
|
-
"",
|
|
108
|
-
"SELF-CHECK before each finding: re-read cited lines; if interpretation depends on missing context, drop it.",
|
|
109
|
-
"",
|
|
110
|
-
"PRIORITY: (1) correctness (typo, wrong var, missing await, off-by-one), (2) security (secrets, unsafe eval, unvalidated input), (3) perf regressions (N+1 queries, unbounded loops, missing pagination).",
|
|
111
|
-
"",
|
|
112
|
-
"SEVERITY:",
|
|
113
|
-
"- [high] = deterministic runtime or security issue with a concrete execution path visible in the diff.",
|
|
114
|
-
"- [medium] = well-supported but probabilistic issue.",
|
|
46
|
+
"Rules:",
|
|
47
|
+
"- Focus on changed lines only.",
|
|
48
|
+
"- Ignore style/refactor/general improvement comments.",
|
|
49
|
+
"- If uncertain, use tools: get_file_at_ref and grep_repository.",
|
|
50
|
+
"- Report only issues clearly visible in diff or verified by tools.",
|
|
51
|
+
'- If uncertain after checking, return exactly: "No issues found."',
|
|
115
52
|
"",
|
|
116
|
-
"
|
|
117
|
-
"-
|
|
118
|
-
"-
|
|
119
|
-
"- Do not infer typos from imagined names.",
|
|
120
|
-
"- Check changes to exported function signatures, shared types, and global data structures for cross-file breakage.",
|
|
53
|
+
"Severity:",
|
|
54
|
+
"- [high]: deterministic runtime/security breakage with clear path.",
|
|
55
|
+
"- [medium]: likely bug with strong evidence.",
|
|
121
56
|
"",
|
|
122
|
-
"
|
|
123
|
-
"- Each finding must use this 4-line markdown block:",
|
|
57
|
+
"Output format (strict):",
|
|
124
58
|
"- `- [high|medium] <title>`",
|
|
125
59
|
"- ` File: <path>`",
|
|
126
60
|
"- ` Line: ~<N>`",
|
|
127
|
-
"- ` Why: <one concise sentence with
|
|
128
|
-
|
|
129
|
-
'- If no confirmed issues: exactly "No issues found."',
|
|
130
|
-
"- GitLab-flavoured markdown.",
|
|
61
|
+
"- ` Why: <one concise sentence with evidence>`",
|
|
62
|
+
'- If no issues: exactly "No issues found."',
|
|
131
63
|
];
|
|
132
64
|
export const buildMainSystemMessages = () => [
|
|
133
65
|
{
|
package/dist/prompt/utils.js
CHANGED
|
@@ -12,3 +12,65 @@ export function sanitizeGitLabMarkdown(input) {
|
|
|
12
12
|
const withClosedFence = fenceCount % 2 === 1 ? `${normalized}\n\`\`\`` : normalized;
|
|
13
13
|
return withClosedFence;
|
|
14
14
|
}
|
|
15
|
+
const NO_ISSUES_SENTENCE = "No confirmed bugs or high-value optimizations found.";
|
|
16
|
+
export function normalizeReviewFindingsMarkdown(input) {
|
|
17
|
+
const normalized = input.replace(/\r\n/g, "\n").trim();
|
|
18
|
+
if (normalized === "" || normalized === NO_ISSUES_SENTENCE)
|
|
19
|
+
return normalized;
|
|
20
|
+
const lines = normalized.split("\n");
|
|
21
|
+
const findings = [];
|
|
22
|
+
const headerRe = /^\s*-?\s*\[(high|medium)\]\s+(.+?)\s*$/i;
|
|
23
|
+
const fileRe = /^\s*[-*]?\s*File:\s*(.+?)\s*$/i;
|
|
24
|
+
const lineRe = /^\s*[-*]?\s*Line:\s*(.+?)\s*$/i;
|
|
25
|
+
const whyRe = /^\s*[-*]?\s*Why:\s*(.+?)\s*$/i;
|
|
26
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
27
|
+
const headerMatch = lines[i].match(headerRe);
|
|
28
|
+
if (headerMatch == null)
|
|
29
|
+
continue;
|
|
30
|
+
const severity = headerMatch[1].toLowerCase();
|
|
31
|
+
const title = headerMatch[2].trim();
|
|
32
|
+
let file = null;
|
|
33
|
+
let line = null;
|
|
34
|
+
let why = null;
|
|
35
|
+
let j = i + 1;
|
|
36
|
+
while (j < lines.length) {
|
|
37
|
+
const nextHeader = lines[j].match(headerRe);
|
|
38
|
+
if (nextHeader != null)
|
|
39
|
+
break;
|
|
40
|
+
if (file == null) {
|
|
41
|
+
const m = lines[j].match(fileRe);
|
|
42
|
+
if (m != null) {
|
|
43
|
+
file = m[1].trim();
|
|
44
|
+
j += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (line == null) {
|
|
49
|
+
const m = lines[j].match(lineRe);
|
|
50
|
+
if (m != null) {
|
|
51
|
+
line = m[1].trim();
|
|
52
|
+
j += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (why == null) {
|
|
57
|
+
const m = lines[j].match(whyRe);
|
|
58
|
+
if (m != null) {
|
|
59
|
+
why = m[1].trim();
|
|
60
|
+
j += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
j += 1;
|
|
65
|
+
}
|
|
66
|
+
if (file != null && line != null && why != null) {
|
|
67
|
+
findings.push({ severity, title, file, line, why });
|
|
68
|
+
i = j - 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (findings.length === 0)
|
|
72
|
+
return normalized;
|
|
73
|
+
return findings
|
|
74
|
+
.map((f) => `- [${f.severity}] ${f.title}\n File: ${f.file}\n Line: ${f.line}\n Why: ${f.why}`)
|
|
75
|
+
.join("\n\n");
|
|
76
|
+
}
|