@krotovm/gitlab-ai-review 1.0.38 → 1.0.39
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 +23 -5
- package/dist/cli/debug-artifacts-html.js +171 -57
- package/dist/prompt/utils.js +10 -3
- package/package.json +1 -1
package/dist/cli/ci-review.js
CHANGED
|
@@ -7,6 +7,9 @@ import { logToolUsageMinimal, MAX_FILE_TOOL_ROUNDS, MAX_TOOL_ROUNDS, MAX_VERIFIC
|
|
|
7
7
|
/** Hard deadline per model call, retries included. Must exceed the gateway's own
|
|
8
8
|
* proxy timeout so the SDK's 5xx retries stay the first line of defense. */
|
|
9
9
|
const COMPLETION_TIMEOUT_MS = 180_000;
|
|
10
|
+
/** Whether to request token usage in streamed completions. Flipped off for the
|
|
11
|
+
* rest of the run the first time a gateway rejects stream_options with a 400. */
|
|
12
|
+
let streamUsageSupported = true;
|
|
10
13
|
/** Tool-result caps. Every tool response is re-sent as prompt on each following
|
|
11
14
|
* round, so oversized results multiply prefill time on self-hosted models. */
|
|
12
15
|
const MAX_TOOL_FILE_CHARS = 12_000;
|
|
@@ -74,12 +77,27 @@ async function createCompletionWithDebug(params) {
|
|
|
74
77
|
// The SDK's own timeout only covers time-to-headers; a stalled SSE body would hang
|
|
75
78
|
// forever without the explicit AbortSignal deadline.
|
|
76
79
|
const { stream: _stream, ...createParams } = request;
|
|
80
|
+
const attempt = (withUsage) => openaiInstance.chat.completions
|
|
81
|
+
.stream(withUsage
|
|
82
|
+
? { ...createParams, stream_options: { include_usage: true } }
|
|
83
|
+
: createParams, { signal: AbortSignal.timeout(COMPLETION_TIMEOUT_MS) })
|
|
84
|
+
.finalChatCompletion();
|
|
77
85
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
86
|
+
let completion;
|
|
87
|
+
try {
|
|
88
|
+
completion = await attempt(streamUsageSupported);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
// Strict OpenAI-compatible gateways 400 on unknown params. Drop the
|
|
92
|
+
// usage request for the rest of the run and retry this call once.
|
|
93
|
+
const rejectedStreamOptions = streamUsageSupported &&
|
|
94
|
+
error?.status === 400 &&
|
|
95
|
+
String(error?.message ?? "").includes("stream_options");
|
|
96
|
+
if (!rejectedStreamOptions)
|
|
97
|
+
throw error;
|
|
98
|
+
streamUsageSupported = false;
|
|
99
|
+
completion = await attempt(false);
|
|
100
|
+
}
|
|
83
101
|
await appendDebugDump(debugDumpFile, debugRecordWriter, {
|
|
84
102
|
kind: "openai_response",
|
|
85
103
|
label: requestLabel,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** @format */
|
|
2
2
|
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { parseReviewFindings } from "../prompt/utils.js";
|
|
3
4
|
function escapeHtml(value) {
|
|
4
5
|
return value
|
|
5
6
|
.replaceAll("&", "&")
|
|
@@ -11,31 +12,94 @@ function escapeHtml(value) {
|
|
|
11
12
|
export async function renderDebugArtifactsHtml(params) {
|
|
12
13
|
const { records, artifactHtmlFile, cliVersion, aiModel } = params;
|
|
13
14
|
const responses = records.filter((r) => r.kind === "openai_response");
|
|
15
|
+
const requests = records.filter((r) => r.kind === "openai_request");
|
|
16
|
+
const errors = records.filter((r) => r.kind === "openai_error");
|
|
14
17
|
const byLabel = new Map();
|
|
15
18
|
for (const response of responses) {
|
|
16
19
|
if (typeof response.label === "string")
|
|
17
20
|
byLabel.set(response.label, response);
|
|
18
21
|
}
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const requestTsByLabel = new Map();
|
|
23
|
+
for (const request of requests) {
|
|
24
|
+
if (typeof request.label === "string" && !requestTsByLabel.has(request.label))
|
|
25
|
+
requestTsByLabel.set(request.label, String(request.ts ?? ""));
|
|
26
|
+
}
|
|
27
|
+
function usageOf(record) {
|
|
28
|
+
const u = record?.response?.usage;
|
|
29
|
+
if (u == null)
|
|
30
|
+
return null;
|
|
31
|
+
return {
|
|
32
|
+
prompt: Number(u.prompt_tokens ?? 0),
|
|
33
|
+
completion: Number(u.completion_tokens ?? 0),
|
|
34
|
+
total: Number(u.total_tokens ?? 0),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function sumUsage(labels) {
|
|
38
|
+
const usage = { prompt: 0, completion: 0, total: 0 };
|
|
39
|
+
let reported = 0;
|
|
40
|
+
for (const label of labels) {
|
|
41
|
+
const u = usageOf(byLabel.get(label));
|
|
42
|
+
if (u == null)
|
|
43
|
+
continue;
|
|
44
|
+
reported += 1;
|
|
45
|
+
usage.prompt += u.prompt;
|
|
46
|
+
usage.completion += u.completion;
|
|
47
|
+
usage.total += u.total;
|
|
48
|
+
}
|
|
49
|
+
return { usage, reported };
|
|
50
|
+
}
|
|
51
|
+
function tokensText(labels) {
|
|
52
|
+
const { usage, reported } = sumUsage(labels);
|
|
53
|
+
if (reported === 0)
|
|
54
|
+
return "tokens: not reported by backend";
|
|
55
|
+
return `prompt: ${usage.prompt.toLocaleString()} • completion: ${usage.completion.toLocaleString()} • total: ${usage.total.toLocaleString()}`;
|
|
56
|
+
}
|
|
57
|
+
function parseTs(value) {
|
|
58
|
+
const ms = Date.parse(String(value ?? ""));
|
|
59
|
+
return Number.isFinite(ms) ? ms : null;
|
|
60
|
+
}
|
|
61
|
+
function durationText(labels) {
|
|
62
|
+
const starts = labels
|
|
63
|
+
.map((label) => parseTs(requestTsByLabel.get(label)))
|
|
64
|
+
.filter((n) => n != null);
|
|
65
|
+
const ends = labels
|
|
66
|
+
.map((label) => parseTs(byLabel.get(label)?.ts))
|
|
67
|
+
.filter((n) => n != null);
|
|
68
|
+
if (starts.length === 0 || ends.length === 0)
|
|
69
|
+
return "";
|
|
70
|
+
return `${((Math.max(...ends) - Math.min(...starts)) / 1000).toFixed(1)}s`;
|
|
71
|
+
}
|
|
72
|
+
const allLabels = Array.from(byLabel.keys());
|
|
73
|
+
const triageLabels = allLabels.filter((l) => l.startsWith("triage_pass"));
|
|
74
|
+
const consolidateLabels = allLabels.filter((l) => l === "consolidate_pass");
|
|
75
|
+
const verificationLabels = allLabels.filter((l) => l.startsWith("verification_pass"));
|
|
76
|
+
const mainLabels = allLabels.filter((l) => l.startsWith("main_review"));
|
|
77
|
+
// Per-file review request groups, in first-seen (chronological) order.
|
|
78
|
+
const fileGroups = new Map();
|
|
79
|
+
const fileLabelRe = /^file_review_(.+?)_(?:round_\d+|final_after_tool_limit)$/;
|
|
80
|
+
for (const response of responses) {
|
|
81
|
+
const label = typeof response.label === "string" ? response.label : "";
|
|
82
|
+
const match = label.match(fileLabelRe);
|
|
83
|
+
if (match == null)
|
|
84
|
+
continue;
|
|
85
|
+
const group = fileGroups.get(match[1]) ?? [];
|
|
86
|
+
group.push(label);
|
|
87
|
+
fileGroups.set(match[1], group);
|
|
88
|
+
}
|
|
27
89
|
function getContent(label) {
|
|
28
90
|
const content = byLabel.get(label)?.response?.choices?.[0]?.message?.content;
|
|
29
91
|
return typeof content === "string" ? content : "";
|
|
30
92
|
}
|
|
31
|
-
function
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
93
|
+
function lastContent(labels) {
|
|
94
|
+
const finalLabel = labels.find((l) => l.endsWith("_final_after_tool_limit"));
|
|
95
|
+
if (finalLabel != null && getContent(finalLabel).trim() !== "")
|
|
96
|
+
return getContent(finalLabel);
|
|
97
|
+
for (let i = labels.length - 1; i >= 0; i -= 1) {
|
|
98
|
+
const content = getContent(labels[i]);
|
|
99
|
+
if (content.trim() !== "")
|
|
100
|
+
return content;
|
|
101
|
+
}
|
|
102
|
+
return "";
|
|
39
103
|
}
|
|
40
104
|
function formatAsPrettyJsonIfPossible(value) {
|
|
41
105
|
const trimmed = value.trim();
|
|
@@ -106,28 +170,22 @@ export async function renderDebugArtifactsHtml(params) {
|
|
|
106
170
|
const trimmed = markdown.trim();
|
|
107
171
|
if (trimmed === "")
|
|
108
172
|
return "<pre>No data</pre>";
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
for (const block of blocks) {
|
|
112
|
-
const lines = block.split("\n").map((l) => l.trimEnd());
|
|
113
|
-
const title = lines[0] ?? "";
|
|
114
|
-
const file = lines.find((l) => l.trimStart().startsWith("File:")) ?? "";
|
|
115
|
-
const line = lines.find((l) => l.trimStart().startsWith("Line:")) ?? "";
|
|
116
|
-
const why = lines.find((l) => l.trimStart().startsWith("Why:")) ?? "";
|
|
117
|
-
if (!title.startsWith("- ["))
|
|
118
|
-
continue;
|
|
119
|
-
const isHigh = title.toLowerCase().includes("[high]");
|
|
120
|
-
items.push(`<div class="finding${isHigh ? " high" : ""}"><div class="title">${escapeHtml(title)}</div><div class="meta">${escapeHtml(file)} • ${escapeHtml(line)}</div><div>${escapeHtml(why)}</div></div>`);
|
|
121
|
-
}
|
|
122
|
-
if (items.length === 0)
|
|
173
|
+
const findings = parseReviewFindings(trimmed);
|
|
174
|
+
if (findings.length === 0)
|
|
123
175
|
return `<pre>${escapeHtml(trimmed)}</pre>`;
|
|
124
|
-
return
|
|
176
|
+
return findings
|
|
177
|
+
.map((f) => `<div class="finding${f.severity === "high" ? " high" : ""}"><div class="title">${escapeHtml(`[${f.severity}] ${f.title}`)}</div><div class="meta">${escapeHtml(`${f.file}:${f.line}`)}</div><div>${escapeHtml(f.why)}</div></div>`)
|
|
178
|
+
.join("\n");
|
|
179
|
+
}
|
|
180
|
+
function sectionHeader(badge, labels) {
|
|
181
|
+
const duration = durationText(labels);
|
|
182
|
+
const parts = [
|
|
183
|
+
tokensText(labels),
|
|
184
|
+
duration === "" ? "" : `took ${duration}`,
|
|
185
|
+
`${labels.length} request(s)`,
|
|
186
|
+
].filter((p) => p !== "");
|
|
187
|
+
return `<div class="row"><span class="badge">${escapeHtml(badge)}</span><span class="tokens">${escapeHtml(parts.join(" • "))}</span></div>`;
|
|
125
188
|
}
|
|
126
|
-
const triageContent = getContent("triage_pass");
|
|
127
|
-
const triageContentPretty = formatAsPrettyJsonIfPossible(triageContent);
|
|
128
|
-
const fileServerLabel = Array.from(byLabel.keys()).find((k) => k.startsWith("file_review_server.js_round_"));
|
|
129
|
-
const fileCiLabel = Array.from(byLabel.keys()).find((k) => k.startsWith("file_review_.gitlab-ci.yml_round_"));
|
|
130
|
-
const consolidateLabel = "consolidate_pass";
|
|
131
189
|
function pickVerificationSection() {
|
|
132
190
|
const afterLimit = getContent("verification_pass_final_after_tool_limit");
|
|
133
191
|
if (afterLimit.trim() !== "")
|
|
@@ -135,7 +193,7 @@ export async function renderDebugArtifactsHtml(params) {
|
|
|
135
193
|
label: "verification_pass_final_after_tool_limit",
|
|
136
194
|
content: afterLimit,
|
|
137
195
|
};
|
|
138
|
-
const roundLabels =
|
|
196
|
+
const roundLabels = verificationLabels
|
|
139
197
|
.filter((k) => k.startsWith("verification_pass_round_"))
|
|
140
198
|
.sort((a, b) => {
|
|
141
199
|
const na = Number(a.replace("verification_pass_round_", ""));
|
|
@@ -154,8 +212,53 @@ export async function renderDebugArtifactsHtml(params) {
|
|
|
154
212
|
return { label: "verification_pass_round_1", content: "" };
|
|
155
213
|
}
|
|
156
214
|
const verificationSection = pickVerificationSection();
|
|
157
|
-
const verificationLabel = verificationSection.label;
|
|
158
215
|
const finalStatus = verificationSection.content.trim() !== "" ? "Verified" : "Fallback";
|
|
216
|
+
// Per-pass token/duration summary table.
|
|
217
|
+
const passRows = [
|
|
218
|
+
["Triage", triageLabels],
|
|
219
|
+
...Array.from(fileGroups.entries()).map(([path, labels]) => [`File review — ${path}`, labels]),
|
|
220
|
+
["Consolidation", consolidateLabels],
|
|
221
|
+
["Verification", verificationLabels],
|
|
222
|
+
["Single-pass fallback", mainLabels],
|
|
223
|
+
].filter(([, labels]) => labels.length > 0);
|
|
224
|
+
const cell = (value, opts) => `<td style="padding:6px 8px;border-bottom:1px solid var(--line);${opts?.strong ? "font-weight:700;" : ""}">${value}</td>`;
|
|
225
|
+
const usageCells = (labels, strong) => {
|
|
226
|
+
const { usage, reported } = sumUsage(labels);
|
|
227
|
+
if (reported === 0)
|
|
228
|
+
return `${cell("n/a", { strong })}${cell("n/a", { strong })}${cell("n/a", { strong })}`;
|
|
229
|
+
return [usage.prompt, usage.completion, usage.total]
|
|
230
|
+
.map((n) => cell(escapeHtml(n.toLocaleString()), { strong }))
|
|
231
|
+
.join("");
|
|
232
|
+
};
|
|
233
|
+
const passTableRows = passRows
|
|
234
|
+
.map(([name, labels]) => `<tr>${cell(escapeHtml(name))}${cell(String(labels.length))}${usageCells(labels)}${cell(escapeHtml(durationText(labels) || "—"))}</tr>`)
|
|
235
|
+
.join("\n");
|
|
236
|
+
const totalRow = `<tr>${cell("Total", { strong: true })}${cell(String(allLabels.length), { strong: true })}${usageCells(allLabels, true)}${cell(escapeHtml(durationText(allLabels) || "—"), { strong: true })}</tr>`;
|
|
237
|
+
const totalUsage = sumUsage(allLabels);
|
|
238
|
+
const totalTokensText = totalUsage.reported === 0 ? "n/a" : totalUsage.usage.total.toLocaleString();
|
|
239
|
+
const wallStarts = requests
|
|
240
|
+
.map((r) => parseTs(r.ts))
|
|
241
|
+
.filter((n) => n != null);
|
|
242
|
+
const wallEnds = [...responses, ...errors]
|
|
243
|
+
.map((r) => parseTs(r.ts))
|
|
244
|
+
.filter((n) => n != null);
|
|
245
|
+
const wallText = wallStarts.length > 0 && wallEnds.length > 0
|
|
246
|
+
? `${((Math.max(...wallEnds) - Math.min(...wallStarts)) / 1000).toFixed(1)}s`
|
|
247
|
+
: "n/a";
|
|
248
|
+
const fileSections = Array.from(fileGroups.entries())
|
|
249
|
+
.map(([path, labels]) => `<div class="section">${sectionHeader(`file: ${path}`, labels)}${renderFindings(lastContent(labels))}</div>`)
|
|
250
|
+
.join("\n");
|
|
251
|
+
const triageRawSections = triageLabels
|
|
252
|
+
.map((label) => `<div class="section">${sectionHeader(`label: ${label}`, [label])}<pre>${escapeHtml(formatAsPrettyJsonIfPossible(getContent(label)))}</pre></div>`)
|
|
253
|
+
.join("\n");
|
|
254
|
+
const errorSection = errors.length === 0
|
|
255
|
+
? ""
|
|
256
|
+
: `<h2>Errors</h2>
|
|
257
|
+
<div class="section">
|
|
258
|
+
${errors
|
|
259
|
+
.map((e) => `<div class="finding high"><div class="title">${escapeHtml(String(e.label ?? "unknown"))}</div><div class="meta">${escapeHtml(String(e.ts ?? ""))}</div><div>${escapeHtml(`${e?.error?.name ?? ""} ${e?.error?.status ?? ""} ${e?.error?.message ?? ""}`.trim())}</div></div>`)
|
|
260
|
+
.join("\n")}
|
|
261
|
+
</div>`;
|
|
159
262
|
const html = `<!doctype html>
|
|
160
263
|
<html lang="en">
|
|
161
264
|
<head>
|
|
@@ -167,13 +270,14 @@ export async function renderDebugArtifactsHtml(params) {
|
|
|
167
270
|
* { box-sizing:border-box; } body { margin:0; background:var(--bg); color:var(--text); font:14px/1.45 Inter,system-ui,sans-serif; padding:24px; }
|
|
168
271
|
.wrap{max-width:1100px;margin:0 auto;} h1,h2{margin:0 0 10px;} h1{font-size:24px;} h2{font-size:18px;margin-top:26px;} .sub{color:var(--muted);margin-bottom:18px;}
|
|
169
272
|
.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:14px 0 22px;} .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px;}
|
|
170
|
-
.k{color:var(--muted);font-size:12px;} .v{font-size:20px;font-weight:700;margin-top:4px;} .ok{color:var(--ok);}
|
|
273
|
+
.k{color:var(--muted);font-size:12px;} .v{font-size:20px;font-weight:700;margin-top:4px;} .ok{color:var(--ok);} .bad{color:var(--high);}
|
|
171
274
|
.section{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px;margin-bottom:14px;}
|
|
172
275
|
.row{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:10px;}
|
|
173
276
|
.badge{border:1px solid var(--line);background:#16223a;border-radius:999px;padding:2px 10px;font-size:12px;color:var(--muted);}
|
|
174
277
|
.tokens{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:var(--muted);}
|
|
175
278
|
.finding{border-left:3px solid var(--med);background:#131f36;padding:10px 12px;border-radius:8px;margin:8px 0;} .finding.high{border-left-color:var(--high);}
|
|
176
279
|
.finding .title{font-weight:700;} .meta{color:var(--muted);font-size:12px;margin:4px 0;}
|
|
280
|
+
table{width:100%;border-collapse:collapse;font-size:13px;}
|
|
177
281
|
pre{margin:8px 0 0;white-space:pre-wrap;word-break:break-word;background:var(--mono-bg);border:1px solid var(--line);padding:10px;border-radius:8px;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#d7e3ff;}
|
|
178
282
|
@media (max-width:900px){.grid{grid-template-columns:1fr 1fr;}} @media (max-width:520px){.grid{grid-template-columns:1fr;}}
|
|
179
283
|
</style>
|
|
@@ -185,16 +289,34 @@ export async function renderDebugArtifactsHtml(params) {
|
|
|
185
289
|
|
|
186
290
|
<div class="grid">
|
|
187
291
|
<div class="card"><div class="k">Model</div><div class="v">${escapeHtml(aiModel)}</div></div>
|
|
188
|
-
<div class="card"><div class="k">Requests</div><div class="v">${escapeHtml(String(
|
|
189
|
-
<div class="card"><div class="k">
|
|
292
|
+
<div class="card"><div class="k">Requests</div><div class="v">${escapeHtml(String(requests.length))}</div></div>
|
|
293
|
+
<div class="card"><div class="k">Files reviewed</div><div class="v">${escapeHtml(String(fileGroups.size))}</div></div>
|
|
294
|
+
<div class="card"><div class="k">Errors</div><div class="v${errors.length > 0 ? " bad" : ""}">${escapeHtml(String(errors.length))}</div></div>
|
|
295
|
+
<div class="card"><div class="k">Total tokens</div><div class="v">${escapeHtml(totalTokensText)}</div></div>
|
|
296
|
+
<div class="card"><div class="k">Wall time</div><div class="v">${escapeHtml(wallText)}</div></div>
|
|
190
297
|
<div class="card"><div class="k">Final Status</div><div class="v ok">${escapeHtml(finalStatus)}</div></div>
|
|
191
298
|
</div>
|
|
192
299
|
|
|
193
|
-
<h2>Token Usage</h2>
|
|
300
|
+
<h2>Token Usage by Pass</h2>
|
|
194
301
|
<div class="section">
|
|
195
|
-
<
|
|
196
|
-
|
|
302
|
+
<table>
|
|
303
|
+
<thead>
|
|
304
|
+
<tr style="text-align:left;color:var(--muted);">
|
|
305
|
+
<th style="padding:6px 8px;border-bottom:1px solid var(--line);">Pass</th>
|
|
306
|
+
<th style="padding:6px 8px;border-bottom:1px solid var(--line);">Requests</th>
|
|
307
|
+
<th style="padding:6px 8px;border-bottom:1px solid var(--line);">Prompt</th>
|
|
308
|
+
<th style="padding:6px 8px;border-bottom:1px solid var(--line);">Completion</th>
|
|
309
|
+
<th style="padding:6px 8px;border-bottom:1px solid var(--line);">Total</th>
|
|
310
|
+
<th style="padding:6px 8px;border-bottom:1px solid var(--line);">Duration</th>
|
|
311
|
+
</tr>
|
|
312
|
+
</thead>
|
|
313
|
+
<tbody>
|
|
314
|
+
${passTableRows}
|
|
315
|
+
${totalRow}
|
|
316
|
+
</tbody>
|
|
317
|
+
</table>
|
|
197
318
|
</div>
|
|
319
|
+
${errorSection}
|
|
198
320
|
|
|
199
321
|
<h2>Pass 1 — Triage Decisions</h2>
|
|
200
322
|
<div class="section">
|
|
@@ -202,28 +324,20 @@ export async function renderDebugArtifactsHtml(params) {
|
|
|
202
324
|
</div>
|
|
203
325
|
|
|
204
326
|
<h2>Pass 1 — Triage (raw model output)</h2>
|
|
205
|
-
|
|
206
|
-
<div class="row"><span class="badge">label: triage_pass</span><span class="tokens">${escapeHtml(findTs("triage_pass"))}</span></div>
|
|
207
|
-
<pre>${escapeHtml(triageContentPretty)}</pre>
|
|
208
|
-
</div>
|
|
327
|
+
${triageRawSections}
|
|
209
328
|
|
|
210
329
|
<h2>Pass 2 — File Reviews</h2>
|
|
211
|
-
${
|
|
212
|
-
? ""
|
|
213
|
-
: `<div class="section"><div class="row"><span class="badge">label: ${escapeHtml(fileServerLabel)}</span><span class="tokens">${escapeHtml(getTokenTriplet(fileServerLabel))}</span></div>${renderFindings(getContent(fileServerLabel))}</div>`}
|
|
214
|
-
${fileCiLabel == null
|
|
215
|
-
? ""
|
|
216
|
-
: `<div class="section"><div class="row"><span class="badge">label: ${escapeHtml(fileCiLabel)}</span><span class="tokens">${escapeHtml(getTokenTriplet(fileCiLabel))}</span></div>${renderFindings(getContent(fileCiLabel))}</div>`}
|
|
330
|
+
${fileSections === "" ? "<div class=\"section\"><pre>No file review records</pre></div>" : fileSections}
|
|
217
331
|
|
|
218
332
|
<h2>Pass 3 — Consolidation</h2>
|
|
219
333
|
<div class="section">
|
|
220
|
-
|
|
221
|
-
${renderFindings(getContent(
|
|
334
|
+
${sectionHeader("label: consolidate_pass", consolidateLabels)}
|
|
335
|
+
${renderFindings(getContent("consolidate_pass"))}
|
|
222
336
|
</div>
|
|
223
337
|
|
|
224
338
|
<h2>Pass 4 — Verification</h2>
|
|
225
339
|
<div class="section">
|
|
226
|
-
|
|
340
|
+
${sectionHeader(`label: ${verificationSection.label}`, verificationLabels)}
|
|
227
341
|
${renderFindings(verificationSection.content)}
|
|
228
342
|
</div>
|
|
229
343
|
</div>
|
package/dist/prompt/utils.js
CHANGED
|
@@ -55,10 +55,10 @@ export function sanitizeGitLabMarkdown(input) {
|
|
|
55
55
|
return withClosedFence;
|
|
56
56
|
}
|
|
57
57
|
const NO_ISSUES_SENTENCE = "No confirmed bugs or high-value optimizations found.";
|
|
58
|
-
|
|
58
|
+
/** Parse labeled (`- [high] … File:/Line:/Why:`) or pretty (`**🔴 High — …**`)
|
|
59
|
+
* finding blocks out of arbitrary model output. Returns [] when none match. */
|
|
60
|
+
export function parseReviewFindings(input) {
|
|
59
61
|
const normalized = input.replace(/\r\n/g, "\n").trim();
|
|
60
|
-
if (normalized === "" || normalized === NO_ISSUES_SENTENCE)
|
|
61
|
-
return normalized;
|
|
62
62
|
// Some providers collapse each finding into one long line.
|
|
63
63
|
// Expand inline markers so parser can recover structured blocks.
|
|
64
64
|
const expanded = normalized
|
|
@@ -141,6 +141,13 @@ export function normalizeReviewFindingsMarkdown(input) {
|
|
|
141
141
|
i = j - 1;
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
+
return findings;
|
|
145
|
+
}
|
|
146
|
+
export function normalizeReviewFindingsMarkdown(input) {
|
|
147
|
+
const normalized = input.replace(/\r\n/g, "\n").trim();
|
|
148
|
+
if (normalized === "" || normalized === NO_ISSUES_SENTENCE)
|
|
149
|
+
return normalized;
|
|
150
|
+
const findings = parseReviewFindings(normalized);
|
|
144
151
|
if (findings.length === 0)
|
|
145
152
|
return normalized;
|
|
146
153
|
const severityLabel = {
|