@krotovm/gitlab-ai-review 1.0.30 → 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",
@@ -599,6 +603,8 @@ export async function reviewMergeRequestMultiPass(params) {
599
603
  const triageMessages = buildTriagePrompt(triageInputs);
600
604
  let triageResult = null;
601
605
  let triageText = null;
606
+ let triageParseReason = null;
607
+ let triageParseError = null;
602
608
  try {
603
609
  const triageCompletion = await createCompletionWithDebug({
604
610
  openaiInstance,
@@ -614,8 +620,12 @@ export async function reviewMergeRequestMultiPass(params) {
614
620
  },
615
621
  });
616
622
  triageText = extractCompletionText(triageCompletion);
617
- if (triageText != null)
618
- triageResult = parseTriageResponse(triageText);
623
+ if (triageText != null) {
624
+ const triageParse = parseTriageResponseDetailed(triageText);
625
+ triageResult = triageParse.result;
626
+ triageParseReason = triageParse.reason;
627
+ triageParseError = triageParse.parseError;
628
+ }
619
629
  }
620
630
  catch (error) {
621
631
  logStep(`Triage pass failed: ${error?.message ?? error}. Falling back to single-pass.`);
@@ -624,7 +634,11 @@ export async function reviewMergeRequestMultiPass(params) {
624
634
  if (triageText != null) {
625
635
  const triagePreview = triageText.replace(/\s+/g, " ").trim().slice(0, 200);
626
636
  const looksLikeHtml = /<html|<!doctype html/i.test(triageText);
627
- logStep(`Triage parse failed: expected JSON but got ${looksLikeHtml ? "HTML/non-JSON" : "non-JSON"} response. Preview: ${triagePreview || "<empty>"}`);
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>"}`);
628
642
  }
629
643
  else {
630
644
  logStep("Triage parse failed: model returned empty response body.");
@@ -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;
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.30",
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": {