@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaitranntt/ccs",
3
- "version": "7.63.0-dev.4",
3
+ "version": "7.63.0-dev.5",
4
4
  "description": "Claude Code Switch - Instant profile switching between Claude, GLM, Kimi, and more",
5
5
  "keywords": [
6
6
  "cli",
@@ -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(/([`*_{}\[\]<>])/g, '\\$1');
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 = cleanText(parsed.summary);
68
- const overallAssessment = cleanText(parsed.overallAssessment);
69
- const overallRationale = cleanText(parsed.overallRationale);
70
- const notes = Array.isArray(parsed.notes) ? parsed.notes.map(cleanText).filter(Boolean) : [];
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
- if (!summary || !ASSESSMENTS[overallAssessment] || !overallRationale || findings === null) {
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 = cleanText(finding?.title);
81
- const file = cleanText(finding?.file);
82
- const what = cleanText(finding?.what);
83
- const why = cleanText(finding?.why);
84
- const fix = cleanText(finding?.fix);
85
- const line =
86
- typeof finding?.line === 'number' && Number.isInteger(finding.line) && finding.line > 0
87
- ? finding.line
88
- : null;
89
-
90
- if (!SEVERITY_HEADERS[severity] || !title || !file || !what || !why || !fix) {
91
- return { ok: false, reason: 'structured output contains an invalid finding' };
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({ severity, title, file, line, what, why, fix });
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
- notes,
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 = ['## Summary', escapeMarkdownText(review.summary), '', '## Findings'];
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
- if (review.notes.length > 0) {
135
- lines.push('', '## Notes');
136
- for (const note of review.notes) {
137
- lines.push(`- ${escapeMarkdownText(note)}`);
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
- '## Overall Assessment',
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
- '## AI Review Incomplete',
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