@kaitranntt/ccs 7.65.0 → 7.65.1

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.65.0",
3
+ "version": "7.65.1",
4
4
  "description": "Claude Code Switch - Instant profile switching between Claude, GLM, Kimi, and more",
5
5
  "keywords": [
6
6
  "cli",
@@ -290,6 +290,66 @@ function readExecutionMetadata(executionFile) {
290
290
  }
291
291
  }
292
292
 
293
+ function readSelectedFiles(manifestFile) {
294
+ if (!manifestFile || !fs.existsSync(manifestFile)) {
295
+ return [];
296
+ }
297
+
298
+ try {
299
+ return fs
300
+ .readFileSync(manifestFile, 'utf8')
301
+ .split('\n')
302
+ .map((line) => cleanText(line))
303
+ .filter(Boolean);
304
+ } catch {
305
+ return [];
306
+ }
307
+ }
308
+
309
+ function formatHotspotFiles(files) {
310
+ if (!files.length) {
311
+ return null;
312
+ }
313
+
314
+ const visible = files.slice(0, 4).map(renderCode).join(', ');
315
+ return files.length > 4 ? `${visible}, and ${files.length - 4} more` : visible;
316
+ }
317
+
318
+ function formatRemainingCoverage(rendering) {
319
+ if (
320
+ typeof rendering.selectedFiles !== 'number' ||
321
+ typeof rendering.reviewableFiles !== 'number'
322
+ ) {
323
+ return null;
324
+ }
325
+
326
+ const remainingFiles = Math.max(rendering.reviewableFiles - rendering.selectedFiles, 0);
327
+ const hasChangeCounts =
328
+ typeof rendering.selectedChanges === 'number' &&
329
+ typeof rendering.reviewableChanges === 'number';
330
+ const remainingChanges = hasChangeCounts
331
+ ? Math.max(rendering.reviewableChanges - rendering.selectedChanges, 0)
332
+ : null;
333
+
334
+ if (remainingFiles === 0 && (!hasChangeCounts || remainingChanges === 0)) {
335
+ return null;
336
+ }
337
+
338
+ if (typeof remainingChanges === 'number') {
339
+ return `${remainingFiles} file${remainingFiles === 1 ? '' : 's'}; ${remainingChanges} changed lines`;
340
+ }
341
+
342
+ return `${remainingFiles} file${remainingFiles === 1 ? '' : 's'}`;
343
+ }
344
+
345
+ function formatFallbackFollowUp(rendering) {
346
+ if (rendering.mode === 'triage') {
347
+ return 'Focus manual review on the hotspot files above, and use `/review` for a deeper pass when release, auth, config, or workflow paths changed.';
348
+ }
349
+
350
+ return 'Use `/review` when you need a deeper maintainer rerun with more surrounding context.';
351
+ }
352
+
293
353
  export function normalizeStructuredOutput(raw) {
294
354
  if (!raw) {
295
355
  return { ok: false, reason: 'missing structured output' };
@@ -461,6 +521,7 @@ export function renderIncompleteReview({
461
521
  runUrl,
462
522
  runtimeTools,
463
523
  turnsUsed,
524
+ selectedFiles,
464
525
  rendering: renderOptions,
465
526
  status,
466
527
  }) {
@@ -468,7 +529,7 @@ export function renderIncompleteReview({
468
529
  const lines = [
469
530
  '### ⚠️ AI Review Incomplete',
470
531
  '',
471
- 'Claude did not return validated structured review output, so this workflow did not publish raw scratch text.',
532
+ 'Claude did not return validated structured review output, so this workflow published deterministic hotspot context instead of raw scratch text.',
472
533
  '',
473
534
  `- Outcome: ${describeIncompleteOutcome({ reason, rendering, turnsUsed, status })}`,
474
535
  ];
@@ -484,6 +545,15 @@ export function renderIncompleteReview({
484
545
  if (runtimeBudget) {
485
546
  lines.push(`- Runtime budget: ${escapeMarkdownText(runtimeBudget)}`);
486
547
  }
548
+ const hotspotFiles = formatHotspotFiles(selectedFiles || []);
549
+ if (hotspotFiles) {
550
+ lines.push(`- Hotspot files in this pass: ${hotspotFiles}`);
551
+ }
552
+ const remainingCoverage = formatRemainingCoverage(rendering);
553
+ if (remainingCoverage) {
554
+ lines.push(`- Remaining reviewable scope not fully covered: ${escapeMarkdownText(remainingCoverage)}`);
555
+ }
556
+ lines.push(`- Manual follow-up: ${escapeMarkdownText(formatFallbackFollowUp(rendering))}`);
487
557
  if (runtimeTools?.length) {
488
558
  lines.push(`- Runtime tools: ${runtimeTools.map(renderCode).join(', ')}`);
489
559
  }
@@ -501,6 +571,7 @@ export function writeReviewFromEnv(env = process.env) {
501
571
  const runUrl = env.AI_REVIEW_RUN_URL || '#';
502
572
  const validation = normalizeStructuredOutput(env.AI_REVIEW_STRUCTURED_OUTPUT);
503
573
  const metadata = readExecutionMetadata(env.AI_REVIEW_EXECUTION_FILE);
574
+ const selectedFiles = readSelectedFiles(env.AI_REVIEW_SCOPE_MANIFEST_FILE);
504
575
  const status = cleanText(env.AI_REVIEW_STATUS).toLowerCase() || null;
505
576
  const rendering = normalizeRenderingMetadata({
506
577
  mode: env.AI_REVIEW_MODE,
@@ -521,6 +592,7 @@ export function writeReviewFromEnv(env = process.env) {
521
592
  runUrl,
522
593
  runtimeTools: metadata.runtimeTools,
523
594
  turnsUsed: metadata.turnsUsed,
595
+ selectedFiles,
524
596
  rendering,
525
597
  status,
526
598
  });
@@ -4,10 +4,14 @@ import { fileURLToPath } from 'node:url';
4
4
 
5
5
  const MODE_LIMITS = {
6
6
  fast: { maxFiles: 16, maxChangedLines: 900, maxPatchLines: 90, maxPatchChars: 7000 },
7
- triage: { maxFiles: 10, maxChangedLines: 700, maxPatchLines: 80, maxPatchChars: 6000 },
7
+ triage: { maxFiles: 6, maxChangedLines: 520, maxPatchLines: 60, maxPatchChars: 4500 },
8
8
  deep: { maxFiles: 20, maxChangedLines: 1600, maxPatchLines: 120, maxPatchChars: 9000 },
9
9
  };
10
10
 
11
+ const TRIAGE_SIZE_CLASS_LIMITS = {
12
+ xlarge: { maxFiles: 4, maxChangedLines: 360, maxPatchLines: 45, maxPatchChars: 3200 },
13
+ };
14
+
11
15
  const MODE_LABELS = {
12
16
  fast: 'diff-focused bounded review',
13
17
  triage: 'hotspot-based bounded review (non-exhaustive)',
@@ -35,6 +39,13 @@ function cleanText(value) {
35
39
  return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ') : '';
36
40
  }
37
41
 
42
+ function normalizeSizeClass(value) {
43
+ const sizeClass = cleanText(value).toLowerCase();
44
+ return sizeClass === 'small' || sizeClass === 'medium' || sizeClass === 'large' || sizeClass === 'xlarge'
45
+ ? sizeClass
46
+ : null;
47
+ }
48
+
38
49
  function escapeMarkdown(value) {
39
50
  return cleanText(value).replace(/\\/g, '\\\\').replace(/([`*_{}[\]<>|])/g, '\\$1');
40
51
  }
@@ -127,8 +138,17 @@ export function normalizePullFiles(files) {
127
138
  }).map((file) => ({ ...file, score: scoreFile(file) }));
128
139
  }
129
140
 
130
- export function buildReviewScope(files, mode) {
131
- const limits = MODE_LIMITS[mode] || MODE_LIMITS.fast;
141
+ function resolveModeLimits(mode, sizeClass) {
142
+ if (mode !== 'triage') {
143
+ return MODE_LIMITS[mode] || MODE_LIMITS.fast;
144
+ }
145
+
146
+ return TRIAGE_SIZE_CLASS_LIMITS[sizeClass] || MODE_LIMITS.triage;
147
+ }
148
+
149
+ export function buildReviewScope(files, mode, options = {}) {
150
+ const sizeClass = normalizeSizeClass(options.sizeClass);
151
+ const limits = resolveModeLimits(mode, sizeClass);
132
152
  const reviewable = files.filter((file) => file.reviewable);
133
153
  const lowSignal = files.filter((file) => !file.reviewable);
134
154
  const usingChangedFallback = reviewable.length === 0;
@@ -260,6 +280,7 @@ export async function writeScopeFromEnv(env = process.env, request) {
260
280
  const prNumber = Number.parseInt(cleanText(env.AI_REVIEW_PR_NUMBER), 10);
261
281
  const baseRef = cleanText(env.AI_REVIEW_BASE_REF || 'dev');
262
282
  const mode = cleanText(env.AI_REVIEW_MODE || 'fast').toLowerCase();
283
+ const sizeClass = normalizeSizeClass(env.AI_REVIEW_PR_SIZE_CLASS);
263
284
  const turnBudget = Number.parseInt(cleanText(env.AI_REVIEW_MAX_TURNS || '0'), 10) || 0;
264
285
  const timeoutMinutes = Number.parseInt(cleanText(env.AI_REVIEW_TIMEOUT_MINUTES || '0'), 10) || 0;
265
286
  const outputFile = env.AI_REVIEW_SCOPE_FILE || '.ccs-ai-review-scope.md';
@@ -287,7 +308,7 @@ export async function writeScopeFromEnv(env = process.env, request) {
287
308
  const files = normalizePullFiles(
288
309
  await collectPullRequestFiles(`${apiUrl}/repos/${repository}/pulls/${prNumber}/files?per_page=100`, fetchPage)
289
310
  );
290
- const scope = buildReviewScope(files, mode);
311
+ const scope = buildReviewScope(files, mode, { sizeClass });
291
312
  const markdown = renderReviewScope({ prNumber, baseRef, turnBudget, timeoutMinutes, scope });
292
313
 
293
314
  fs.mkdirSync(path.dirname(outputFile), { recursive: true });