@kaitranntt/ccs 7.63.0-dev.4 → 7.63.0-dev.5
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/package.json
CHANGED
|
@@ -15,12 +15,26 @@ const SEVERITY_HEADERS = {
|
|
|
15
15
|
low: '### 🟢 Low',
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const STATUS_LABELS = {
|
|
19
|
+
pass: '✅',
|
|
20
|
+
fail: '⚠️',
|
|
21
|
+
na: 'N/A',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const RENDERER_OWNED_MARKUP_PATTERNS = [
|
|
25
|
+
{ pattern: /^#{1,6}\s/u, reason: 'markdown heading' },
|
|
26
|
+
{ pattern: /^\s*Verdict\s*:/iu, reason: 'verdict label' },
|
|
27
|
+
{ pattern: /^\s*PR\s*#?\d+\s*Review(?:\s*[:.-]|$)/iu, reason: 'ad hoc PR heading' },
|
|
28
|
+
{ pattern: /\|\s*[-:]+\s*\|/u, reason: 'markdown table' },
|
|
29
|
+
{ pattern: /```/u, reason: 'code fence' },
|
|
30
|
+
];
|
|
31
|
+
|
|
18
32
|
function cleanText(value) {
|
|
19
33
|
return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ') : '';
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
function escapeMarkdownText(value) {
|
|
23
|
-
return cleanText(value).replace(/\\/g, '\\\\').replace(/([`*_{}
|
|
37
|
+
return cleanText(value).replace(/\\/g, '\\\\').replace(/([`*_{}[\]<>|])/g, '\\$1');
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
function renderCode(value) {
|
|
@@ -30,6 +44,63 @@ function renderCode(value) {
|
|
|
30
44
|
return `${fence}${text}${fence}`;
|
|
31
45
|
}
|
|
32
46
|
|
|
47
|
+
function validatePlainTextField(fieldName, value) {
|
|
48
|
+
const text = cleanText(value);
|
|
49
|
+
if (!text) {
|
|
50
|
+
return { ok: false, reason: `${fieldName} is required` };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const match = RENDERER_OWNED_MARKUP_PATTERNS.find(({ pattern }) => pattern.test(text));
|
|
54
|
+
if (match) {
|
|
55
|
+
return { ok: false, reason: `${fieldName} contains ${match.reason}` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { ok: true, value: text };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeStringList(fieldName, raw) {
|
|
62
|
+
if (!Array.isArray(raw)) {
|
|
63
|
+
return { ok: false, reason: `${fieldName} must be an array` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const values = [];
|
|
67
|
+
for (const [index, item] of raw.entries()) {
|
|
68
|
+
const validation = validatePlainTextField(`${fieldName}[${index}]`, item);
|
|
69
|
+
if (!validation.ok) return validation;
|
|
70
|
+
values.push(validation.value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { ok: true, value: values };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeChecklistRows(fieldName, labelField, raw) {
|
|
77
|
+
if (!Array.isArray(raw)) {
|
|
78
|
+
return { ok: false, reason: `${fieldName} must be an array` };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rows = [];
|
|
82
|
+
for (const [index, item] of raw.entries()) {
|
|
83
|
+
const label = validatePlainTextField(`${fieldName}[${index}].${labelField}`, item?.[labelField]);
|
|
84
|
+
if (!label.ok) return label;
|
|
85
|
+
|
|
86
|
+
const notes = validatePlainTextField(`${fieldName}[${index}].notes`, item?.notes);
|
|
87
|
+
if (!notes.ok) return notes;
|
|
88
|
+
|
|
89
|
+
const status = cleanText(item?.status).toLowerCase();
|
|
90
|
+
if (!STATUS_LABELS[status]) {
|
|
91
|
+
return { ok: false, reason: `${fieldName}[${index}].status is invalid` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
rows.push({ [labelField]: label.value, status, notes: notes.value });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (rows.length === 0) {
|
|
98
|
+
return { ok: false, reason: `${fieldName} must contain at least 1 item` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { ok: true, value: rows };
|
|
102
|
+
}
|
|
103
|
+
|
|
33
104
|
function readExecutionMetadata(executionFile) {
|
|
34
105
|
if (!executionFile || !fs.existsSync(executionFile)) {
|
|
35
106
|
return {};
|
|
@@ -64,50 +135,106 @@ export function normalizeStructuredOutput(raw) {
|
|
|
64
135
|
return { ok: false, reason: 'structured output must be an object' };
|
|
65
136
|
}
|
|
66
137
|
|
|
67
|
-
const summary =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
138
|
+
const summary = validatePlainTextField('summary', parsed.summary);
|
|
139
|
+
if (!summary.ok) return summary;
|
|
140
|
+
|
|
141
|
+
const overallAssessment = cleanText(parsed.overallAssessment).toLowerCase();
|
|
142
|
+
const overallRationale = validatePlainTextField('overallRationale', parsed.overallRationale);
|
|
143
|
+
if (!overallRationale.ok) return overallRationale;
|
|
144
|
+
|
|
71
145
|
const findings = Array.isArray(parsed.findings) ? parsed.findings : null;
|
|
146
|
+
const securityChecklist = normalizeChecklistRows('securityChecklist', 'check', parsed.securityChecklist);
|
|
147
|
+
if (!securityChecklist.ok) return securityChecklist;
|
|
148
|
+
|
|
149
|
+
const ccsCompliance = normalizeChecklistRows('ccsCompliance', 'rule', parsed.ccsCompliance);
|
|
150
|
+
if (!ccsCompliance.ok) return ccsCompliance;
|
|
151
|
+
|
|
152
|
+
const informational = normalizeStringList('informational', parsed.informational);
|
|
153
|
+
if (!informational.ok) return informational;
|
|
72
154
|
|
|
73
|
-
|
|
155
|
+
const strengths = normalizeStringList('strengths', parsed.strengths);
|
|
156
|
+
if (!strengths.ok) return strengths;
|
|
157
|
+
|
|
158
|
+
if (!ASSESSMENTS[overallAssessment] || findings === null) {
|
|
74
159
|
return { ok: false, reason: 'structured output is missing required review fields' };
|
|
75
160
|
}
|
|
76
161
|
|
|
77
162
|
const normalizedFindings = [];
|
|
78
|
-
for (const finding of findings) {
|
|
79
|
-
const severity = cleanText(finding?.severity);
|
|
80
|
-
const title =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (!
|
|
91
|
-
|
|
163
|
+
for (const [index, finding] of findings.entries()) {
|
|
164
|
+
const severity = cleanText(finding?.severity).toLowerCase();
|
|
165
|
+
const title = validatePlainTextField(`findings[${index}].title`, finding?.title);
|
|
166
|
+
if (!title.ok) return title;
|
|
167
|
+
|
|
168
|
+
const file = validatePlainTextField(`findings[${index}].file`, finding?.file);
|
|
169
|
+
if (!file.ok) return file;
|
|
170
|
+
|
|
171
|
+
const what = validatePlainTextField(`findings[${index}].what`, finding?.what);
|
|
172
|
+
if (!what.ok) return what;
|
|
173
|
+
|
|
174
|
+
const why = validatePlainTextField(`findings[${index}].why`, finding?.why);
|
|
175
|
+
if (!why.ok) return why;
|
|
176
|
+
|
|
177
|
+
const fix = validatePlainTextField(`findings[${index}].fix`, finding?.fix);
|
|
178
|
+
if (!fix.ok) return fix;
|
|
179
|
+
|
|
180
|
+
let line = null;
|
|
181
|
+
if (finding && Object.hasOwn(finding, 'line')) {
|
|
182
|
+
if (finding.line === null) {
|
|
183
|
+
line = null;
|
|
184
|
+
} else if (typeof finding.line === 'number' && Number.isInteger(finding.line) && finding.line > 0) {
|
|
185
|
+
line = finding.line;
|
|
186
|
+
} else {
|
|
187
|
+
return { ok: false, reason: `findings[${index}].line is invalid` };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!SEVERITY_HEADERS[severity]) {
|
|
192
|
+
return { ok: false, reason: `findings[${index}].severity is invalid` };
|
|
92
193
|
}
|
|
93
194
|
|
|
94
|
-
normalizedFindings.push({
|
|
195
|
+
normalizedFindings.push({
|
|
196
|
+
severity,
|
|
197
|
+
title: title.value,
|
|
198
|
+
file: file.value,
|
|
199
|
+
line,
|
|
200
|
+
what: what.value,
|
|
201
|
+
why: why.value,
|
|
202
|
+
fix: fix.value,
|
|
203
|
+
});
|
|
95
204
|
}
|
|
96
205
|
|
|
97
206
|
return {
|
|
98
207
|
ok: true,
|
|
99
208
|
value: {
|
|
100
|
-
summary,
|
|
209
|
+
summary: summary.value,
|
|
101
210
|
findings: normalizedFindings,
|
|
102
211
|
overallAssessment,
|
|
103
|
-
overallRationale,
|
|
104
|
-
|
|
212
|
+
overallRationale: overallRationale.value,
|
|
213
|
+
securityChecklist: securityChecklist.value,
|
|
214
|
+
ccsCompliance: ccsCompliance.value,
|
|
215
|
+
informational: informational.value,
|
|
216
|
+
strengths: strengths.value,
|
|
105
217
|
},
|
|
106
218
|
};
|
|
107
219
|
}
|
|
108
220
|
|
|
221
|
+
function renderChecklistTable(title, labelHeader, labelKey, rows) {
|
|
222
|
+
const lines = ['', title, '', `| ${labelHeader} | Status | Notes |`, '|---|---|---|'];
|
|
223
|
+
for (const row of rows) {
|
|
224
|
+
lines.push(
|
|
225
|
+
`| ${escapeMarkdownText(row[labelKey])} | ${STATUS_LABELS[row.status]} | ${escapeMarkdownText(row.notes)} |`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return lines;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function renderBulletSection(title, items) {
|
|
232
|
+
if (items.length === 0) return [];
|
|
233
|
+
return ['', title, ...items.map((item) => `- ${escapeMarkdownText(item)}`)];
|
|
234
|
+
}
|
|
235
|
+
|
|
109
236
|
export function renderStructuredReview(review, { model }) {
|
|
110
|
-
const lines = ['
|
|
237
|
+
const lines = ['### 📋 Summary', '', escapeMarkdownText(review.summary), '', '### 🔍 Findings'];
|
|
111
238
|
|
|
112
239
|
if (review.findings.length === 0) {
|
|
113
240
|
lines.push('No confirmed issues found after reviewing the diff and surrounding code.');
|
|
@@ -116,34 +243,31 @@ export function renderStructuredReview(review, { model }) {
|
|
|
116
243
|
const findings = review.findings.filter((finding) => finding.severity === severity);
|
|
117
244
|
if (findings.length === 0) continue;
|
|
118
245
|
|
|
119
|
-
lines.push(SEVERITY_HEADERS[severity]);
|
|
246
|
+
lines.push('', SEVERITY_HEADERS[severity], '');
|
|
120
247
|
for (const finding of findings) {
|
|
121
248
|
const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
|
122
249
|
lines.push(`- **${renderCode(location)} — ${escapeMarkdownText(finding.title)}**`);
|
|
123
250
|
lines.push(` Problem: ${escapeMarkdownText(finding.what)}`);
|
|
124
251
|
lines.push(` Why it matters: ${escapeMarkdownText(finding.why)}`);
|
|
125
252
|
lines.push(` Suggested fix: ${escapeMarkdownText(finding.fix)}`);
|
|
253
|
+
lines.push('');
|
|
126
254
|
}
|
|
127
|
-
lines.push('');
|
|
128
|
-
}
|
|
129
|
-
if (lines[lines.length - 1] === '') {
|
|
130
|
-
lines.pop();
|
|
131
255
|
}
|
|
256
|
+
if (lines[lines.length - 1] === '') lines.pop();
|
|
132
257
|
}
|
|
133
258
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
}
|
|
259
|
+
lines.push(...renderChecklistTable('### 🔒 Security Checklist', 'Check', 'check', review.securityChecklist));
|
|
260
|
+
lines.push(...renderChecklistTable('### 📊 CCS Compliance', 'Rule', 'rule', review.ccsCompliance));
|
|
261
|
+
lines.push(...renderBulletSection('### 💡 Informational', review.informational));
|
|
262
|
+
lines.push(...renderBulletSection("### ✅ What's Done Well", review.strengths));
|
|
140
263
|
|
|
141
264
|
lines.push(
|
|
142
265
|
'',
|
|
143
|
-
'
|
|
266
|
+
'### 🎯 Overall Assessment',
|
|
267
|
+
'',
|
|
144
268
|
`**${ASSESSMENTS[review.overallAssessment]}** — ${escapeMarkdownText(review.overallRationale)}`,
|
|
145
269
|
'',
|
|
146
|
-
`> Reviewed by \`${model}\``
|
|
270
|
+
`> 🤖 Reviewed by \`${model}\``
|
|
147
271
|
);
|
|
148
272
|
|
|
149
273
|
return lines.join('\n');
|
|
@@ -151,7 +275,7 @@ export function renderStructuredReview(review, { model }) {
|
|
|
151
275
|
|
|
152
276
|
export function renderIncompleteReview({ model, reason, runUrl, runtimeTools, turnsUsed }) {
|
|
153
277
|
const lines = [
|
|
154
|
-
'
|
|
278
|
+
'### ⚠️ AI Review Incomplete',
|
|
155
279
|
'',
|
|
156
280
|
'Claude did not return validated structured review output, so this workflow did not publish raw scratch text.',
|
|
157
281
|
'',
|
|
@@ -165,7 +289,7 @@ export function renderIncompleteReview({ model, reason, runUrl, runtimeTools, tu
|
|
|
165
289
|
lines.push(`- Turns used: ${turnsUsed}`);
|
|
166
290
|
}
|
|
167
291
|
|
|
168
|
-
lines.push('', `Re-run \`/review\` or inspect [the workflow run](${runUrl}).`, '', `> Reviewed by \`${model}\``);
|
|
292
|
+
lines.push('', `Re-run \`/review\` or inspect [the workflow run](${runUrl}).`, '', `> 🤖 Reviewed by \`${model}\``);
|
|
169
293
|
return lines.join('\n');
|
|
170
294
|
}
|
|
171
295
|
|