@krotovm/gitlab-ai-review 1.0.29 → 1.0.31

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.
@@ -1,6 +1,6 @@
1
1
  /** @format */
2
2
  import OpenAI from "openai";
3
- import { buildAnswer, buildConsolidatePrompt, buildFileReviewPrompt, buildPrompt, buildTriagePrompt, buildVerificationPrompt, extractCompletionText, parseTriageResponse, } from "../prompt/index.js";
3
+ import { buildAnswer, buildConsolidatePrompt, buildFileReviewPrompt, buildPrompt, buildTriagePrompt, buildVerificationPrompt, extractCompletionText, parseTriageResponseDetailed, 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, MAX_VERIFICATION_TOOL_ROUNDS, TOOL_NAME_GET_FILE, TOOL_NAME_GREP, } from "./tooling.js";
6
6
  async function appendDebugDump(_debugDumpFile, debugRecordWriter, record) {
@@ -164,6 +164,7 @@ async function mapWithConcurrency(items, concurrency, fn) {
164
164
  export async function reviewMergeRequestWithTools(params) {
165
165
  const { openaiInstance, aiModel, promptLimits, changes, refs, gitLabProjectApiUrl, projectId, headers, forceTools, loggers, debugDumpFile, debugRecordWriter, } = params;
166
166
  const { logDebug, logStep } = loggers;
167
+ logStep(`Single-pass review started for ${changes.length} file(s) (fallback mode).`);
167
168
  const messages = buildPrompt({
168
169
  changes: changes.map((change) => ({ diff: change.diff })),
169
170
  limits: promptLimits,
@@ -217,6 +218,7 @@ export async function reviewMergeRequestWithTools(params) {
217
218
  },
218
219
  ];
219
220
  for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
221
+ logStep(`Single-pass round ${round + 1}/${MAX_TOOL_ROUNDS}: requesting model response...`);
220
222
  const completion = await createCompletionWithDebug({
221
223
  openaiInstance,
222
224
  requestLabel: `main_review_round_${round + 1}`,
@@ -236,6 +238,7 @@ export async function reviewMergeRequestWithTools(params) {
236
238
  return buildAnswer(completion);
237
239
  const toolCalls = message.tool_calls ?? [];
238
240
  logDebug(`main-review round=${round + 1} tool_calls=${toolCalls.length} finish_reason=${completion.choices[0]?.finish_reason ?? "unknown"}`);
241
+ logStep(`Single-pass round ${round + 1}: model returned ${toolCalls.length} tool call(s).`);
239
242
  if (toolCalls.length === 0)
240
243
  return buildAnswer(completion);
241
244
  messages.push({
@@ -290,6 +293,7 @@ export async function reviewMergeRequestWithTools(params) {
290
293
  role: "user",
291
294
  content: `Tool-call limit reached (${MAX_TOOL_ROUNDS}). Do not call any tools. Provide your best-effort final review now, strictly following the required output format. If confidence is low, return the exact no-issues sentence.`,
292
295
  });
296
+ logStep("Single-pass tool-call limit reached. Requesting final answer.");
293
297
  const finalCompletion = await createCompletionWithDebug({
294
298
  openaiInstance,
295
299
  requestLabel: "main_review_final_after_tool_limit",
@@ -598,6 +602,9 @@ export async function reviewMergeRequestMultiPass(params) {
598
602
  }));
599
603
  const triageMessages = buildTriagePrompt(triageInputs);
600
604
  let triageResult = null;
605
+ let triageText = null;
606
+ let triageParseReason = null;
607
+ let triageParseError = null;
601
608
  try {
602
609
  const triageCompletion = await createCompletionWithDebug({
603
610
  openaiInstance,
@@ -612,15 +619,31 @@ export async function reviewMergeRequestMultiPass(params) {
612
619
  response_format: { type: "json_object" },
613
620
  },
614
621
  });
615
- const triageText = extractCompletionText(triageCompletion);
616
- if (triageText != null)
617
- triageResult = parseTriageResponse(triageText);
622
+ triageText = extractCompletionText(triageCompletion);
623
+ if (triageText != null) {
624
+ const triageParse = parseTriageResponseDetailed(triageText);
625
+ triageResult = triageParse.result;
626
+ triageParseReason = triageParse.reason;
627
+ triageParseError = triageParse.parseError;
628
+ }
618
629
  }
619
630
  catch (error) {
620
631
  logStep(`Triage pass failed: ${error?.message ?? error}. Falling back to single-pass.`);
621
632
  }
622
633
  if (triageResult == null) {
623
- logStep("Triage parse failed. Falling back to single-pass pipeline.");
634
+ if (triageText != null) {
635
+ const triagePreview = triageText.replace(/\s+/g, " ").trim().slice(0, 200);
636
+ const looksLikeHtml = /<html|<!doctype html/i.test(triageText);
637
+ const parseReasonText = triageParseReason != null
638
+ ? `reason=${triageParseReason}`
639
+ : `reason=${looksLikeHtml ? "html_response" : "unknown_non_json"}`;
640
+ const parseErrorText = triageParseError != null ? ` parse_error=${triageParseError}` : "";
641
+ logStep(`Triage parse failed: ${parseReasonText}.${parseErrorText} Preview: ${triagePreview || "<empty>"}`);
642
+ }
643
+ else {
644
+ logStep("Triage parse failed: model returned empty response body.");
645
+ }
646
+ logStep("Falling back to single-pass pipeline.");
624
647
  return await reviewMergeRequestWithTools({
625
648
  openaiInstance,
626
649
  aiModel,
@@ -58,29 +58,48 @@ export function buildTriagePrompt(changes) {
58
58
  ];
59
59
  }
60
60
  export function parseTriageResponse(text) {
61
+ return parseTriageResponseDetailed(text).result;
62
+ }
63
+ export function parseTriageResponseDetailed(text) {
64
+ const cleaned = text
65
+ .replace(/```json?\s*/g, "")
66
+ .replace(/```\s*/g, "")
67
+ .trim();
68
+ if (cleaned === "") {
69
+ return { result: null, reason: "empty_response", parseError: null };
70
+ }
61
71
  try {
62
- const cleaned = text
63
- .replace(/```json?\s*/g, "")
64
- .replace(/```\s*/g, "")
65
- .trim();
66
72
  const parsed = JSON.parse(cleaned);
67
73
  if (typeof parsed.summary === "string" && Array.isArray(parsed.files)) {
68
74
  return {
69
- summary: parsed.summary,
70
- files: parsed.files
71
- .filter((f) => typeof f.path === "string" &&
72
- (f.verdict === "NEEDS_REVIEW" || f.verdict === "SKIP"))
73
- .map((f) => ({
74
- path: f.path,
75
- verdict: f.verdict,
76
- })),
75
+ result: {
76
+ summary: parsed.summary,
77
+ files: parsed.files
78
+ .filter((f) => typeof f.path === "string" &&
79
+ (f.verdict === "NEEDS_REVIEW" || f.verdict === "SKIP"))
80
+ .map((f) => ({
81
+ path: f.path,
82
+ verdict: f.verdict,
83
+ })),
84
+ },
85
+ reason: null,
86
+ parseError: null,
77
87
  };
78
88
  }
89
+ return { result: null, reason: "invalid_schema", parseError: null };
79
90
  }
80
- catch {
91
+ catch (error) {
81
92
  // JSON parse failure — caller will fall back to single-pass.
93
+ return {
94
+ result: null,
95
+ reason: "invalid_json",
96
+ parseError: error instanceof Error
97
+ ? error.message
98
+ : typeof error === "string"
99
+ ? error
100
+ : JSON.stringify(error),
101
+ };
82
102
  }
83
- return null;
84
103
  }
85
104
  export function buildFileReviewPrompt(params) {
86
105
  const { filePath, fileDiff, summary, otherChangedFiles, allowTools = false, } = params;
@@ -126,7 +145,7 @@ export function buildConsolidatePrompt(params) {
126
145
  ];
127
146
  }
128
147
  export function buildVerificationPrompt(params) {
129
- const { perFileFindings, summary, consolidatedFindings, maxFindings, refs, } = params;
148
+ const { perFileFindings, summary, consolidatedFindings, maxFindings, refs } = params;
130
149
  const findingsText = perFileFindings
131
150
  .map((f) => `### ${f.path}\n${f.findings}`)
132
151
  .join("\n\n");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@krotovm/gitlab-ai-review",
4
- "version": "1.0.29",
4
+ "version": "1.0.31",
5
5
  "description": "CLI tool to generate AI code reviews for GitLab merge requests.",
6
6
  "main": "dist/cli.js",
7
7
  "bin": {