@krotovm/gitlab-ai-review 1.0.37 → 1.0.38

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.
@@ -827,6 +827,10 @@ export async function reviewMergeRequestMultiPass(params) {
827
827
  if (failedPaths.length === reviewFiles.length) {
828
828
  throw new Error(`All ${reviewFiles.length} per-file reviews failed (e.g. ${failedPaths[0]}). See log above.`);
829
829
  }
830
+ // Deterministic note header: title + triage summary. Prepended in code so
831
+ // every final-answer path gets it regardless of model formatting discipline.
832
+ const summaryText = triageResult.summary.trim().replace(/\s*\n\s*/g, " ");
833
+ const withHeader = (body) => `### 🤖 AI Code Review\n\n${summaryText === "" ? "" : `> ${summaryText}\n\n`}${body}`;
830
834
  logStep("Pass 3/4: consolidating findings");
831
835
  const consolidateMessages = buildConsolidatePrompt({
832
836
  perFileFindings,
@@ -836,7 +840,7 @@ export async function reviewMergeRequestMultiPass(params) {
836
840
  });
837
841
  if (consolidateMessages == null) {
838
842
  const DISCLAIMER = "This comment was generated by AI review bot.";
839
- return `${NO_FINDINGS_SENTENCE}\n\n---\n_${DISCLAIMER}_`;
843
+ return withHeader(`${NO_FINDINGS_SENTENCE}\n\n---\n_${DISCLAIMER}_`);
840
844
  }
841
845
  try {
842
846
  const consolidateCompletion = await createCompletionWithDebug({
@@ -853,7 +857,7 @@ export async function reviewMergeRequestMultiPass(params) {
853
857
  });
854
858
  const consolidatedText = extractCompletionText(consolidateCompletion);
855
859
  if (consolidatedText == null || consolidatedText.trim() === "") {
856
- return buildAnswer(consolidateCompletion);
860
+ return withHeader(buildAnswer(consolidateCompletion));
857
861
  }
858
862
  logStep("Pass 4/4: verifying consolidated findings (repo tools)");
859
863
  const verificationMessages = buildVerificationPrompt({
@@ -879,11 +883,11 @@ export async function reviewMergeRequestMultiPass(params) {
879
883
  debugDumpFile,
880
884
  debugRecordWriter,
881
885
  });
882
- return buildAnswer(verificationCompletion);
886
+ return withHeader(buildAnswer(verificationCompletion));
883
887
  }
884
888
  catch (error) {
885
889
  logStep(`Verification failed: ${error?.message ?? error}. Returning consolidated findings.`);
886
- return buildAnswer(consolidateCompletion);
890
+ return withHeader(buildAnswer(consolidateCompletion));
887
891
  }
888
892
  }
889
893
  catch (error) {
@@ -893,6 +897,6 @@ export async function reviewMergeRequestMultiPass(params) {
893
897
  .filter((f) => !isEmptyReviewBody(f.findings))
894
898
  .map((f) => f.findings)
895
899
  .join("\n");
896
- return `${raw || NO_FINDINGS_SENTENCE}\n\n---\n_${DISCLAIMER}_`;
900
+ return withHeader(`${raw || NO_FINDINGS_SENTENCE}\n\n---\n_${DISCLAIMER}_`);
897
901
  }
898
902
  }
@@ -70,11 +70,16 @@ export function normalizeReviewFindingsMarkdown(input) {
70
70
  const lines = expanded.split("\n");
71
71
  const findings = [];
72
72
  const headerRe = /^\s*(?:[-*•]\s*)?\[(high|medium)\]\s+(.+?)\s*$/i;
73
+ // Already-rendered block (see renderer below) — parsing it too keeps this
74
+ // function idempotent when a model echoes pretty findings back verbatim.
75
+ const prettyHeaderRe = /^\s*(?:[-*•]\s*)?\*\*(?:🔴|🟠)?\s*(high|medium)\s*[—–-]\s*(.+?)\*\*\s*$/i;
76
+ const matchHeader = (line) => line.match(headerRe) ?? line.match(prettyHeaderRe);
73
77
  const fileRe = /^\s*[-*]?\s*File:\s*(.+?)\s*$/i;
74
78
  const lineRe = /^\s*[-*]?\s*Line:\s*(.+?)\s*$/i;
75
79
  const whyRe = /^\s*[-*]?\s*Why:\s*(.+?)\s*$/i;
80
+ const prettyFileLineRe = /^\s*`([^`\n]+?):(~?[^`\n]*)`\s*$/;
76
81
  for (let i = 0; i < lines.length; i += 1) {
77
- const headerMatch = lines[i].match(headerRe);
82
+ const headerMatch = matchHeader(lines[i]);
78
83
  if (headerMatch == null)
79
84
  continue;
80
85
  const severity = headerMatch[1].toLowerCase();
@@ -84,9 +89,18 @@ export function normalizeReviewFindingsMarkdown(input) {
84
89
  let why = null;
85
90
  let j = i + 1;
86
91
  while (j < lines.length) {
87
- const nextHeader = lines[j].match(headerRe);
92
+ const nextHeader = matchHeader(lines[j]);
88
93
  if (nextHeader != null)
89
94
  break;
95
+ if (file == null && line == null) {
96
+ const m = lines[j].match(prettyFileLineRe);
97
+ if (m != null) {
98
+ file = m[1].trim();
99
+ line = m[2].trim();
100
+ j += 1;
101
+ continue;
102
+ }
103
+ }
90
104
  if (file == null) {
91
105
  const m = lines[j].match(fileRe);
92
106
  if (m != null) {
@@ -111,6 +125,15 @@ export function normalizeReviewFindingsMarkdown(input) {
111
125
  continue;
112
126
  }
113
127
  }
128
+ // Pretty blocks carry the why as an unlabeled line after `file:line`.
129
+ if (why == null && file != null && line != null) {
130
+ const text = lines[j].trim();
131
+ if (text !== "" && !/^(-{3,}|_)/.test(text)) {
132
+ why = text;
133
+ j += 1;
134
+ continue;
135
+ }
136
+ }
114
137
  j += 1;
115
138
  }
116
139
  if (file != null && line != null && why != null) {
@@ -120,7 +143,11 @@ export function normalizeReviewFindingsMarkdown(input) {
120
143
  }
121
144
  if (findings.length === 0)
122
145
  return normalized;
146
+ const severityLabel = {
147
+ high: "🔴 High",
148
+ medium: "🟠 Medium",
149
+ };
123
150
  return findings
124
- .map((f) => `- [${f.severity}] ${f.title} \n File: ${f.file} \n Line: ${f.line} \n Why: ${f.why}`)
151
+ .map((f) => `- **${severityLabel[f.severity]} — ${f.title}** \n \`${f.file}:${f.line}\` \n ${f.why}`)
125
152
  .join("\n\n");
126
153
  }
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.37",
4
+ "version": "1.0.38",
5
5
  "description": "CLI tool to generate AI code reviews for GitLab merge requests.",
6
6
  "main": "dist/cli.js",
7
7
  "bin": {