@kaitranntt/ccs 7.69.1-dev.5 → 7.69.1-dev.6
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/commands/tokens-command.d.ts.map +1 -1
- package/dist/commands/tokens-command.js +26 -37
- package/dist/commands/tokens-command.js.map +1 -1
- package/package.json +1 -1
- package/scripts/github/build-ai-review-packet.mjs +0 -242
- package/scripts/github/normalize-ai-review-output.mjs +0 -934
- package/scripts/github/prepare-ai-review-scope.mjs +0 -324
- package/scripts/github/run-ai-review-direct.mjs +0 -349
|
@@ -1,934 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const ASSESSMENTS = {
|
|
6
|
-
approved: '✅ APPROVED',
|
|
7
|
-
approved_with_notes: '⚠️ APPROVED WITH NOTES',
|
|
8
|
-
changes_requested: '❌ CHANGES REQUESTED',
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const SEVERITY_ORDER = ['high', 'medium', 'low'];
|
|
12
|
-
const SEVERITY_HEADERS = {
|
|
13
|
-
high: '### 🔴 High',
|
|
14
|
-
medium: '### 🟡 Medium',
|
|
15
|
-
low: '### 🟢 Low',
|
|
16
|
-
};
|
|
17
|
-
const SEVERITY_SUMMARY_LABELS = {
|
|
18
|
-
high: '🔴 High',
|
|
19
|
-
medium: '🟡 Medium',
|
|
20
|
-
low: '🟢 Low',
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const STATUS_LABELS = {
|
|
24
|
-
pass: '✅',
|
|
25
|
-
fail: '⚠️',
|
|
26
|
-
na: 'N/A',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const REVIEW_MODE_DETAILS = {
|
|
30
|
-
fast: 'selected-file packaged review',
|
|
31
|
-
triage: 'expanded packaged review with broader coverage',
|
|
32
|
-
deep: 'maintainer-triggered expanded packet review',
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const RENDERER_OWNED_MARKUP_PATTERNS = [
|
|
36
|
-
{ pattern: /^#{1,6}\s/u, reason: 'markdown heading' },
|
|
37
|
-
{ pattern: /^\s*Verdict\s*:/iu, reason: 'verdict label' },
|
|
38
|
-
{ pattern: /^\s*PR\s*#?\d+\s*Review(?:\s*[:.-]|$)/iu, reason: 'ad hoc PR heading' },
|
|
39
|
-
{ pattern: /\|\s*[-:]+\s*\|/u, reason: 'markdown table' },
|
|
40
|
-
{ pattern: /```/u, reason: 'code fence' },
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
const INLINE_CODE_TOKEN_PATTERN =
|
|
44
|
-
/\b[A-Za-z_][A-Za-z0-9_.]*\([^()\n]*\)|(?<![\w`])\.?[\w-]+(?:\/[\w.-]+)+\.[\w.-]+(?::\d+)?|\b[\w.-]+\/[\w.-]+@[\w.-]+\b|--[a-z0-9][a-z0-9-]*\b|\b[A-Z][A-Z0-9]*_[A-Z0-9_]+\b|\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/gu;
|
|
45
|
-
const CODE_BLOCK_LANGUAGE_PATTERN = /^[A-Za-z0-9#+.-]{1,20}$/u;
|
|
46
|
-
const MAX_FINDING_SNIPPETS = 2;
|
|
47
|
-
const MAX_SNIPPET_LINES = 20;
|
|
48
|
-
const MAX_SNIPPET_CHARACTERS = 1200;
|
|
49
|
-
const TOP_FINDINGS_LIMIT = 3;
|
|
50
|
-
|
|
51
|
-
function cleanText(value) {
|
|
52
|
-
return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ') : '';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function cleanMultilineText(value) {
|
|
56
|
-
if (typeof value !== 'string') {
|
|
57
|
-
return '';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return value
|
|
61
|
-
.replace(/\r\n/g, '\n')
|
|
62
|
-
.replace(/\r/g, '\n')
|
|
63
|
-
.replace(/^\n+/u, '')
|
|
64
|
-
.replace(/\n+$/u, '');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function escapeMarkdown(value) {
|
|
68
|
-
return String(value)
|
|
69
|
-
.replace(/\\/g, '\\\\')
|
|
70
|
-
.replace(/([`*_{}[\]<>|])/g, '\\$1');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function escapeMarkdownText(value) {
|
|
74
|
-
return escapeMarkdown(cleanText(value));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function renderCode(value) {
|
|
78
|
-
const text = cleanText(value);
|
|
79
|
-
const longestFence = Math.max(...[...text.matchAll(/`+/g)].map((match) => match[0].length), 0);
|
|
80
|
-
const fence = '`'.repeat(longestFence + 1);
|
|
81
|
-
return `${fence}${text}${fence}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function renderCodeBlock(value, language) {
|
|
85
|
-
const text = cleanMultilineText(value);
|
|
86
|
-
const longestFence = Math.max(...[...text.matchAll(/`+/gu)].map((match) => match[0].length), 0);
|
|
87
|
-
const fence = '`'.repeat(Math.max(3, longestFence + 1));
|
|
88
|
-
const info = cleanText(language);
|
|
89
|
-
return `${fence}${info}\n${text}\n${fence}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function renderInlineText(value) {
|
|
93
|
-
const text = cleanText(value);
|
|
94
|
-
if (!text) {
|
|
95
|
-
return '';
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let rendered = '';
|
|
99
|
-
let lastIndex = 0;
|
|
100
|
-
for (const match of text.matchAll(INLINE_CODE_TOKEN_PATTERN)) {
|
|
101
|
-
const token = match[0];
|
|
102
|
-
const index = match.index ?? 0;
|
|
103
|
-
if (index < lastIndex) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
rendered += escapeMarkdown(text.slice(lastIndex, index));
|
|
108
|
-
rendered += renderCode(token);
|
|
109
|
-
lastIndex = index + token.length;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
rendered += escapeMarkdown(text.slice(lastIndex));
|
|
113
|
-
return rendered;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function parsePositiveInteger(value) {
|
|
117
|
-
if (value === null || value === undefined || value === '') {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const parsed = typeof value === 'number' ? value : Number.parseInt(cleanText(value), 10);
|
|
122
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function normalizeReviewMode(value) {
|
|
126
|
-
const mode = cleanText(value).toLowerCase();
|
|
127
|
-
return REVIEW_MODE_DETAILS[mode] ? mode : null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function normalizeRenderingMetadata(raw) {
|
|
131
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
132
|
-
return {};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const mode = normalizeReviewMode(raw.mode);
|
|
136
|
-
const maxTurns = parsePositiveInteger(raw.maxTurns);
|
|
137
|
-
const timeoutMinutes = parsePositiveInteger(raw.timeoutMinutes);
|
|
138
|
-
const timeoutSeconds = parsePositiveInteger(raw.timeoutSeconds);
|
|
139
|
-
const selectedFiles = parsePositiveInteger(raw.selectedFiles);
|
|
140
|
-
const reviewableFiles = parsePositiveInteger(raw.reviewableFiles);
|
|
141
|
-
const selectedChanges = parsePositiveInteger(raw.selectedChanges);
|
|
142
|
-
const reviewableChanges = parsePositiveInteger(raw.reviewableChanges);
|
|
143
|
-
const packetIncludedFiles = parsePositiveInteger(raw.packetIncludedFiles);
|
|
144
|
-
const packetTotalFiles = parsePositiveInteger(raw.packetTotalFiles);
|
|
145
|
-
const packetOmittedFiles = parsePositiveInteger(raw.packetOmittedFiles);
|
|
146
|
-
const scopeLabel = cleanText(raw.scopeLabel).toLowerCase();
|
|
147
|
-
const metadata = {};
|
|
148
|
-
|
|
149
|
-
if (mode) metadata.mode = mode;
|
|
150
|
-
if (maxTurns) metadata.maxTurns = maxTurns;
|
|
151
|
-
if (timeoutMinutes) metadata.timeoutMinutes = timeoutMinutes;
|
|
152
|
-
if (timeoutSeconds) metadata.timeoutSeconds = timeoutSeconds;
|
|
153
|
-
if (selectedFiles) metadata.selectedFiles = selectedFiles;
|
|
154
|
-
if (reviewableFiles) metadata.reviewableFiles = reviewableFiles;
|
|
155
|
-
if (selectedChanges) metadata.selectedChanges = selectedChanges;
|
|
156
|
-
if (reviewableChanges) metadata.reviewableChanges = reviewableChanges;
|
|
157
|
-
if (packetIncludedFiles !== null) metadata.packetIncludedFiles = packetIncludedFiles;
|
|
158
|
-
if (packetTotalFiles !== null) metadata.packetTotalFiles = packetTotalFiles;
|
|
159
|
-
if (packetOmittedFiles !== null) metadata.packetOmittedFiles = packetOmittedFiles;
|
|
160
|
-
if (scopeLabel === 'reviewable files' || scopeLabel === 'changed files')
|
|
161
|
-
metadata.scopeLabel = scopeLabel;
|
|
162
|
-
|
|
163
|
-
return metadata;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function mergeRenderingMetadata(...sources) {
|
|
167
|
-
const merged = {};
|
|
168
|
-
for (const source of sources) {
|
|
169
|
-
Object.assign(merged, normalizeRenderingMetadata(source));
|
|
170
|
-
}
|
|
171
|
-
return merged;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function formatTurnBudget(rendering) {
|
|
175
|
-
return typeof rendering.maxTurns === 'number' ? `${rendering.maxTurns} turns` : null;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function formatTimeBudget(rendering) {
|
|
179
|
-
if (typeof rendering.timeoutMinutes === 'number') {
|
|
180
|
-
return `${rendering.timeoutMinutes} minute${rendering.timeoutMinutes === 1 ? '' : 's'}`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (typeof rendering.timeoutSeconds === 'number') {
|
|
184
|
-
return `${rendering.timeoutSeconds} second${rendering.timeoutSeconds === 1 ? '' : 's'}`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function formatCombinedBudget(rendering) {
|
|
191
|
-
const parts = [formatTurnBudget(rendering), formatTimeBudget(rendering)].filter(Boolean);
|
|
192
|
-
return parts.length > 0 ? parts.join(' / ') : null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function formatScopeSummary(rendering) {
|
|
196
|
-
if (
|
|
197
|
-
typeof rendering.selectedFiles !== 'number' ||
|
|
198
|
-
typeof rendering.reviewableFiles !== 'number'
|
|
199
|
-
) {
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const scopeLabel = rendering.scopeLabel || 'reviewable files';
|
|
204
|
-
const fileScope = `${rendering.selectedFiles}/${rendering.reviewableFiles} ${scopeLabel}`;
|
|
205
|
-
if (
|
|
206
|
-
typeof rendering.selectedChanges === 'number' &&
|
|
207
|
-
typeof rendering.reviewableChanges === 'number'
|
|
208
|
-
) {
|
|
209
|
-
const changeLabel =
|
|
210
|
-
scopeLabel === 'reviewable files' ? 'reviewable changed lines' : 'changed lines';
|
|
211
|
-
return `${fileScope}; ${rendering.selectedChanges}/${rendering.reviewableChanges} ${changeLabel}`;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return fileScope;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function formatPacketCoverage(rendering) {
|
|
218
|
-
if (
|
|
219
|
-
typeof rendering.packetIncludedFiles !== 'number' ||
|
|
220
|
-
typeof rendering.packetTotalFiles !== 'number'
|
|
221
|
-
) {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const packetSummary = `${rendering.packetIncludedFiles}/${rendering.packetTotalFiles} selected files included in the final review packet`;
|
|
226
|
-
if (typeof rendering.packetOmittedFiles === 'number' && rendering.packetOmittedFiles > 0) {
|
|
227
|
-
return `${packetSummary}; ${rendering.packetOmittedFiles} selected file${rendering.packetOmittedFiles === 1 ? '' : 's'} omitted for packet budget`;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return packetSummary;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function formatReviewContext(rendering) {
|
|
234
|
-
const parts = [];
|
|
235
|
-
|
|
236
|
-
if (rendering.mode) {
|
|
237
|
-
parts.push(renderCode(rendering.mode));
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
typeof rendering.selectedFiles === 'number' &&
|
|
242
|
-
typeof rendering.reviewableFiles === 'number'
|
|
243
|
-
) {
|
|
244
|
-
parts.push(`${rendering.selectedFiles}/${rendering.reviewableFiles} files`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
typeof rendering.selectedChanges === 'number' &&
|
|
249
|
-
typeof rendering.reviewableChanges === 'number'
|
|
250
|
-
) {
|
|
251
|
-
parts.push(`${rendering.selectedChanges}/${rendering.reviewableChanges} lines`);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
typeof rendering.packetIncludedFiles === 'number' &&
|
|
256
|
-
typeof rendering.packetTotalFiles === 'number'
|
|
257
|
-
) {
|
|
258
|
-
parts.push(`packet ${rendering.packetIncludedFiles}/${rendering.packetTotalFiles}`);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const runtimeBudget =
|
|
262
|
-
formatCombinedBudget(rendering) || formatTimeBudget(rendering) || formatTurnBudget(rendering);
|
|
263
|
-
if (runtimeBudget) {
|
|
264
|
-
parts.push(runtimeBudget);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (parts.length === 0) {
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return `> 🧭 ${parts.join(' • ')}`;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function classifyFallbackReason(reason) {
|
|
275
|
-
const normalized = cleanText(reason).toLowerCase();
|
|
276
|
-
if (!normalized || normalized === 'missing structured output') {
|
|
277
|
-
return 'missing';
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (normalized === 'structured output is not valid json') {
|
|
281
|
-
return 'invalid_json';
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return 'invalid_fields';
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function describeIncompleteOutcome({ reason, rendering, turnsUsed, status }) {
|
|
288
|
-
const reviewLabel = rendering.mode ? `${renderCode(rendering.mode)} review` : 'bounded review';
|
|
289
|
-
const turnBudget = formatTurnBudget(rendering);
|
|
290
|
-
const timeBudget = formatTimeBudget(rendering);
|
|
291
|
-
const combinedBudget = formatCombinedBudget(rendering);
|
|
292
|
-
const exhaustedTurnBudget =
|
|
293
|
-
typeof turnsUsed === 'number' &&
|
|
294
|
-
typeof rendering.maxTurns === 'number' &&
|
|
295
|
-
turnsUsed >= rendering.maxTurns;
|
|
296
|
-
|
|
297
|
-
if (status === 'cancelled' && timeBudget) {
|
|
298
|
-
return `The ${reviewLabel} hit the workflow runtime cap before it produced validated structured output. The run stayed bounded to ${timeBudget}.`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (exhaustedTurnBudget) {
|
|
302
|
-
return `The ${reviewLabel} reached its ${rendering.maxTurns}-turn runtime budget before it produced validated structured output.`;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (combinedBudget && classifyFallbackReason(reason) === 'missing') {
|
|
306
|
-
return `The ${reviewLabel} ended before it could produce validated structured output within the available ${combinedBudget} runtime budget.`;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
classifyFallbackReason(reason) === 'missing' ||
|
|
311
|
-
classifyFallbackReason(reason) === 'invalid_json'
|
|
312
|
-
) {
|
|
313
|
-
return `The ${reviewLabel} ended without validated structured output, so the normalizer published the safe fallback comment instead.`;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return `The ${reviewLabel} returned incomplete structured data, so the normalizer published the safe fallback comment instead.`;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function validatePlainTextField(fieldName, value) {
|
|
320
|
-
const text = cleanText(value);
|
|
321
|
-
if (!text) {
|
|
322
|
-
return { ok: false, reason: `${fieldName} is required` };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const match = RENDERER_OWNED_MARKUP_PATTERNS.find(({ pattern }) => pattern.test(text));
|
|
326
|
-
if (match) {
|
|
327
|
-
return { ok: false, reason: `${fieldName} contains ${match.reason}` };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return { ok: true, value: text };
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function normalizeStringList(fieldName, raw) {
|
|
334
|
-
if (!Array.isArray(raw)) {
|
|
335
|
-
return { ok: false, reason: `${fieldName} must be an array` };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const values = [];
|
|
339
|
-
for (const [index, item] of raw.entries()) {
|
|
340
|
-
const validation = validatePlainTextField(`${fieldName}[${index}]`, item);
|
|
341
|
-
if (!validation.ok) return validation;
|
|
342
|
-
values.push(validation.value);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return { ok: true, value: values };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function normalizeChecklistRows(fieldName, labelField, raw) {
|
|
349
|
-
if (!Array.isArray(raw)) {
|
|
350
|
-
return { ok: false, reason: `${fieldName} must be an array` };
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const rows = [];
|
|
354
|
-
for (const [index, item] of raw.entries()) {
|
|
355
|
-
const label = validatePlainTextField(
|
|
356
|
-
`${fieldName}[${index}].${labelField}`,
|
|
357
|
-
item?.[labelField]
|
|
358
|
-
);
|
|
359
|
-
if (!label.ok) return label;
|
|
360
|
-
|
|
361
|
-
const notes = validatePlainTextField(`${fieldName}[${index}].notes`, item?.notes);
|
|
362
|
-
if (!notes.ok) return notes;
|
|
363
|
-
|
|
364
|
-
const status = cleanText(item?.status).toLowerCase();
|
|
365
|
-
if (!STATUS_LABELS[status]) {
|
|
366
|
-
return { ok: false, reason: `${fieldName}[${index}].status is invalid` };
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
rows.push({ [labelField]: label.value, status, notes: notes.value });
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (rows.length === 0) {
|
|
373
|
-
return { ok: false, reason: `${fieldName} must contain at least 1 item` };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return { ok: true, value: rows };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function normalizeFindingSnippets(fieldName, raw) {
|
|
380
|
-
if (raw === null || raw === undefined) {
|
|
381
|
-
return { ok: true, value: [] };
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (!Array.isArray(raw)) {
|
|
385
|
-
return { ok: false, reason: `${fieldName} must be an array` };
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (raw.length > MAX_FINDING_SNIPPETS) {
|
|
389
|
-
return {
|
|
390
|
-
ok: false,
|
|
391
|
-
reason: `${fieldName} must contain at most ${MAX_FINDING_SNIPPETS} snippets`,
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const snippets = [];
|
|
396
|
-
for (const [index, item] of raw.entries()) {
|
|
397
|
-
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
398
|
-
return { ok: false, reason: `${fieldName}[${index}] must be an object` };
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
let label = null;
|
|
402
|
-
if (Object.hasOwn(item, 'label') && item.label !== null && item.label !== undefined) {
|
|
403
|
-
const labelValidation = validatePlainTextField(`${fieldName}[${index}].label`, item.label);
|
|
404
|
-
if (!labelValidation.ok) return labelValidation;
|
|
405
|
-
label = labelValidation.value;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
let language = null;
|
|
409
|
-
if (Object.hasOwn(item, 'language') && item.language !== null && item.language !== undefined) {
|
|
410
|
-
const normalizedLanguage = cleanText(item.language).toLowerCase();
|
|
411
|
-
if (normalizedLanguage) {
|
|
412
|
-
if (!CODE_BLOCK_LANGUAGE_PATTERN.test(normalizedLanguage)) {
|
|
413
|
-
return { ok: false, reason: `${fieldName}[${index}].language is invalid` };
|
|
414
|
-
}
|
|
415
|
-
language = normalizedLanguage;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const code = cleanMultilineText(item.code);
|
|
420
|
-
if (!code) {
|
|
421
|
-
return { ok: false, reason: `${fieldName}[${index}].code is required` };
|
|
422
|
-
}
|
|
423
|
-
if (code.length > MAX_SNIPPET_CHARACTERS) {
|
|
424
|
-
return {
|
|
425
|
-
ok: false,
|
|
426
|
-
reason: `${fieldName}[${index}].code exceeds ${MAX_SNIPPET_CHARACTERS} characters`,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const lineCount = code.split('\n').length;
|
|
431
|
-
if (lineCount > MAX_SNIPPET_LINES) {
|
|
432
|
-
return {
|
|
433
|
-
ok: false,
|
|
434
|
-
reason: `${fieldName}[${index}].code exceeds ${MAX_SNIPPET_LINES} lines`,
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const snippet = { code };
|
|
439
|
-
if (label) snippet.label = label;
|
|
440
|
-
if (language) snippet.language = language;
|
|
441
|
-
snippets.push(snippet);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return { ok: true, value: snippets };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function readExecutionMetadata(executionFile) {
|
|
448
|
-
if (!executionFile || !fs.existsSync(executionFile)) {
|
|
449
|
-
return {};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
try {
|
|
453
|
-
const turns = JSON.parse(fs.readFileSync(executionFile, 'utf8'));
|
|
454
|
-
const init = turns.find((turn) => turn?.type === 'system' && turn?.subtype === 'init');
|
|
455
|
-
const result = [...turns].reverse().find((turn) => turn?.type === 'result');
|
|
456
|
-
return {
|
|
457
|
-
runtimeTools: Array.isArray(init?.tools) ? init.tools : [],
|
|
458
|
-
turnsUsed: typeof result?.num_turns === 'number' ? result.num_turns : null,
|
|
459
|
-
};
|
|
460
|
-
} catch {
|
|
461
|
-
return {};
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function readSelectedFiles(manifestFile) {
|
|
466
|
-
if (!manifestFile || !fs.existsSync(manifestFile)) {
|
|
467
|
-
return [];
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
try {
|
|
471
|
-
return fs
|
|
472
|
-
.readFileSync(manifestFile, 'utf8')
|
|
473
|
-
.split('\n')
|
|
474
|
-
.map((line) => cleanText(line))
|
|
475
|
-
.filter(Boolean);
|
|
476
|
-
} catch {
|
|
477
|
-
return [];
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function formatHotspotFiles(files) {
|
|
482
|
-
if (!files.length) {
|
|
483
|
-
return null;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const visible = files.slice(0, 4).map(renderCode).join(', ');
|
|
487
|
-
return files.length > 4 ? `${visible}, and ${files.length - 4} more` : visible;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function formatRemainingCoverage(rendering) {
|
|
491
|
-
if (
|
|
492
|
-
typeof rendering.reviewableFiles !== 'number' ||
|
|
493
|
-
(typeof rendering.packetIncludedFiles !== 'number' &&
|
|
494
|
-
typeof rendering.selectedFiles !== 'number')
|
|
495
|
-
) {
|
|
496
|
-
return null;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const coveredFiles =
|
|
500
|
-
typeof rendering.packetIncludedFiles === 'number'
|
|
501
|
-
? rendering.packetIncludedFiles
|
|
502
|
-
: rendering.selectedFiles;
|
|
503
|
-
const remainingFiles = Math.max(rendering.reviewableFiles - coveredFiles, 0);
|
|
504
|
-
const packetOmittedFiles =
|
|
505
|
-
typeof rendering.packetOmittedFiles === 'number' ? rendering.packetOmittedFiles : 0;
|
|
506
|
-
const hasChangeCounts =
|
|
507
|
-
packetOmittedFiles === 0 &&
|
|
508
|
-
typeof rendering.selectedChanges === 'number' &&
|
|
509
|
-
typeof rendering.reviewableChanges === 'number';
|
|
510
|
-
const remainingChanges = hasChangeCounts
|
|
511
|
-
? Math.max(rendering.reviewableChanges - rendering.selectedChanges, 0)
|
|
512
|
-
: null;
|
|
513
|
-
|
|
514
|
-
if (remainingFiles === 0 && (!hasChangeCounts || remainingChanges === 0)) {
|
|
515
|
-
return null;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (typeof remainingChanges === 'number') {
|
|
519
|
-
return `${remainingFiles} file${remainingFiles === 1 ? '' : 's'}; ${remainingChanges} changed lines`;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
return `${remainingFiles} file${remainingFiles === 1 ? '' : 's'}`;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function formatFallbackFollowUp(rendering) {
|
|
526
|
-
if (rendering.mode === 'triage') {
|
|
527
|
-
return 'Focus manual review on the selected files above, and use `/review` for a deeper pass when release, auth, config, or workflow paths changed.';
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return 'Use `/review` when you need a deeper maintainer rerun with more surrounding context.';
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
export function normalizeStructuredOutput(raw) {
|
|
534
|
-
if (!raw) {
|
|
535
|
-
return { ok: false, reason: 'missing structured output' };
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
let parsed;
|
|
539
|
-
try {
|
|
540
|
-
parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
541
|
-
} catch {
|
|
542
|
-
return { ok: false, reason: 'structured output is not valid JSON' };
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
546
|
-
return { ok: false, reason: 'structured output must be an object' };
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const summary = validatePlainTextField('summary', parsed.summary);
|
|
550
|
-
if (!summary.ok) return summary;
|
|
551
|
-
|
|
552
|
-
const overallAssessment = cleanText(parsed.overallAssessment).toLowerCase();
|
|
553
|
-
const overallRationale = validatePlainTextField('overallRationale', parsed.overallRationale);
|
|
554
|
-
if (!overallRationale.ok) return overallRationale;
|
|
555
|
-
|
|
556
|
-
const findings = Array.isArray(parsed.findings) ? parsed.findings : null;
|
|
557
|
-
const securityChecklist = normalizeChecklistRows(
|
|
558
|
-
'securityChecklist',
|
|
559
|
-
'check',
|
|
560
|
-
parsed.securityChecklist
|
|
561
|
-
);
|
|
562
|
-
if (!securityChecklist.ok) return securityChecklist;
|
|
563
|
-
|
|
564
|
-
const ccsCompliance = normalizeChecklistRows('ccsCompliance', 'rule', parsed.ccsCompliance);
|
|
565
|
-
if (!ccsCompliance.ok) return ccsCompliance;
|
|
566
|
-
|
|
567
|
-
const informational = normalizeStringList('informational', parsed.informational);
|
|
568
|
-
if (!informational.ok) return informational;
|
|
569
|
-
|
|
570
|
-
const strengths = normalizeStringList('strengths', parsed.strengths);
|
|
571
|
-
if (!strengths.ok) return strengths;
|
|
572
|
-
|
|
573
|
-
const rendering = normalizeRenderingMetadata(parsed.rendering);
|
|
574
|
-
|
|
575
|
-
if (!ASSESSMENTS[overallAssessment] || findings === null) {
|
|
576
|
-
return { ok: false, reason: 'structured output is missing required review fields' };
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const normalizedFindings = [];
|
|
580
|
-
for (const [index, finding] of findings.entries()) {
|
|
581
|
-
const severity = cleanText(finding?.severity).toLowerCase();
|
|
582
|
-
const title = validatePlainTextField(`findings[${index}].title`, finding?.title);
|
|
583
|
-
if (!title.ok) return title;
|
|
584
|
-
|
|
585
|
-
const file = validatePlainTextField(`findings[${index}].file`, finding?.file);
|
|
586
|
-
if (!file.ok) return file;
|
|
587
|
-
|
|
588
|
-
const what = validatePlainTextField(`findings[${index}].what`, finding?.what);
|
|
589
|
-
if (!what.ok) return what;
|
|
590
|
-
|
|
591
|
-
const why = validatePlainTextField(`findings[${index}].why`, finding?.why);
|
|
592
|
-
if (!why.ok) return why;
|
|
593
|
-
|
|
594
|
-
const fix = validatePlainTextField(`findings[${index}].fix`, finding?.fix);
|
|
595
|
-
if (!fix.ok) return fix;
|
|
596
|
-
const snippets = normalizeFindingSnippets(`findings[${index}].snippets`, finding?.snippets);
|
|
597
|
-
if (!snippets.ok) return snippets;
|
|
598
|
-
|
|
599
|
-
let line = null;
|
|
600
|
-
if (finding && Object.hasOwn(finding, 'line')) {
|
|
601
|
-
if (finding.line === null) {
|
|
602
|
-
line = null;
|
|
603
|
-
} else if (
|
|
604
|
-
typeof finding.line === 'number' &&
|
|
605
|
-
Number.isInteger(finding.line) &&
|
|
606
|
-
finding.line > 0
|
|
607
|
-
) {
|
|
608
|
-
line = finding.line;
|
|
609
|
-
} else {
|
|
610
|
-
return { ok: false, reason: `findings[${index}].line is invalid` };
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
if (!SEVERITY_HEADERS[severity]) {
|
|
615
|
-
return { ok: false, reason: `findings[${index}].severity is invalid` };
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
normalizedFindings.push({
|
|
619
|
-
severity,
|
|
620
|
-
title: title.value,
|
|
621
|
-
file: file.value,
|
|
622
|
-
line,
|
|
623
|
-
what: what.value,
|
|
624
|
-
why: why.value,
|
|
625
|
-
fix: fix.value,
|
|
626
|
-
snippets: snippets.value,
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const value = {
|
|
631
|
-
summary: summary.value,
|
|
632
|
-
findings: normalizedFindings,
|
|
633
|
-
overallAssessment,
|
|
634
|
-
overallRationale: overallRationale.value,
|
|
635
|
-
securityChecklist: securityChecklist.value,
|
|
636
|
-
ccsCompliance: ccsCompliance.value,
|
|
637
|
-
informational: informational.value,
|
|
638
|
-
strengths: strengths.value,
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
if (Object.keys(rendering).length > 0) {
|
|
642
|
-
value.rendering = rendering;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
return { ok: true, value };
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function renderChecklistTable(labelHeader, labelKey, rows) {
|
|
649
|
-
const lines = [`| ${labelHeader} | Status | Notes |`, '|---|---|---|'];
|
|
650
|
-
for (const row of rows) {
|
|
651
|
-
lines.push(
|
|
652
|
-
`| ${renderInlineText(row[labelKey])} | ${STATUS_LABELS[row.status]} | ${renderInlineText(row.notes)} |`
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
return lines;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function renderBulletSection(items) {
|
|
659
|
-
if (items.length === 0) return [];
|
|
660
|
-
return items.map((item) => `- ${renderInlineText(item)}`);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function renderFindingSnippets(snippets) {
|
|
664
|
-
if (!Array.isArray(snippets) || snippets.length === 0) {
|
|
665
|
-
return [];
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const lines = [];
|
|
669
|
-
for (const snippet of snippets) {
|
|
670
|
-
const label = snippet.label ? `Evidence: ${renderInlineText(snippet.label)}` : 'Evidence:';
|
|
671
|
-
if (lines.length > 0) {
|
|
672
|
-
lines.push('');
|
|
673
|
-
}
|
|
674
|
-
lines.push(label, '', ...renderCodeBlock(snippet.code, snippet.language).split('\n'));
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
return lines;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function renderSection(title, bodyLines) {
|
|
681
|
-
if (!bodyLines.length) {
|
|
682
|
-
return [];
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
return ['', title, '', ...bodyLines];
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function renderFindingReference(finding) {
|
|
689
|
-
return finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function getOrderedFindings(findings) {
|
|
693
|
-
return SEVERITY_ORDER.flatMap((severity) =>
|
|
694
|
-
findings.filter((finding) => finding.severity === severity)
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function renderTopFindings(findings) {
|
|
699
|
-
if (findings.length === 0) {
|
|
700
|
-
return ['No confirmed issues found after reviewing the diff and surrounding code.'];
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const orderedFindings = getOrderedFindings(findings);
|
|
704
|
-
const lines = orderedFindings
|
|
705
|
-
.slice(0, TOP_FINDINGS_LIMIT)
|
|
706
|
-
.map(
|
|
707
|
-
(finding) =>
|
|
708
|
-
`- ${SEVERITY_SUMMARY_LABELS[finding.severity]} ${renderCode(renderFindingReference(finding))} — ${renderInlineText(finding.title)}`
|
|
709
|
-
);
|
|
710
|
-
|
|
711
|
-
if (orderedFindings.length > TOP_FINDINGS_LIMIT) {
|
|
712
|
-
const remaining = orderedFindings.length - TOP_FINDINGS_LIMIT;
|
|
713
|
-
lines.push(`- ${remaining} more finding${remaining === 1 ? '' : 's'} in the details below.`);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
return lines;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function renderDetailedFindings(findings) {
|
|
720
|
-
if (findings.length === 0) {
|
|
721
|
-
return [];
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const lines = [];
|
|
725
|
-
for (const severity of SEVERITY_ORDER) {
|
|
726
|
-
const scopedFindings = findings.filter((finding) => finding.severity === severity);
|
|
727
|
-
if (scopedFindings.length === 0) continue;
|
|
728
|
-
|
|
729
|
-
lines.push(`**${SEVERITY_SUMMARY_LABELS[severity]} (${scopedFindings.length})**`, '');
|
|
730
|
-
for (const [index, finding] of scopedFindings.entries()) {
|
|
731
|
-
const snippets = Array.isArray(finding.snippets) ? finding.snippets : [];
|
|
732
|
-
lines.push(`#### ${index + 1}. ${renderInlineText(finding.title)}`);
|
|
733
|
-
lines.push(`- Location: ${renderCode(renderFindingReference(finding))}`);
|
|
734
|
-
lines.push(`- Impact: ${renderInlineText(finding.why)}`);
|
|
735
|
-
lines.push(`- Problem: ${renderInlineText(finding.what)}`);
|
|
736
|
-
lines.push(`- Fix: ${renderInlineText(finding.fix)}`);
|
|
737
|
-
if (snippets.length > 0) {
|
|
738
|
-
lines.push('', ...renderFindingSnippets(snippets));
|
|
739
|
-
}
|
|
740
|
-
lines.push('');
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
if (lines[lines.length - 1] === '') {
|
|
745
|
-
lines.pop();
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
return lines;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
export function renderStructuredReview(review, { model, rendering: renderOptions } = {}) {
|
|
752
|
-
const rendering = mergeRenderingMetadata(review?.rendering, renderOptions);
|
|
753
|
-
const lines = ['### 📋 Summary', '', renderInlineText(review.summary)];
|
|
754
|
-
const reviewContext = formatReviewContext(rendering);
|
|
755
|
-
|
|
756
|
-
if (reviewContext) {
|
|
757
|
-
lines.push('', reviewContext);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
lines.push('', '### 🔍 Findings');
|
|
761
|
-
if (review.findings.length === 0) {
|
|
762
|
-
lines.push('', 'No confirmed issues found after reviewing the diff and surrounding code.');
|
|
763
|
-
} else {
|
|
764
|
-
for (const severity of SEVERITY_ORDER) {
|
|
765
|
-
const scopedFindings = review.findings.filter((finding) => finding.severity === severity);
|
|
766
|
-
if (scopedFindings.length === 0) continue;
|
|
767
|
-
|
|
768
|
-
lines.push('', SEVERITY_HEADERS[severity], '');
|
|
769
|
-
for (const finding of scopedFindings) {
|
|
770
|
-
const snippets = Array.isArray(finding.snippets) ? finding.snippets : [];
|
|
771
|
-
lines.push(
|
|
772
|
-
`- **${renderCode(renderFindingReference(finding))} — ${renderInlineText(finding.title)}**`
|
|
773
|
-
);
|
|
774
|
-
lines.push(` Problem: ${renderInlineText(finding.what)}`);
|
|
775
|
-
lines.push(` Why it matters: ${renderInlineText(finding.why)}`);
|
|
776
|
-
lines.push(` Suggested fix: ${renderInlineText(finding.fix)}`);
|
|
777
|
-
|
|
778
|
-
if (snippets.length > 0) {
|
|
779
|
-
lines.push('');
|
|
780
|
-
lines.push(...renderFindingSnippets(snippets));
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
lines.push('');
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
if (lines[lines.length - 1] === '') {
|
|
788
|
-
lines.pop();
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
lines.push(
|
|
793
|
-
...renderSection(
|
|
794
|
-
'### 🔒 Security Checklist',
|
|
795
|
-
renderChecklistTable('Check', 'check', review.securityChecklist)
|
|
796
|
-
)
|
|
797
|
-
);
|
|
798
|
-
lines.push(
|
|
799
|
-
...renderSection(
|
|
800
|
-
'### 📊 CCS Compliance',
|
|
801
|
-
renderChecklistTable('Rule', 'rule', review.ccsCompliance)
|
|
802
|
-
)
|
|
803
|
-
);
|
|
804
|
-
lines.push(...renderSection('### 💡 Informational', renderBulletSection(review.informational)));
|
|
805
|
-
lines.push(...renderSection("### ✅ What's Done Well", renderBulletSection(review.strengths)));
|
|
806
|
-
|
|
807
|
-
lines.push(
|
|
808
|
-
'',
|
|
809
|
-
'### 🎯 Overall Assessment',
|
|
810
|
-
'',
|
|
811
|
-
`**${ASSESSMENTS[review.overallAssessment]}** — ${renderInlineText(review.overallRationale)}`,
|
|
812
|
-
'',
|
|
813
|
-
`> 🤖 Reviewed by \`${model}\``
|
|
814
|
-
);
|
|
815
|
-
|
|
816
|
-
return lines.join('\n');
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
export function renderIncompleteReview({
|
|
820
|
-
model,
|
|
821
|
-
reason,
|
|
822
|
-
runUrl,
|
|
823
|
-
runtimeTools,
|
|
824
|
-
turnsUsed,
|
|
825
|
-
selectedFiles,
|
|
826
|
-
rendering: renderOptions,
|
|
827
|
-
status,
|
|
828
|
-
}) {
|
|
829
|
-
const rendering = mergeRenderingMetadata(renderOptions);
|
|
830
|
-
const lines = [
|
|
831
|
-
'### ⚠️ AI Review Incomplete',
|
|
832
|
-
'',
|
|
833
|
-
'Claude did not return validated structured review output, so this workflow published deterministic hotspot context instead of raw scratch text.',
|
|
834
|
-
'',
|
|
835
|
-
`- Outcome: ${describeIncompleteOutcome({ reason, rendering, turnsUsed, status })}`,
|
|
836
|
-
];
|
|
837
|
-
|
|
838
|
-
if (rendering.mode) {
|
|
839
|
-
lines.push(
|
|
840
|
-
`- Review mode: ${renderCode(rendering.mode)} (${escapeMarkdownText(REVIEW_MODE_DETAILS[rendering.mode])})`
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
const scopeSummary = formatScopeSummary(rendering);
|
|
844
|
-
if (scopeSummary) {
|
|
845
|
-
lines.push(`- Review scope: ${escapeMarkdownText(scopeSummary)}`);
|
|
846
|
-
}
|
|
847
|
-
const packetCoverage = formatPacketCoverage(rendering);
|
|
848
|
-
if (packetCoverage) {
|
|
849
|
-
lines.push(`- Packet coverage: ${escapeMarkdownText(packetCoverage)}`);
|
|
850
|
-
}
|
|
851
|
-
const runtimeBudget = formatCombinedBudget(rendering);
|
|
852
|
-
if (runtimeBudget) {
|
|
853
|
-
lines.push(`- Runtime budget: ${escapeMarkdownText(runtimeBudget)}`);
|
|
854
|
-
}
|
|
855
|
-
const hotspotFiles = formatHotspotFiles(selectedFiles || []);
|
|
856
|
-
if (hotspotFiles) {
|
|
857
|
-
lines.push(`- Hotspot files in this pass: ${hotspotFiles}`);
|
|
858
|
-
}
|
|
859
|
-
const remainingCoverage = formatRemainingCoverage(rendering);
|
|
860
|
-
if (remainingCoverage) {
|
|
861
|
-
lines.push(
|
|
862
|
-
`- Remaining reviewable scope not fully covered: ${escapeMarkdownText(remainingCoverage)}`
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
lines.push(`- Manual follow-up: ${escapeMarkdownText(formatFallbackFollowUp(rendering))}`);
|
|
866
|
-
if (runtimeTools?.length) {
|
|
867
|
-
lines.push(`- Runtime tools: ${runtimeTools.map(renderCode).join(', ')}`);
|
|
868
|
-
}
|
|
869
|
-
if (typeof turnsUsed === 'number') {
|
|
870
|
-
lines.push(`- Turns used: ${turnsUsed}`);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
lines.push(
|
|
874
|
-
'',
|
|
875
|
-
`Re-run \`/review\` or inspect [the workflow run](${runUrl}).`,
|
|
876
|
-
'',
|
|
877
|
-
`> 🤖 Reviewed by \`${model}\``
|
|
878
|
-
);
|
|
879
|
-
return lines.join('\n');
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
export function writeReviewFromEnv(env = process.env) {
|
|
883
|
-
const outputFile = env.AI_REVIEW_OUTPUT_FILE || 'pr_review.md';
|
|
884
|
-
const model = env.AI_REVIEW_MODEL || 'unknown-model';
|
|
885
|
-
const runUrl = env.AI_REVIEW_RUN_URL || '#';
|
|
886
|
-
const validation = normalizeStructuredOutput(env.AI_REVIEW_STRUCTURED_OUTPUT);
|
|
887
|
-
const metadata = readExecutionMetadata(env.AI_REVIEW_EXECUTION_FILE);
|
|
888
|
-
const selectedFiles = readSelectedFiles(env.AI_REVIEW_SCOPE_MANIFEST_FILE);
|
|
889
|
-
const status = cleanText(env.AI_REVIEW_STATUS).toLowerCase() || null;
|
|
890
|
-
const rendering = normalizeRenderingMetadata({
|
|
891
|
-
mode: env.AI_REVIEW_MODE,
|
|
892
|
-
selectedFiles: env.AI_REVIEW_SELECTED_FILES,
|
|
893
|
-
reviewableFiles: env.AI_REVIEW_REVIEWABLE_FILES,
|
|
894
|
-
selectedChanges: env.AI_REVIEW_SELECTED_CHANGES,
|
|
895
|
-
reviewableChanges: env.AI_REVIEW_REVIEWABLE_CHANGES,
|
|
896
|
-
packetIncludedFiles: env.AI_REVIEW_PACKET_INCLUDED_FILES,
|
|
897
|
-
packetTotalFiles: env.AI_REVIEW_PACKET_TOTAL_FILES,
|
|
898
|
-
packetOmittedFiles: env.AI_REVIEW_PACKET_OMITTED_FILES,
|
|
899
|
-
scopeLabel: env.AI_REVIEW_SCOPE_LABEL,
|
|
900
|
-
maxTurns: env.AI_REVIEW_MAX_TURNS,
|
|
901
|
-
timeoutMinutes: env.AI_REVIEW_TIMEOUT_MINUTES ?? env.AI_REVIEW_TIMEOUT_MINUTES_BUDGET,
|
|
902
|
-
timeoutSeconds: env.AI_REVIEW_TIMEOUT_SECONDS ?? env.AI_REVIEW_TIMEOUT_SEC,
|
|
903
|
-
});
|
|
904
|
-
const content = validation.ok
|
|
905
|
-
? renderStructuredReview(validation.value, { model, rendering })
|
|
906
|
-
: renderIncompleteReview({
|
|
907
|
-
model,
|
|
908
|
-
reason: validation.reason,
|
|
909
|
-
runUrl,
|
|
910
|
-
runtimeTools: metadata.runtimeTools,
|
|
911
|
-
turnsUsed: metadata.turnsUsed,
|
|
912
|
-
selectedFiles,
|
|
913
|
-
rendering,
|
|
914
|
-
status,
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
918
|
-
fs.writeFileSync(outputFile, `${content}\n`, 'utf8');
|
|
919
|
-
|
|
920
|
-
if (!validation.ok) {
|
|
921
|
-
console.warn(
|
|
922
|
-
`::warning::AI review output normalization fell back to incomplete comment: ${validation.reason}`
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
return { usedFallback: !validation.ok, content };
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
const isMain =
|
|
930
|
-
process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
931
|
-
|
|
932
|
-
if (isMain) {
|
|
933
|
-
writeReviewFromEnv();
|
|
934
|
-
}
|